308 lines
12 KiB
PHP
308 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\Studio\Providers;
|
|
|
|
use App\Models\Collection;
|
|
use App\Models\User;
|
|
use App\Services\CollectionService;
|
|
use App\Services\Studio\Contracts\CreatorStudioProvider;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Support\Collection as SupportCollection;
|
|
|
|
final class CollectionStudioProvider implements CreatorStudioProvider
|
|
{
|
|
public function __construct(
|
|
private readonly CollectionService $collections,
|
|
) {
|
|
}
|
|
|
|
public function key(): string
|
|
{
|
|
return 'collections';
|
|
}
|
|
|
|
public function label(): string
|
|
{
|
|
return 'Collections';
|
|
}
|
|
|
|
public function icon(): string
|
|
{
|
|
return 'fa-solid fa-layer-group';
|
|
}
|
|
|
|
public function createUrl(): string
|
|
{
|
|
return route('settings.collections.create');
|
|
}
|
|
|
|
public function indexUrl(): string
|
|
{
|
|
return route('studio.collections');
|
|
}
|
|
|
|
public function summary(User $user): array
|
|
{
|
|
$baseQuery = Collection::query()->withTrashed()->where('user_id', $user->id);
|
|
|
|
$count = (clone $baseQuery)
|
|
->whereNull('deleted_at')
|
|
->where('lifecycle_state', '!=', Collection::LIFECYCLE_ARCHIVED)
|
|
->count();
|
|
|
|
$draftCount = (clone $baseQuery)
|
|
->whereNull('deleted_at')
|
|
->where(function (Builder $query): void {
|
|
$query->where('lifecycle_state', Collection::LIFECYCLE_DRAFT)
|
|
->orWhere('workflow_state', Collection::WORKFLOW_DRAFT)
|
|
->orWhere('workflow_state', Collection::WORKFLOW_IN_REVIEW);
|
|
})
|
|
->count();
|
|
|
|
$publishedCount = (clone $baseQuery)
|
|
->whereNull('deleted_at')
|
|
->whereIn('lifecycle_state', [Collection::LIFECYCLE_PUBLISHED, Collection::LIFECYCLE_FEATURED, Collection::LIFECYCLE_SCHEDULED])
|
|
->count();
|
|
|
|
$recentPublishedCount = (clone $baseQuery)
|
|
->whereNull('deleted_at')
|
|
->whereIn('lifecycle_state', [Collection::LIFECYCLE_PUBLISHED, Collection::LIFECYCLE_FEATURED, Collection::LIFECYCLE_SCHEDULED])
|
|
->where('published_at', '>=', now()->subDays(30))
|
|
->count();
|
|
|
|
$archivedCount = (clone $baseQuery)
|
|
->where(function (Builder $query): void {
|
|
$query->whereNotNull('deleted_at')
|
|
->orWhere('lifecycle_state', Collection::LIFECYCLE_ARCHIVED);
|
|
})
|
|
->count();
|
|
|
|
return [
|
|
'key' => $this->key(),
|
|
'label' => $this->label(),
|
|
'icon' => $this->icon(),
|
|
'count' => $count,
|
|
'draft_count' => $draftCount,
|
|
'published_count' => $publishedCount,
|
|
'archived_count' => $archivedCount,
|
|
'trend_value' => $recentPublishedCount,
|
|
'trend_label' => 'published in 30d',
|
|
'quick_action_label' => 'Create collection',
|
|
'index_url' => $this->indexUrl(),
|
|
'create_url' => $this->createUrl(),
|
|
];
|
|
}
|
|
|
|
public function items(User $user, string $bucket = 'all', int $limit = 200): SupportCollection
|
|
{
|
|
$query = Collection::query()
|
|
->withTrashed()
|
|
->where('user_id', $user->id)
|
|
->with(['user.profile', 'coverArtwork'])
|
|
->orderByDesc('updated_at')
|
|
->limit($limit);
|
|
|
|
if ($bucket === 'drafts') {
|
|
$query->whereNull('deleted_at')
|
|
->where(function (Builder $builder): void {
|
|
$builder->where('lifecycle_state', Collection::LIFECYCLE_DRAFT)
|
|
->orWhere('workflow_state', Collection::WORKFLOW_DRAFT)
|
|
->orWhere('workflow_state', Collection::WORKFLOW_IN_REVIEW);
|
|
});
|
|
} elseif ($bucket === 'scheduled') {
|
|
$query->whereNull('deleted_at')
|
|
->where('lifecycle_state', Collection::LIFECYCLE_SCHEDULED);
|
|
} elseif ($bucket === 'archived') {
|
|
$query->where(function (Builder $builder): void {
|
|
$builder->whereNotNull('deleted_at')
|
|
->orWhere('lifecycle_state', Collection::LIFECYCLE_ARCHIVED);
|
|
});
|
|
} elseif ($bucket === 'published') {
|
|
$query->whereNull('deleted_at')
|
|
->whereIn('lifecycle_state', [Collection::LIFECYCLE_PUBLISHED, Collection::LIFECYCLE_FEATURED, Collection::LIFECYCLE_SCHEDULED]);
|
|
} else {
|
|
$query->whereNull('deleted_at')->where('lifecycle_state', '!=', Collection::LIFECYCLE_ARCHIVED);
|
|
}
|
|
|
|
return collect($this->collections->mapCollectionCardPayloads($query->get(), true, $user))
|
|
->map(fn (array $item): array => $this->mapItem($item));
|
|
}
|
|
|
|
public function topItems(User $user, int $limit = 5): SupportCollection
|
|
{
|
|
$collections = Collection::query()
|
|
->where('user_id', $user->id)
|
|
->whereNull('deleted_at')
|
|
->whereIn('lifecycle_state', [Collection::LIFECYCLE_PUBLISHED, Collection::LIFECYCLE_FEATURED])
|
|
->with(['user.profile', 'coverArtwork'])
|
|
->orderByDesc('ranking_score')
|
|
->orderByDesc('views_count')
|
|
->limit($limit)
|
|
->get();
|
|
|
|
return collect($this->collections->mapCollectionCardPayloads($collections, true, $user))
|
|
->map(fn (array $item): array => $this->mapItem($item));
|
|
}
|
|
|
|
public function analytics(User $user): array
|
|
{
|
|
$totals = Collection::query()
|
|
->where('user_id', $user->id)
|
|
->whereNull('deleted_at')
|
|
->selectRaw('COALESCE(SUM(views_count), 0) as views')
|
|
->selectRaw('COALESCE(SUM(likes_count + followers_count), 0) as appreciation')
|
|
->selectRaw('COALESCE(SUM(shares_count), 0) as shares')
|
|
->selectRaw('COALESCE(SUM(comments_count), 0) as comments')
|
|
->selectRaw('COALESCE(SUM(saves_count), 0) as saves')
|
|
->first();
|
|
|
|
return [
|
|
'views' => (int) ($totals->views ?? 0),
|
|
'appreciation' => (int) ($totals->appreciation ?? 0),
|
|
'shares' => (int) ($totals->shares ?? 0),
|
|
'comments' => (int) ($totals->comments ?? 0),
|
|
'saves' => (int) ($totals->saves ?? 0),
|
|
];
|
|
}
|
|
|
|
public function scheduledItems(User $user, int $limit = 50): SupportCollection
|
|
{
|
|
return $this->items($user, 'scheduled', $limit);
|
|
}
|
|
|
|
private function mapItem(array $item): array
|
|
{
|
|
$status = $item['lifecycle_state'] ?? 'draft';
|
|
if ($status === Collection::LIFECYCLE_FEATURED) {
|
|
$status = 'published';
|
|
}
|
|
|
|
return [
|
|
'id' => sprintf('%s:%d', $this->key(), (int) $item['id']),
|
|
'numeric_id' => (int) $item['id'],
|
|
'module' => $this->key(),
|
|
'module_label' => $this->label(),
|
|
'module_icon' => $this->icon(),
|
|
'title' => $item['title'],
|
|
'subtitle' => $item['subtitle'] ?: ($item['type'] ? ucfirst((string) $item['type']) : null),
|
|
'description' => $item['summary'] ?: $item['description'],
|
|
'status' => $status,
|
|
'visibility' => $item['visibility'],
|
|
'image_url' => $item['cover_image'],
|
|
'preview_url' => $item['url'],
|
|
'view_url' => $item['url'],
|
|
'edit_url' => $item['edit_url'] ?: $item['manage_url'],
|
|
'manage_url' => $item['manage_url'],
|
|
'analytics_url' => route('settings.collections.analytics', ['collection' => $item['id']]),
|
|
'create_url' => $this->createUrl(),
|
|
'actions' => $this->actionsFor($item, $status),
|
|
'created_at' => $item['published_at'] ?? $item['updated_at'],
|
|
'updated_at' => $item['updated_at'],
|
|
'published_at' => $item['published_at'] ?? null,
|
|
'scheduled_at' => $status === Collection::LIFECYCLE_SCHEDULED ? ($item['published_at'] ?? null) : null,
|
|
'featured' => (bool) ($item['is_featured'] ?? false),
|
|
'metrics' => [
|
|
'views' => (int) ($item['views_count'] ?? 0),
|
|
'appreciation' => (int) (($item['likes_count'] ?? 0) + ($item['followers_count'] ?? 0)),
|
|
'shares' => (int) ($item['shares_count'] ?? 0),
|
|
'comments' => (int) ($item['comments_count'] ?? 0),
|
|
'saves' => (int) ($item['saves_count'] ?? 0),
|
|
],
|
|
'engagement_score' => (int) ($item['views_count'] ?? 0)
|
|
+ ((int) ($item['likes_count'] ?? 0) * 2)
|
|
+ ((int) ($item['followers_count'] ?? 0) * 2)
|
|
+ ((int) ($item['comments_count'] ?? 0) * 3)
|
|
+ ((int) ($item['shares_count'] ?? 0) * 2)
|
|
+ ((int) ($item['saves_count'] ?? 0) * 2),
|
|
'taxonomies' => [
|
|
'categories' => [],
|
|
'tags' => [],
|
|
],
|
|
];
|
|
}
|
|
|
|
private function actionsFor(array $item, string $status): array
|
|
{
|
|
$collectionId = (int) $item['id'];
|
|
$actions = [];
|
|
$featured = (bool) ($item['is_featured'] ?? false);
|
|
|
|
if ($status === 'draft') {
|
|
$actions[] = $this->requestAction(
|
|
'publish',
|
|
'Publish',
|
|
'fa-solid fa-rocket',
|
|
route('settings.collections.lifecycle', ['collection' => $collectionId]),
|
|
[
|
|
'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
|
|
'visibility' => Collection::VISIBILITY_PUBLIC,
|
|
'published_at' => now()->toIso8601String(),
|
|
]
|
|
);
|
|
}
|
|
|
|
if (in_array($status, ['published', 'scheduled'], true)) {
|
|
$actions[] = $this->requestAction(
|
|
'archive',
|
|
'Archive',
|
|
'fa-solid fa-box-archive',
|
|
route('settings.collections.lifecycle', ['collection' => $collectionId]),
|
|
[
|
|
'lifecycle_state' => Collection::LIFECYCLE_ARCHIVED,
|
|
'archived_at' => now()->toIso8601String(),
|
|
]
|
|
);
|
|
|
|
$actions[] = $featured
|
|
? $this->requestAction('unfeature', 'Remove feature', 'fa-solid fa-star-half-stroke', route('settings.collections.unfeature', ['collection' => $collectionId]), [], null, 'delete')
|
|
: $this->requestAction('feature', 'Feature', 'fa-solid fa-star', route('settings.collections.feature', ['collection' => $collectionId]), []);
|
|
|
|
if ($status === 'scheduled') {
|
|
$actions[] = $this->requestAction('publish_now', 'Publish now', 'fa-solid fa-bolt', route('api.studio.schedule.publishNow', ['module' => 'collections', 'id' => $collectionId]), []);
|
|
$actions[] = $this->requestAction('unschedule', 'Unschedule', 'fa-solid fa-calendar-xmark', route('api.studio.schedule.unschedule', ['module' => 'collections', 'id' => $collectionId]), []);
|
|
}
|
|
}
|
|
|
|
if ($status === 'archived') {
|
|
$actions[] = $this->requestAction(
|
|
'restore',
|
|
'Restore',
|
|
'fa-solid fa-rotate-left',
|
|
route('settings.collections.lifecycle', ['collection' => $collectionId]),
|
|
[
|
|
'lifecycle_state' => Collection::LIFECYCLE_DRAFT,
|
|
'visibility' => Collection::VISIBILITY_PRIVATE,
|
|
'archived_at' => null,
|
|
]
|
|
);
|
|
}
|
|
|
|
$actions[] = $this->requestAction(
|
|
'delete',
|
|
'Delete',
|
|
'fa-solid fa-trash',
|
|
route('settings.collections.destroy', ['collection' => $collectionId]),
|
|
[],
|
|
'Delete this collection permanently?',
|
|
'delete'
|
|
);
|
|
|
|
return $actions;
|
|
}
|
|
|
|
private function requestAction(string $key, string $label, string $icon, string $url, array $payload = [], ?string $confirm = null, string $method = 'post'): array
|
|
{
|
|
return [
|
|
'key' => $key,
|
|
'label' => $label,
|
|
'icon' => $icon,
|
|
'type' => 'request',
|
|
'method' => $method,
|
|
'url' => $url,
|
|
'payload' => $payload,
|
|
'confirm' => $confirm,
|
|
];
|
|
}
|
|
} |