Studio: make grid checkbox rectangular and commit table changes
This commit is contained in:
229
app/Services/Studio/StudioMetricsService.php
Normal file
229
app/Services/Studio/StudioMetricsService.php
Normal file
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Studio;
|
||||
|
||||
use App\Models\Artwork;
|
||||
use App\Models\ArtworkStats;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Provides dashboard KPI data for the Studio overview page.
|
||||
*/
|
||||
final class StudioMetricsService
|
||||
{
|
||||
private const CACHE_TTL = 300; // 5 minutes
|
||||
|
||||
/**
|
||||
* Get dashboard KPI metrics for a creator.
|
||||
*
|
||||
* @return array{total_artworks: int, views_30d: int, favourites_30d: int, shares_30d: int, followers: int}
|
||||
*/
|
||||
public function getDashboardKpis(int $userId): array
|
||||
{
|
||||
$cacheKey = "studio.kpi.{$userId}";
|
||||
|
||||
return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($userId) {
|
||||
$totalArtworks = Artwork::where('user_id', $userId)
|
||||
->whereNull('deleted_at')
|
||||
->count();
|
||||
|
||||
// Aggregate stats from artwork_stats for this user's artworks
|
||||
$statsAgg = DB::table('artwork_stats')
|
||||
->join('artworks', 'artworks.id', '=', 'artwork_stats.artwork_id')
|
||||
->where('artworks.user_id', $userId)
|
||||
->whereNull('artworks.deleted_at')
|
||||
->selectRaw('
|
||||
COALESCE(SUM(artwork_stats.views), 0) as total_views,
|
||||
COALESCE(SUM(artwork_stats.favorites), 0) as total_favourites,
|
||||
COALESCE(SUM(artwork_stats.shares_count), 0) as total_shares
|
||||
')
|
||||
->first();
|
||||
|
||||
// Views in last 30 days from hourly snapshots if available, fallback to totals
|
||||
$views30d = 0;
|
||||
try {
|
||||
if (\Illuminate\Support\Facades\Schema::hasTable('artwork_metric_snapshots_hourly')) {
|
||||
$views30d = (int) DB::table('artwork_metric_snapshots_hourly')
|
||||
->join('artworks', 'artworks.id', '=', 'artwork_metric_snapshots_hourly.artwork_id')
|
||||
->where('artworks.user_id', $userId)
|
||||
->where('artwork_metric_snapshots_hourly.bucket_hour', '>=', now()->subDays(30))
|
||||
->sum('artwork_metric_snapshots_hourly.views_count');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Table or column doesn't exist — fall back to totals
|
||||
}
|
||||
|
||||
if ($views30d === 0) {
|
||||
$views30d = (int) ($statsAgg->total_views ?? 0);
|
||||
}
|
||||
|
||||
$followers = DB::table('user_followers')
|
||||
->where('user_id', $userId)
|
||||
->count();
|
||||
|
||||
return [
|
||||
'total_artworks' => $totalArtworks,
|
||||
'views_30d' => $views30d,
|
||||
'favourites_30d' => (int) ($statsAgg->total_favourites ?? 0),
|
||||
'shares_30d' => (int) ($statsAgg->total_shares ?? 0),
|
||||
'followers' => $followers,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get top performing artworks for a creator in the last 7 days.
|
||||
*
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
public function getTopPerformers(int $userId, int $limit = 6): \Illuminate\Support\Collection
|
||||
{
|
||||
$cacheKey = "studio.top_performers.{$userId}";
|
||||
|
||||
return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($userId, $limit) {
|
||||
return Artwork::where('user_id', $userId)
|
||||
->whereNull('deleted_at')
|
||||
->where('is_public', true)
|
||||
->with(['stats', 'tags'])
|
||||
->whereHas('stats')
|
||||
->orderByDesc(
|
||||
ArtworkStats::select('heat_score')
|
||||
->whereColumn('artwork_stats.artwork_id', 'artworks.id')
|
||||
->limit(1)
|
||||
)
|
||||
->limit($limit)
|
||||
->get()
|
||||
->map(fn (Artwork $art) => [
|
||||
'id' => $art->id,
|
||||
'title' => $art->title,
|
||||
'slug' => $art->slug,
|
||||
'thumb_url' => $art->thumbUrl('md'),
|
||||
'favourites' => (int) ($art->stats?->favorites ?? 0),
|
||||
'shares' => (int) ($art->stats?->shares_count ?? 0),
|
||||
'heat_score' => (float) ($art->stats?->heat_score ?? 0),
|
||||
'ranking_score' => (float) ($art->stats?->ranking_score ?? 0),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent comments on a creator's artworks.
|
||||
*
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
public function getRecentComments(int $userId, int $limit = 5): \Illuminate\Support\Collection
|
||||
{
|
||||
return DB::table('artwork_comments')
|
||||
->join('artworks', 'artworks.id', '=', 'artwork_comments.artwork_id')
|
||||
->join('users', 'users.id', '=', 'artwork_comments.user_id')
|
||||
->where('artworks.user_id', $userId)
|
||||
->whereNull('artwork_comments.deleted_at')
|
||||
->orderByDesc('artwork_comments.created_at')
|
||||
->limit($limit)
|
||||
->select([
|
||||
'artwork_comments.id',
|
||||
'artwork_comments.content as body',
|
||||
'artwork_comments.created_at',
|
||||
'users.name as author_name',
|
||||
'users.username as author_username',
|
||||
'artworks.title as artwork_title',
|
||||
'artworks.slug as artwork_slug',
|
||||
])
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregate analytics across all artworks for the Studio Analytics page.
|
||||
*
|
||||
* @return array{totals: array, top_artworks: array, content_breakdown: array}
|
||||
*/
|
||||
public function getAnalyticsOverview(int $userId): array
|
||||
{
|
||||
$cacheKey = "studio.analytics_overview.{$userId}";
|
||||
|
||||
return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($userId) {
|
||||
// Totals
|
||||
$totals = DB::table('artwork_stats')
|
||||
->join('artworks', 'artworks.id', '=', 'artwork_stats.artwork_id')
|
||||
->where('artworks.user_id', $userId)
|
||||
->whereNull('artworks.deleted_at')
|
||||
->selectRaw('
|
||||
COALESCE(SUM(artwork_stats.views), 0) as views,
|
||||
COALESCE(SUM(artwork_stats.favorites), 0) as favourites,
|
||||
COALESCE(SUM(artwork_stats.shares_count), 0) as shares,
|
||||
COALESCE(SUM(artwork_stats.downloads), 0) as downloads,
|
||||
COALESCE(SUM(artwork_stats.comments_count), 0) as comments,
|
||||
COALESCE(AVG(artwork_stats.ranking_score), 0) as avg_ranking,
|
||||
COALESCE(AVG(artwork_stats.heat_score), 0) as avg_heat
|
||||
')
|
||||
->first();
|
||||
|
||||
// Top 10 artworks by ranking score
|
||||
$topArtworks = Artwork::where('user_id', $userId)
|
||||
->whereNull('deleted_at')
|
||||
->where('is_public', true)
|
||||
->with(['stats'])
|
||||
->whereHas('stats')
|
||||
->orderByDesc(
|
||||
ArtworkStats::select('ranking_score')
|
||||
->whereColumn('artwork_stats.artwork_id', 'artworks.id')
|
||||
->limit(1)
|
||||
)
|
||||
->limit(10)
|
||||
->get()
|
||||
->map(fn (Artwork $art) => [
|
||||
'id' => $art->id,
|
||||
'title' => $art->title,
|
||||
'slug' => $art->slug,
|
||||
'thumb_url' => $art->thumbUrl('sq'),
|
||||
'views' => (int) ($art->stats?->views ?? 0),
|
||||
'favourites' => (int) ($art->stats?->favorites ?? 0),
|
||||
'shares' => (int) ($art->stats?->shares_count ?? 0),
|
||||
'downloads' => (int) ($art->stats?->downloads ?? 0),
|
||||
'comments' => (int) ($art->stats?->comments_count ?? 0),
|
||||
'ranking_score' => (float) ($art->stats?->ranking_score ?? 0),
|
||||
'heat_score' => (float) ($art->stats?->heat_score ?? 0),
|
||||
]);
|
||||
|
||||
// Content type breakdown
|
||||
$contentBreakdown = DB::table('artworks')
|
||||
->join('artwork_category', 'artwork_category.artwork_id', '=', 'artworks.id')
|
||||
->join('categories', 'categories.id', '=', 'artwork_category.category_id')
|
||||
->join('content_types', 'content_types.id', '=', 'categories.content_type_id')
|
||||
->where('artworks.user_id', $userId)
|
||||
->whereNull('artworks.deleted_at')
|
||||
->groupBy('content_types.id', 'content_types.name', 'content_types.slug')
|
||||
->select([
|
||||
'content_types.name',
|
||||
'content_types.slug',
|
||||
DB::raw('COUNT(DISTINCT artworks.id) as count'),
|
||||
])
|
||||
->orderByDesc('count')
|
||||
->get()
|
||||
->map(fn ($row) => [
|
||||
'name' => $row->name,
|
||||
'slug' => $row->slug,
|
||||
'count' => (int) $row->count,
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
|
||||
return [
|
||||
'totals' => [
|
||||
'views' => (int) ($totals->views ?? 0),
|
||||
'favourites' => (int) ($totals->favourites ?? 0),
|
||||
'shares' => (int) ($totals->shares ?? 0),
|
||||
'downloads' => (int) ($totals->downloads ?? 0),
|
||||
'comments' => (int) ($totals->comments ?? 0),
|
||||
'avg_ranking' => round((float) ($totals->avg_ranking ?? 0), 1),
|
||||
'avg_heat' => round((float) ($totals->avg_heat ?? 0), 1),
|
||||
],
|
||||
'top_artworks' => $topArtworks->values()->all(),
|
||||
'content_breakdown' => $contentBreakdown,
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user