diff --git a/app/Http/Controllers/Web/CategoryController.php b/app/Http/Controllers/Web/CategoryController.php new file mode 100644 index 00000000..02b74eee --- /dev/null +++ b/app/Http/Controllers/Web/CategoryController.php @@ -0,0 +1,18 @@ +build(WebPortalContext::fromRequest(), $slug)); + } +} diff --git a/app/Services/Web/CategoryPageData.php b/app/Services/Web/CategoryPageData.php new file mode 100644 index 00000000..129b7342 --- /dev/null +++ b/app/Services/Web/CategoryPageData.php @@ -0,0 +1,218 @@ + + */ + public function build(WebPortalContext $context, string $slug): array + { + $portalValues = $context->portalValues; + $language = $context->language; + + $category = Category::query() + ->whereIn('portal', $portalValues) + ->where('is_active', true) + ->whereHas('translations', fn (Builder $query) => $query->where('locale', $language)->where('slug', $slug)) + ->with([ + 'translations' => fn ($query) => $query->where('locale', $language), + 'parent.translations' => fn ($query) => $query->where('locale', $language), + ]) + ->first(); + + if (! $category) { + abort(404); + } + + $childCategories = Category::query() + ->where('parent_id', $category->id) + ->where('is_active', true) + ->with(['translations' => fn ($query) => $query->where('locale', $language)]) + ->get(); + + $categoryIds = collect([$category->id])->merge($childCategories->pluck('id'))->unique()->values()->all(); + + $base = static fn (): Builder => PressRelease::query() + ->withoutGlobalScope(PortalScope::class) + ->forPortalValues($portalValues) + ->forLanguage($language) + ->visibleInArchive() + ->whereIn('category_id', $categoryIds); + + // Kategoriespezifischer Stichtag: jüngste Meldung dieser Branche. + $latest = $base()->max('published_at'); + $referenceNow = $latest ? Carbon::parse($latest) : now(); + if ($referenceNow->gt(now())) { + $referenceNow = now(); + } + $recentSince = $referenceNow->copy()->subDays(7); + $previousSince = $referenceNow->copy()->subDays(14); + $monthSince = $referenceNow->copy()->subDays(30); + + $with = [ + 'company', + 'category.translations' => fn ($query) => $query->where('locale', $language), + 'images' => fn ($query) => $query->orderByDesc('is_preview')->orderBy('sort_order')->limit(1), + ]; + + $leadRelease = $base()->with($with)->orderByDesc('published_at')->first(); + + $topReleases = $base() + ->with($with) + ->when($leadRelease, fn (Builder $query) => $query->whereKeyNot($leadRelease->getKey())) + ->orderByDesc('published_at') + ->limit(2) + ->get(); + + $excludeIds = collect([$leadRelease?->getKey()])->merge($topReleases->modelKeys())->filter()->all(); + + $feedReleases = $base() + ->with($with) + ->when($excludeIds !== [], fn (Builder $query) => $query->whereNotIn('id', $excludeIds)) + ->orderByDesc('published_at') + ->limit(12) + ->get(); + + $mostReadReleases = $base() + ->orderByDesc('hits') + ->orderByDesc('published_at') + ->limit(5) + ->get(['id', 'slug', 'title', 'hits', 'portal', 'language']); + + $stats = [ + 'today' => $base()->whereDate('published_at', $referenceNow->toDateString())->count(), + 'week' => $base()->where('published_at', '>=', $recentSince)->count(), + 'previousWeek' => $base()->where('published_at', '>=', $previousSince)->where('published_at', '<', $recentSince)->count(), + 'month' => $base()->where('published_at', '>=', $monthSince)->count(), + 'total' => $base()->count(), + 'activeCompanies' => $base()->where('published_at', '>=', $recentSince)->distinct()->count('company_id'), + 'totalCompanies' => $base()->distinct()->count('company_id'), + ]; + + $newsroomFilter = static function (Builder $query) use ($portalValues, $categoryIds, $language): void { + $query + ->forPortalValues($portalValues) + ->forLanguage($language) + ->published() + ->whereIn('category_id', $categoryIds); + }; + + $topNewsrooms = Company::query() + ->withoutGlobalScope(PortalScope::class) + ->whereIn('portal', $portalValues) + ->where('is_active', true) + ->whereHas('pressReleases', function (Builder $query) use ($newsroomFilter, $recentSince): void { + $newsroomFilter($query); + $query->where('published_at', '>=', $recentSince); + }) + ->withCount([ + 'pressReleases as recent_releases_count' => function (Builder $query) use ($newsroomFilter, $recentSince): void { + $newsroomFilter($query); + $query->where('published_at', '>=', $recentSince); + }, + 'pressReleases as today_releases_count' => function (Builder $query) use ($newsroomFilter, $referenceNow): void { + $newsroomFilter($query); + $query->whereDate('published_at', $referenceNow->toDateString()); + }, + ]) + ->orderByDesc('recent_releases_count') + ->limit(6) + ->get() + ->map(fn (Company $company): array => [ + 'name' => $company->name, + 'slug' => $company->slug, + 'initial' => mb_strtoupper(mb_substr((string) $company->name, 0, 1)), + 'count' => (int) $company->recent_releases_count, + 'today' => (int) $company->today_releases_count > 0, + ]); + + $publishedCount = static fn (Builder $query) => $query + ->forPortalValues($portalValues) + ->forLanguage($language) + ->published(); + + $subCategories = $childCategories + ->map(function (Category $child) use ($recentSince, $previousSince, $publishedCount): ?array { + $translation = $child->translations->first(); + + if (! $translation) { + return null; + } + + $total = PressRelease::query()->withoutGlobalScope(PortalScope::class) + ->where('category_id', $child->id)->tap($publishedCount)->count(); + $recent = PressRelease::query()->withoutGlobalScope(PortalScope::class) + ->where('category_id', $child->id)->tap($publishedCount)->where('published_at', '>=', $recentSince)->count(); + $previous = PressRelease::query()->withoutGlobalScope(PortalScope::class) + ->where('category_id', $child->id)->tap($publishedCount) + ->where('published_at', '>=', $previousSince)->where('published_at', '<', $recentSince)->count(); + + return [ + 'name' => $translation->name, + 'href' => $translation->slug ? route('kategorie', ['slug' => $translation->slug]) : route('web.home'), + 'total' => $total, + 'recent' => $recent, + 'delta' => $recent - $previous, + ]; + }) + ->filter() + ->sortByDesc('total') + ->values(); + + $relatedCategories = Category::query() + ->whereIn('portal', $portalValues) + ->where('is_active', true) + ->whereNull('parent_id') + ->whereKeyNot($category->id) + ->when($category->parent_id, fn (Builder $query) => $query->whereKeyNot($category->parent_id)) + ->with(['translations' => fn ($query) => $query->where('locale', $language)]) + ->withCount([ + 'pressReleases as total_count' => fn (Builder $query) => $publishedCount($query), + ]) + ->orderByDesc('total_count') + ->limit(8) + ->get() + ->map(function (Category $related): ?array { + $translation = $related->translations->first(); + + if (! $translation) { + return null; + } + + return [ + 'name' => $translation->name, + 'href' => $translation->slug ? route('kategorie', ['slug' => $translation->slug]) : route('web.home'), + 'count' => (int) ($related->total_count ?? 0), + ]; + }) + ->filter() + ->values(); + + return [ + 'category' => $category, + 'referenceNow' => $referenceNow, + 'leadRelease' => $leadRelease, + 'topReleases' => $topReleases, + 'feedReleases' => $feedReleases, + 'mostReadReleases' => $mostReadReleases, + 'categoryStats' => $stats, + 'topNewsrooms' => $topNewsrooms, + 'subCategories' => $subCategories, + 'relatedCategories' => $relatedCategories, + ]; + } +} diff --git a/routes/web.php b/routes/web.php index 471fe584..64bd8465 100644 --- a/routes/web.php +++ b/routes/web.php @@ -2,6 +2,7 @@ use App\Enums\Portal; use App\Http\Controllers\PressReleasePreviewController; +use App\Http\Controllers\Web\CategoryController; use App\Http\Controllers\Web\NewsroomDetailController; use App\Http\Controllers\Web\NewsroomsController; use App\Http\Controllers\Web\PreiseController; @@ -253,204 +254,6 @@ $webHomeData = static function (Portal $primaryPortal, string $language = 'de'): ]; }; -// Kategorie-/Branchenseite je Ausgabe. -$categoryPage = static function (string $edition, string $slug) { - $context = WebPortalContext::fromRequest(); - $language = $context->language; - $portalValues = $context->portalValues; - - $category = Category::query() - ->whereIn('portal', $portalValues) - ->where('is_active', true) - ->whereHas('translations', fn (Builder $query) => $query->where('locale', $language)->where('slug', $slug)) - ->with([ - 'translations' => fn ($query) => $query->where('locale', $language), - 'parent.translations' => fn ($query) => $query->where('locale', $language), - ]) - ->first(); - - if (! $category) { - abort(404); - } - - $childCategories = Category::query() - ->where('parent_id', $category->id) - ->where('is_active', true) - ->with(['translations' => fn ($query) => $query->where('locale', $language)]) - ->get(); - - $categoryIds = collect([$category->id])->merge($childCategories->pluck('id'))->unique()->values()->all(); - - $base = static fn (): Builder => PressRelease::query() - ->withoutGlobalScope(PortalScope::class) - ->forPortalValues($portalValues) - ->forLanguage($language) - ->visibleInArchive() - ->whereIn('category_id', $categoryIds); - - // Kategoriespezifischer Stichtag: jüngste Meldung dieser Branche. - $latest = $base()->max('published_at'); - $referenceNow = $latest ? Carbon::parse($latest) : now(); - if ($referenceNow->gt(now())) { - $referenceNow = now(); - } - $recentSince = $referenceNow->copy()->subDays(7); - $previousSince = $referenceNow->copy()->subDays(14); - $monthSince = $referenceNow->copy()->subDays(30); - - $with = [ - 'company', - 'category.translations' => fn ($query) => $query->where('locale', $language), - 'images' => fn ($query) => $query->orderByDesc('is_preview')->orderBy('sort_order')->limit(1), - ]; - - $leadRelease = $base()->with($with)->orderByDesc('published_at')->first(); - - $topReleases = $base() - ->with($with) - ->when($leadRelease, fn (Builder $query) => $query->whereKeyNot($leadRelease->getKey())) - ->orderByDesc('published_at') - ->limit(2) - ->get(); - - $excludeIds = collect([$leadRelease?->getKey()])->merge($topReleases->modelKeys())->filter()->all(); - - $feedReleases = $base() - ->with($with) - ->when($excludeIds !== [], fn (Builder $query) => $query->whereNotIn('id', $excludeIds)) - ->orderByDesc('published_at') - ->limit(12) - ->get(); - - $mostReadReleases = $base() - ->orderByDesc('hits') - ->orderByDesc('published_at') - ->limit(5) - ->get(['id', 'slug', 'title', 'hits', 'portal', 'language']); - - $stats = [ - 'today' => $base()->whereDate('published_at', $referenceNow->toDateString())->count(), - 'week' => $base()->where('published_at', '>=', $recentSince)->count(), - 'previousWeek' => $base()->where('published_at', '>=', $previousSince)->where('published_at', '<', $recentSince)->count(), - 'month' => $base()->where('published_at', '>=', $monthSince)->count(), - 'total' => $base()->count(), - 'activeCompanies' => $base()->where('published_at', '>=', $recentSince)->distinct()->count('company_id'), - 'totalCompanies' => $base()->distinct()->count('company_id'), - ]; - - $newsroomFilter = static function (Builder $query) use ($portalValues, $categoryIds, $language): void { - $query - ->forPortalValues($portalValues) - ->forLanguage($language) - ->published() - ->whereIn('category_id', $categoryIds); - }; - - $topNewsrooms = Company::query() - ->withoutGlobalScope(PortalScope::class) - ->whereIn('portal', $portalValues) - ->where('is_active', true) - ->whereHas('pressReleases', function (Builder $query) use ($newsroomFilter, $recentSince): void { - $newsroomFilter($query); - $query->where('published_at', '>=', $recentSince); - }) - ->withCount([ - 'pressReleases as recent_releases_count' => function (Builder $query) use ($newsroomFilter, $recentSince): void { - $newsroomFilter($query); - $query->where('published_at', '>=', $recentSince); - }, - 'pressReleases as today_releases_count' => function (Builder $query) use ($newsroomFilter, $referenceNow): void { - $newsroomFilter($query); - $query->whereDate('published_at', $referenceNow->toDateString()); - }, - ]) - ->orderByDesc('recent_releases_count') - ->limit(6) - ->get() - ->map(fn (Company $company): array => [ - 'name' => $company->name, - 'slug' => $company->slug, - 'initial' => mb_strtoupper(mb_substr((string) $company->name, 0, 1)), - 'count' => (int) $company->recent_releases_count, - 'today' => (int) $company->today_releases_count > 0, - ]); - - $publishedCount = static fn (Builder $query) => $query - ->forPortalValues($portalValues) - ->forLanguage($language) - ->published(); - - $subCategories = $childCategories - ->map(function (Category $child) use ($recentSince, $previousSince, $publishedCount): ?array { - $translation = $child->translations->first(); - - if (! $translation) { - return null; - } - - $total = PressRelease::query()->withoutGlobalScope(PortalScope::class) - ->where('category_id', $child->id)->tap($publishedCount)->count(); - $recent = PressRelease::query()->withoutGlobalScope(PortalScope::class) - ->where('category_id', $child->id)->tap($publishedCount)->where('published_at', '>=', $recentSince)->count(); - $previous = PressRelease::query()->withoutGlobalScope(PortalScope::class) - ->where('category_id', $child->id)->tap($publishedCount) - ->where('published_at', '>=', $previousSince)->where('published_at', '<', $recentSince)->count(); - - return [ - 'name' => $translation->name, - 'href' => $translation->slug ? route('kategorie', ['slug' => $translation->slug]) : route('web.home'), - 'total' => $total, - 'recent' => $recent, - 'delta' => $recent - $previous, - ]; - }) - ->filter() - ->sortByDesc('total') - ->values(); - - $relatedCategories = Category::query() - ->whereIn('portal', $portalValues) - ->where('is_active', true) - ->whereNull('parent_id') - ->whereKeyNot($category->id) - ->when($category->parent_id, fn (Builder $query) => $query->whereKeyNot($category->parent_id)) - ->with(['translations' => fn ($query) => $query->where('locale', $language)]) - ->withCount([ - 'pressReleases as total_count' => fn (Builder $query) => $publishedCount($query), - ]) - ->orderByDesc('total_count') - ->limit(8) - ->get() - ->map(function (Category $related): ?array { - $translation = $related->translations->first(); - - if (! $translation) { - return null; - } - - return [ - 'name' => $translation->name, - 'href' => $translation->slug ? route('kategorie', ['slug' => $translation->slug]) : route('web.home'), - 'count' => (int) ($related->total_count ?? 0), - ]; - }) - ->filter() - ->values(); - - return view('web.kategorie', [ - 'category' => $category, - 'referenceNow' => $referenceNow, - 'leadRelease' => $leadRelease, - 'topReleases' => $topReleases, - 'feedReleases' => $feedReleases, - 'mostReadReleases' => $mostReadReleases, - 'categoryStats' => $stats, - 'topNewsrooms' => $topNewsrooms, - 'subCategories' => $subCategories, - 'relatedCategories' => $relatedCategories, - ]); -}; - // Detailseite einer Pressemitteilung je Ausgabe. $releaseDetailPage = static function (string $edition, string $slug) { $context = WebPortalContext::fromRequest(); @@ -664,7 +467,7 @@ Route::get('{path}', function (string $path) use ($htmlRedirect) { // --------------------------------------------------------------------------- // Öffentliche Inhalte je Ausgabe (Sprache): /de/... und /en/... // --------------------------------------------------------------------------- -Route::prefix('{edition}')->whereIn('edition', SetEdition::EDITIONS)->group(function () use ($applyWebDomainConfig, $webHomeData, $categoryPage, $releaseDetailPage) { +Route::prefix('{edition}')->whereIn('edition', SetEdition::EDITIONS)->group(function () use ($applyWebDomainConfig, $webHomeData, $releaseDetailPage) { // Startseite je Ausgabe Route::get('/', function (string $edition) use ($applyWebDomainConfig, $webHomeData) { $domain = request()->getHost(); @@ -684,7 +487,7 @@ Route::prefix('{edition}')->whereIn('edition', SetEdition::EDITIONS)->group(func })->name('web.home'); // Kategorie / Branchenseite (z.B. /de/category/energie-klima) - Route::get('category/{slug}', $categoryPage)->name('kategorie'); + Route::get('category/{slug}', CategoryController::class)->name('kategorie'); // Detailseite einer Pressemitteilung (z.B. /de/press-release/ki-revolution) Route::get('press-release/{slug}', $releaseDetailPage)->name('release.detail');