Snapshot of main at 9f39a6e1e (2026-11-02)
This commit is contained in:
commit
6ccf1792d7
2580 files changed
+1040202
No files matched your search
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\Admin;
|
||||
|
||||
use App\Helpers\SearchOptimizationHelper;
|
||||
use App\Models\Locations\City;
|
||||
use App\Models\Locations\Country;
|
||||
use App\Models\Locations\District;
|
||||
use App\Models\Locations\Division;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Component;
|
||||
|
||||
class SearchMetrics extends Component
|
||||
{
|
||||
public array $metrics = [];
|
||||
public array $recentSearches = [];
|
||||
public string $period = 'last_year';
|
||||
|
||||
public array $periods = [
|
||||
'last_week' => 'Last week',
|
||||
'last_month' => 'Last month',
|
||||
'last_6months' => 'Last 6 months',
|
||||
'last_year' => 'Last year',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
abort(403, 'Unauthorized');
|
||||
}
|
||||
|
||||
if (getActiveProfileType() !== 'Admin') {
|
||||
abort(403, 'Unauthorized');
|
||||
}
|
||||
|
||||
$activeProfile = getActiveProfile();
|
||||
if (!$activeProfile) {
|
||||
abort(403, 'Unauthorized');
|
||||
}
|
||||
|
||||
\App\Helpers\ProfileAuthorizationHelper::authorize($activeProfile);
|
||||
|
||||
if (!$this->hasLogsPermission()) {
|
||||
abort(403, 'Unauthorized');
|
||||
}
|
||||
|
||||
$this->loadMetrics();
|
||||
}
|
||||
|
||||
protected function hasLogsPermission(): bool
|
||||
{
|
||||
$webUser = Auth::guard('web')->user();
|
||||
$activeId = session('activeProfileId');
|
||||
if (!$webUser || !$activeId) return false;
|
||||
$prefix = 'Admin\\' . $activeId . '\\';
|
||||
$scopedRoles = $webUser->roles->filter(fn($r) => str_starts_with($r->name, $prefix));
|
||||
foreach ($scopedRoles as $scopedRole) {
|
||||
$roleName = str_replace($prefix, '', $scopedRole->name);
|
||||
$adminRole = \App\Models\AdminRole::where('name', $roleName)->first();
|
||||
if ($adminRole && in_array('view search-metrics', $adminRole->permissions ?? [])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function refresh()
|
||||
{
|
||||
$this->loadMetrics();
|
||||
}
|
||||
|
||||
public function updatedPeriod()
|
||||
{
|
||||
$this->loadMetrics();
|
||||
}
|
||||
|
||||
private function periodSince(): \Carbon\Carbon
|
||||
{
|
||||
return match ($this->period) {
|
||||
'last_week' => now()->subWeek(),
|
||||
'last_month' => now()->subMonth(),
|
||||
'last_6months' => now()->subMonths(6),
|
||||
default => now()->subMonths(12)->startOfMonth(),
|
||||
};
|
||||
}
|
||||
|
||||
public function downloadLog()
|
||||
{
|
||||
$csv = implode(',', ['time', 'query', 'categories', 'results', 'exec_s', 'lang', 'type', 'location']) . "\n";
|
||||
|
||||
foreach ($this->recentSearches as $row) {
|
||||
$categories = implode('|', array_column($row['category_tags'] ?? [], 'name'));
|
||||
$csv .= implode(',', [
|
||||
\Carbon\Carbon::parse($row['timestamp'])->format('Y-m-d'),
|
||||
'"' . str_replace('"', '""', $row['search_string'] ?? '') . '"',
|
||||
'"' . str_replace('"', '""', $categories) . '"',
|
||||
$row['total_results'] ?? 0,
|
||||
number_format($row['execution_time'] ?? 0, 4),
|
||||
$row['locale'] ?? '',
|
||||
$row['user_type'] ?? '',
|
||||
'"' . str_replace('"', '""', $row['location_label'] ?? '') . '"',
|
||||
]) . "\n";
|
||||
}
|
||||
|
||||
$filename = 'search-metrics-' . now()->format('Y-m') . '.csv';
|
||||
|
||||
return response()->streamDownload(fn () => print($csv), $filename, [
|
||||
'Content-Type' => 'text/csv',
|
||||
]);
|
||||
}
|
||||
|
||||
private function loadMetrics(): void
|
||||
{
|
||||
$since = $this->periodSince();
|
||||
$this->metrics = SearchOptimizationHelper::getSearchMetrics($since);
|
||||
|
||||
$raw = SearchOptimizationHelper::readCurrentMonthEntries(50, $since);
|
||||
|
||||
// Collect all unique category IDs across all entries, then resolve names once
|
||||
$allIds = array_values(array_unique(array_merge(...array_map(
|
||||
fn($s) => $s['category_ids'] ?? [],
|
||||
$raw
|
||||
))));
|
||||
|
||||
$tagMap = [];
|
||||
if (!empty($allIds)) {
|
||||
$locale = app()->getLocale();
|
||||
$categories = \App\Models\Category::whereIn('id', $allIds)
|
||||
->with(['translations' => fn($q) => $q->where('locale', $locale)])
|
||||
->get()
|
||||
->keyBy('id');
|
||||
|
||||
foreach ($allIds as $id) {
|
||||
$cat = $categories->get($id);
|
||||
$tagMap[$id] = [
|
||||
'name' => $cat?->translation?->name ?? $cat?->name ?? $id,
|
||||
'color' => $cat?->relatedColor ?? 'gray',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Collect all unique location IDs and resolve names in one query per type
|
||||
$locale = app()->getLocale();
|
||||
$locationNameMap = $this->resolveLocationNames($raw, $locale);
|
||||
|
||||
// Replace category_ids with resolved name+color tags, and add location label per entry
|
||||
$this->recentSearches = array_map(function ($search) use ($tagMap, $locationNameMap) {
|
||||
$search['category_tags'] = array_map(
|
||||
fn($id) => $tagMap[$id] ?? ['name' => $id, 'color' => 'gray'],
|
||||
$search['category_ids'] ?? []
|
||||
);
|
||||
|
||||
// Build location short label matching LocationTrait::name_short logic:
|
||||
// city + (district or division), or country if no city
|
||||
$city = $locationNameMap['city'][$search['city_id'] ?? null] ?? null;
|
||||
$district = $locationNameMap['district'][$search['district_id'] ?? null] ?? null;
|
||||
$division = $locationNameMap['division'][$search['division_id'] ?? null] ?? null;
|
||||
$country = $locationNameMap['country'][$search['country_id'] ?? null] ?? null;
|
||||
|
||||
$parts = [];
|
||||
if ($city) $parts[] = $city;
|
||||
if ($district && count($parts) < 2) $parts[] = $district;
|
||||
if ($division && count($parts) < 2) $parts[] = $division;
|
||||
if (!$city && $country) $parts[] = $country;
|
||||
|
||||
$search['location_label'] = preg_replace('/\s+,/', ',', implode(' ', $parts)) ?: null;
|
||||
|
||||
return $search;
|
||||
}, $raw);
|
||||
}
|
||||
|
||||
private function resolveLocationNames(array $entries, string $locale): array
|
||||
{
|
||||
$ids = ['country' => [], 'division' => [], 'city' => [], 'district' => []];
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
if (!empty($entry['country_id'])) $ids['country'][] = $entry['country_id'];
|
||||
if (!empty($entry['division_id'])) $ids['division'][] = $entry['division_id'];
|
||||
if (!empty($entry['city_id'])) $ids['city'][] = $entry['city_id'];
|
||||
if (!empty($entry['district_id'])) $ids['district'][] = $entry['district_id'];
|
||||
}
|
||||
|
||||
$resolve = function ($model, array $uniqueIds) use ($locale): array {
|
||||
if (empty($uniqueIds)) return [];
|
||||
return $model::whereIn('id', array_unique($uniqueIds))
|
||||
->with(['translations' => fn($q) => $q->where('locale', $locale)])
|
||||
->get()
|
||||
->keyBy('id')
|
||||
->map(fn($r) => $r->translations->first()?->name ?? $r->name ?? null)
|
||||
->toArray();
|
||||
};
|
||||
|
||||
return [
|
||||
'country' => $resolve(Country::class, $ids['country']),
|
||||
'division' => $resolve(Division::class, $ids['division']),
|
||||
'city' => $resolve(City::class, $ids['city']),
|
||||
'district' => $resolve(District::class, $ids['district']),
|
||||
];
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.admin.search-metrics');
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user