Files
SkinbaseNova/app/Services/Studio/Providers/CardStudioProvider.php

261 lines
9.7 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Studio\Providers;
use App\Models\NovaCard;
use App\Models\User;
use App\Services\Studio\Contracts\CreatorStudioProvider;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
final class CardStudioProvider implements CreatorStudioProvider
{
public function key(): string
{
return 'cards';
}
public function label(): string
{
return 'Cards';
}
public function icon(): string
{
return 'fa-solid fa-id-card';
}
public function createUrl(): string
{
return route('studio.cards.create');
}
public function indexUrl(): string
{
return route('studio.cards.index');
}
public function summary(User $user): array
{
$baseQuery = NovaCard::query()->withTrashed()->where('user_id', $user->id);
$count = (clone $baseQuery)
->whereNull('deleted_at')
->whereNotIn('status', [NovaCard::STATUS_HIDDEN, NovaCard::STATUS_REJECTED])
->count();
$draftCount = (clone $baseQuery)
->whereNull('deleted_at')
->where('status', NovaCard::STATUS_DRAFT)
->count();
$publishedCount = (clone $baseQuery)
->whereNull('deleted_at')
->where('status', NovaCard::STATUS_PUBLISHED)
->count();
$recentPublishedCount = (clone $baseQuery)
->whereNull('deleted_at')
->where('status', NovaCard::STATUS_PUBLISHED)
->where('published_at', '>=', now()->subDays(30))
->count();
$archivedCount = (clone $baseQuery)
->where(function (Builder $query): void {
$query->whereNotNull('deleted_at')
->orWhereIn('status', [NovaCard::STATUS_HIDDEN, NovaCard::STATUS_REJECTED]);
})
->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 card',
'index_url' => $this->indexUrl(),
'create_url' => $this->createUrl(),
];
}
public function items(User $user, string $bucket = 'all', int $limit = 200): Collection
{
$query = NovaCard::query()
->withTrashed()
->where('user_id', $user->id)
->with(['category', 'tags'])
->orderByDesc('updated_at')
->limit($limit);
if ($bucket === 'drafts') {
$query->whereNull('deleted_at')->where('status', NovaCard::STATUS_DRAFT);
} elseif ($bucket === 'scheduled') {
$query->whereNull('deleted_at')->where('status', NovaCard::STATUS_SCHEDULED);
} elseif ($bucket === 'archived') {
$query->where(function (Builder $builder): void {
$builder->whereNotNull('deleted_at')
->orWhereIn('status', [NovaCard::STATUS_HIDDEN, NovaCard::STATUS_REJECTED]);
});
} elseif ($bucket === 'published') {
$query->whereNull('deleted_at')->where('status', NovaCard::STATUS_PUBLISHED);
} else {
$query->whereNull('deleted_at')->whereNotIn('status', [NovaCard::STATUS_HIDDEN, NovaCard::STATUS_REJECTED]);
}
return $query->get()->map(fn (NovaCard $card): array => $this->mapItem($card));
}
public function topItems(User $user, int $limit = 5): Collection
{
return NovaCard::query()
->where('user_id', $user->id)
->whereNull('deleted_at')
->where('status', NovaCard::STATUS_PUBLISHED)
->orderByDesc('trending_score')
->orderByDesc('views_count')
->limit($limit)
->get()
->map(fn (NovaCard $card): array => $this->mapItem($card));
}
public function analytics(User $user): array
{
$totals = NovaCard::query()
->where('user_id', $user->id)
->whereNull('deleted_at')
->selectRaw('COALESCE(SUM(views_count), 0) as views')
->selectRaw('COALESCE(SUM(likes_count + favorites_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): Collection
{
return $this->items($user, 'scheduled', $limit);
}
private function mapItem(NovaCard $card): array
{
$status = $card->deleted_at || in_array($card->status, [NovaCard::STATUS_HIDDEN, NovaCard::STATUS_REJECTED], true)
? 'archived'
: $card->status;
return [
'id' => sprintf('%s:%d', $this->key(), (int) $card->id),
'numeric_id' => (int) $card->id,
'module' => $this->key(),
'module_label' => $this->label(),
'module_icon' => $this->icon(),
'title' => $card->title,
'subtitle' => $card->category?->name ?: strtoupper((string) $card->format),
'description' => $card->description,
'status' => $status,
'visibility' => $card->visibility,
'image_url' => $card->previewUrl(),
'preview_url' => route('studio.cards.preview', ['id' => $card->id]),
'view_url' => $card->status === NovaCard::STATUS_PUBLISHED ? $card->publicUrl() : route('studio.cards.preview', ['id' => $card->id]),
'edit_url' => route('studio.cards.edit', ['id' => $card->id]),
'manage_url' => route('studio.cards.edit', ['id' => $card->id]),
'analytics_url' => route('studio.cards.analytics', ['id' => $card->id]),
'create_url' => $this->createUrl(),
'actions' => $this->actionsFor($card, $status),
'created_at' => $card->created_at?->toIso8601String(),
'updated_at' => $card->updated_at?->toIso8601String(),
'published_at' => $card->published_at?->toIso8601String(),
'scheduled_at' => $card->scheduled_for?->toIso8601String(),
'schedule_timezone' => $card->scheduling_timezone,
'featured' => (bool) $card->featured,
'metrics' => [
'views' => (int) $card->views_count,
'appreciation' => (int) ($card->likes_count + $card->favorites_count),
'shares' => (int) $card->shares_count,
'comments' => (int) $card->comments_count,
'saves' => (int) $card->saves_count,
],
'engagement_score' => (int) $card->views_count
+ ((int) $card->likes_count * 2)
+ ((int) $card->favorites_count * 2)
+ ((int) $card->comments_count * 3)
+ ((int) $card->shares_count * 2)
+ ((int) $card->saves_count * 2),
'taxonomies' => [
'categories' => $card->category ? [[
'id' => (int) $card->category->id,
'name' => (string) $card->category->name,
'slug' => (string) $card->category->slug,
]] : [],
'tags' => $card->tags->map(fn ($entry): array => [
'id' => (int) $entry->id,
'name' => (string) $entry->name,
'slug' => (string) $entry->slug,
])->values()->all(),
],
];
}
private function actionsFor(NovaCard $card, string $status): array
{
$actions = [
[
'key' => 'duplicate',
'label' => 'Duplicate',
'icon' => 'fa-solid fa-id-card',
'type' => 'request',
'method' => 'post',
'url' => route('api.cards.duplicate', ['id' => $card->id]),
'redirect_pattern' => route('studio.cards.edit', ['id' => '__ID__']),
],
];
if ($status === NovaCard::STATUS_DRAFT) {
$actions[] = [
'key' => 'delete',
'label' => 'Delete draft',
'icon' => 'fa-solid fa-trash',
'type' => 'request',
'method' => 'delete',
'url' => route('api.cards.drafts.destroy', ['id' => $card->id]),
'confirm' => 'Delete this card draft?',
];
}
if ($status === NovaCard::STATUS_SCHEDULED) {
$actions[] = [
'key' => 'publish_now',
'label' => 'Publish now',
'icon' => 'fa-solid fa-bolt',
'type' => 'request',
'method' => 'post',
'url' => route('api.studio.schedule.publishNow', ['module' => 'cards', 'id' => $card->id]),
];
$actions[] = [
'key' => 'unschedule',
'label' => 'Unschedule',
'icon' => 'fa-solid fa-calendar-xmark',
'type' => 'request',
'method' => 'post',
'url' => route('api.studio.schedule.unschedule', ['module' => 'cards', 'id' => $card->id]),
];
}
return $actions;
}
}