842 lines
37 KiB
PHP
842 lines
37 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Artwork;
|
|
use App\Models\Collection;
|
|
use App\Models\Group;
|
|
use App\Models\User;
|
|
use App\Services\Maturity\ArtworkMaturityService;
|
|
use App\Services\ThumbnailPresenter;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
|
|
class GroupService
|
|
{
|
|
public function __construct(
|
|
private readonly GroupMembershipService $memberships,
|
|
private readonly GroupCardService $cards,
|
|
private readonly CollectionService $collections,
|
|
private readonly GroupMediaService $media,
|
|
private readonly GroupJoinRequestService $joinRequests,
|
|
private readonly GroupRecruitmentService $recruitment,
|
|
private readonly GroupPostService $posts,
|
|
private readonly GroupProjectService $projects,
|
|
private readonly GroupReleaseService $releases,
|
|
private readonly GroupChallengeService $challenges,
|
|
private readonly GroupEventService $events,
|
|
private readonly GroupAssetService $assets,
|
|
private readonly GroupActivityService $activity,
|
|
private readonly GroupHistoryService $history,
|
|
private readonly GroupReputationService $reputation,
|
|
private readonly ArtworkMaturityService $maturity,
|
|
) {
|
|
}
|
|
|
|
public function makeUniqueSlug(string $source, ?int $ignoreGroupId = null): string
|
|
{
|
|
$base = Str::slug(Str::limit($source, 90, ''));
|
|
$base = $base !== '' ? $base : 'group';
|
|
$slug = $base;
|
|
$suffix = 2;
|
|
|
|
while (Group::query()
|
|
->where('slug', $slug)
|
|
->when($ignoreGroupId !== null, fn ($query) => $query->where('id', '!=', $ignoreGroupId))
|
|
->exists()) {
|
|
$slug = Str::limit($base, 84, '') . '-' . $suffix;
|
|
$suffix++;
|
|
}
|
|
|
|
return $slug;
|
|
}
|
|
|
|
public function createGroup(User $owner, array $attributes): Group
|
|
{
|
|
$storedAvatarPath = null;
|
|
$storedBannerPath = null;
|
|
|
|
try {
|
|
return DB::transaction(function () use ($owner, $attributes, &$storedAvatarPath, &$storedBannerPath): Group {
|
|
$group = new Group();
|
|
$group->owner()->associate($owner);
|
|
$group->featured_artwork_id = null;
|
|
$group->is_verified = false;
|
|
$group->founded_at = $attributes['founded_at'] ?? null;
|
|
$group->name = (string) $attributes['name'];
|
|
$group->slug = $this->makeUniqueSlug((string) ($attributes['slug'] ?? $attributes['name']));
|
|
$group->headline = $attributes['headline'] ?? null;
|
|
$group->bio = $attributes['bio'] ?? null;
|
|
$group->type = $attributes['type'] ?? null;
|
|
$group->visibility = (string) ($attributes['visibility'] ?? Group::VISIBILITY_PUBLIC);
|
|
$group->status = Group::LIFECYCLE_ACTIVE;
|
|
$group->membership_policy = (string) ($attributes['membership_policy'] ?? Group::MEMBERSHIP_INVITE_ONLY);
|
|
$group->website_url = $attributes['website_url'] ?? null;
|
|
$group->links_json = $this->normalizeLinks($attributes['links_json'] ?? []);
|
|
$group->avatar_path = $this->normalizeMediaPath($attributes['avatar_path'] ?? null);
|
|
$group->banner_path = $this->normalizeMediaPath($attributes['banner_path'] ?? null);
|
|
$group->artworks_count = 0;
|
|
$group->collections_count = 0;
|
|
$group->followers_count = 0;
|
|
$group->last_activity_at = now();
|
|
$group->save();
|
|
|
|
if (($attributes['avatar_file'] ?? null) instanceof UploadedFile) {
|
|
$storedAvatarPath = $this->media->storeUploadedImage($group, $attributes['avatar_file'], 'avatar');
|
|
$group->avatar_path = $storedAvatarPath;
|
|
}
|
|
|
|
if (($attributes['banner_file'] ?? null) instanceof UploadedFile) {
|
|
$storedBannerPath = $this->media->storeUploadedImage($group, $attributes['banner_file'], 'banner');
|
|
$group->banner_path = $storedBannerPath;
|
|
}
|
|
|
|
if ($storedAvatarPath !== null || $storedBannerPath !== null) {
|
|
$group->save();
|
|
}
|
|
|
|
$this->memberships->ensureOwnerMembership($group);
|
|
|
|
return $group->fresh(['owner.profile']);
|
|
});
|
|
} catch (\Throwable $exception) {
|
|
$this->media->deleteIfManaged($storedAvatarPath);
|
|
$this->media->deleteIfManaged($storedBannerPath);
|
|
|
|
throw $exception;
|
|
}
|
|
}
|
|
|
|
public function updateGroup(Group $group, array $attributes, User $actor): Group
|
|
{
|
|
$storedAvatarPath = null;
|
|
$storedBannerPath = null;
|
|
$obsoleteAvatarPath = null;
|
|
$obsoleteBannerPath = null;
|
|
|
|
try {
|
|
$updatedGroup = DB::transaction(function () use ($group, $attributes, $actor, &$storedAvatarPath, &$storedBannerPath, &$obsoleteAvatarPath, &$obsoleteBannerPath): Group {
|
|
$originalAvatarPath = $group->avatar_path;
|
|
$originalBannerPath = $group->banner_path;
|
|
|
|
$group->fill([
|
|
'name' => (string) ($attributes['name'] ?? $group->name),
|
|
'slug' => $this->makeUniqueSlug((string) ($attributes['slug'] ?? $attributes['name'] ?? $group->slug), (int) $group->id),
|
|
'headline' => $attributes['headline'] ?? null,
|
|
'bio' => $attributes['bio'] ?? null,
|
|
'type' => $attributes['type'] ?? $group->type,
|
|
'visibility' => (string) ($attributes['visibility'] ?? $group->visibility),
|
|
'membership_policy' => (string) ($attributes['membership_policy'] ?? $group->membership_policy ?? Group::MEMBERSHIP_INVITE_ONLY),
|
|
'founded_at' => $attributes['founded_at'] ?? $group->founded_at,
|
|
'website_url' => $attributes['website_url'] ?? null,
|
|
'links_json' => $this->normalizeLinks($attributes['links_json'] ?? $group->links_json ?? []),
|
|
'avatar_path' => array_key_exists('avatar_path', $attributes) ? $this->normalizeMediaPath($attributes['avatar_path']) : $group->avatar_path,
|
|
'banner_path' => array_key_exists('banner_path', $attributes) ? $this->normalizeMediaPath($attributes['banner_path']) : $group->banner_path,
|
|
'featured_artwork_id' => $this->normalizeFeaturedArtworkId($group, $attributes['featured_artwork_id'] ?? $group->featured_artwork_id),
|
|
'last_activity_at' => now(),
|
|
]);
|
|
$group->save();
|
|
|
|
if (($attributes['avatar_file'] ?? null) instanceof UploadedFile) {
|
|
$storedAvatarPath = $this->media->storeUploadedImage($group, $attributes['avatar_file'], 'avatar');
|
|
$group->avatar_path = $storedAvatarPath;
|
|
}
|
|
|
|
if (($attributes['banner_file'] ?? null) instanceof UploadedFile) {
|
|
$storedBannerPath = $this->media->storeUploadedImage($group, $attributes['banner_file'], 'banner');
|
|
$group->banner_path = $storedBannerPath;
|
|
}
|
|
|
|
if ($storedAvatarPath !== null || $storedBannerPath !== null) {
|
|
$group->save();
|
|
}
|
|
|
|
$this->memberships->ensureOwnerMembership($group);
|
|
|
|
$obsoleteAvatarPath = $originalAvatarPath !== $group->avatar_path ? $originalAvatarPath : null;
|
|
$obsoleteBannerPath = $originalBannerPath !== $group->banner_path ? $originalBannerPath : null;
|
|
|
|
return $group->fresh(['owner.profile']);
|
|
});
|
|
} catch (\Throwable $exception) {
|
|
$this->media->deleteIfManaged($storedAvatarPath);
|
|
$this->media->deleteIfManaged($storedBannerPath);
|
|
|
|
throw $exception;
|
|
}
|
|
|
|
$this->media->deleteIfManaged($obsoleteAvatarPath);
|
|
$this->media->deleteIfManaged($obsoleteBannerPath);
|
|
|
|
return $updatedGroup;
|
|
}
|
|
|
|
public function syncArtworkCount(Group $group): void
|
|
{
|
|
$group->forceFill([
|
|
'artworks_count' => Artwork::query()
|
|
->where('group_id', $group->id)
|
|
->whereNull('deleted_at')
|
|
->count(),
|
|
'last_activity_at' => now(),
|
|
])->save();
|
|
}
|
|
|
|
public function syncCollectionCount(Group $group): void
|
|
{
|
|
$group->forceFill([
|
|
'collections_count' => Collection::query()
|
|
->where('group_id', $group->id)
|
|
->whereNull('deleted_at')
|
|
->count(),
|
|
'last_activity_at' => now(),
|
|
])->save();
|
|
}
|
|
|
|
public function studioOptionsForUser(User $user): array
|
|
{
|
|
$groups = Group::query()
|
|
->with(['owner.profile', 'members'])
|
|
->where('status', '!=', Group::LIFECYCLE_SUSPENDED)
|
|
->where(function ($query) use ($user): void {
|
|
$query->where('owner_user_id', $user->id)
|
|
->orWhereHas('members', function ($memberQuery) use ($user): void {
|
|
$memberQuery->where('user_id', $user->id)
|
|
->where('status', Group::STATUS_ACTIVE);
|
|
});
|
|
})
|
|
->orderBy('name')
|
|
->get();
|
|
|
|
return $groups->map(function (Group $group) use ($user): array {
|
|
$canPublishArtworks = $group->canPublishArtworks($user);
|
|
$canSubmitArtworkForReview = $group->canSubmitArtworkForReview($user);
|
|
$canManageReleases = $group->canManageReleases($user);
|
|
$canViewReputation = $group->canViewReputationDashboard($user);
|
|
|
|
return [
|
|
'id' => (int) $group->id,
|
|
'name' => (string) $group->name,
|
|
'slug' => (string) $group->slug,
|
|
'role' => $group->activeRoleFor($user),
|
|
'role_label' => Group::displayRole($group->activeRoleFor($user)),
|
|
'status' => (string) ($group->status ?? Group::LIFECYCLE_ACTIVE),
|
|
'avatar_url' => $group->avatarUrl(),
|
|
'artworks_count' => (int) $group->artworks_count,
|
|
'collections_count' => (int) $group->collections_count,
|
|
'followers_count' => (int) $group->followers_count,
|
|
'permissions' => [
|
|
'can_publish_artworks' => $canPublishArtworks,
|
|
'can_submit_artwork_for_review' => $canSubmitArtworkForReview,
|
|
],
|
|
'public_url' => $group->publicUrl(),
|
|
'studio_url' => route('studio.groups.show', ['group' => $group]),
|
|
'studio_artworks_url' => route('studio.groups.artworks', ['group' => $group]),
|
|
'studio_collections_url' => route('studio.groups.collections', ['group' => $group]),
|
|
'studio_members_url' => route('studio.groups.members', ['group' => $group]),
|
|
'studio_invitations_url' => route('studio.groups.invitations', ['group' => $group]),
|
|
'studio_join_requests_url' => route('studio.groups.join-requests', ['group' => $group]),
|
|
'studio_review_url' => route('studio.groups.review', ['group' => $group]),
|
|
'studio_recruitment_url' => route('studio.groups.recruitment', ['group' => $group]),
|
|
'studio_posts_url' => route('studio.groups.posts.index', ['group' => $group]),
|
|
'studio_settings_url' => route('studio.groups.settings', ['group' => $group]),
|
|
'studio_projects_url' => route('studio.groups.projects.index', ['group' => $group]),
|
|
'studio_releases_url' => $canManageReleases ? route('studio.groups.releases.index', ['group' => $group]) : null,
|
|
'studio_challenges_url' => route('studio.groups.challenges.index', ['group' => $group]),
|
|
'studio_events_url' => route('studio.groups.events.index', ['group' => $group]),
|
|
'studio_assets_url' => route('studio.groups.assets.index', ['group' => $group]),
|
|
'studio_reputation_url' => $canViewReputation ? route('studio.groups.reputation', ['group' => $group]) : null,
|
|
'studio_activity_url' => route('studio.groups.activity', ['group' => $group]),
|
|
'upload_url' => ($canPublishArtworks || $canSubmitArtworkForReview) ? route('upload', ['group' => $group->slug]) : null,
|
|
'collection_create_url' => route('settings.collections.create', ['group' => $group->slug]),
|
|
];
|
|
})->values()->all();
|
|
}
|
|
|
|
public function mapGroupCard(Group $group, ?User $viewer = null): array
|
|
{
|
|
return $this->cards->mapGroupCard($group, $viewer);
|
|
}
|
|
|
|
public function mapGroupDetail(Group $group, ?User $viewer = null): array
|
|
{
|
|
$recruitment = $this->recruitment->payloadForGroup($group);
|
|
|
|
return array_merge($this->mapGroupCard($group, $viewer), [
|
|
'website_url' => $group->website_url,
|
|
'bio' => $group->bio,
|
|
'links' => $this->normalizeLinks($group->links_json ?? []),
|
|
'avatar_path' => $group->avatar_path,
|
|
'banner_path' => $group->banner_path,
|
|
'featured_artwork_id' => $group->featured_artwork_id ? (int) $group->featured_artwork_id : null,
|
|
'founded_at' => $group->founded_at?->toISOString(),
|
|
'last_activity_at' => $group->last_activity_at?->toISOString(),
|
|
'created_at' => $group->created_at?->toISOString(),
|
|
'current_join_request' => $this->joinRequests->currentRequestFor($group, $viewer),
|
|
'recruitment' => $recruitment,
|
|
'pinned_post' => $this->posts->pinnedPost($group),
|
|
'featured_release' => $this->releases->featuredRelease($group, $viewer),
|
|
'featured_project' => $this->projects->featuredProject($group, $viewer),
|
|
'active_challenge' => $this->challenges->activeChallenge($group, $viewer),
|
|
'upcoming_event' => $this->events->upcomingEvent($group, $viewer),
|
|
'badge_showcase' => $this->reputation->groupBadges($group, 8),
|
|
'top_contributors' => $this->reputation->topContributors($group, 6),
|
|
'trust_signals' => $this->reputation->trustSignals($group),
|
|
]);
|
|
}
|
|
|
|
public function recentPostCards(Group $group, int $limit = 3): array
|
|
{
|
|
return $this->posts->recentPosts($group, $limit);
|
|
}
|
|
|
|
public function recentProjectCards(Group $group, ?User $viewer = null, int $limit = 3): array
|
|
{
|
|
return $this->projects->publicListing($group, $viewer, $limit);
|
|
}
|
|
|
|
public function recentReleaseCards(Group $group, ?User $viewer = null, int $limit = 3): array
|
|
{
|
|
return $this->releases->publicListing($group, $viewer, $limit);
|
|
}
|
|
|
|
public function recentChallengeCards(Group $group, ?User $viewer = null, int $limit = 3): array
|
|
{
|
|
return $this->challenges->publicListing($group, $viewer, $limit);
|
|
}
|
|
|
|
public function recentEventCards(Group $group, ?User $viewer = null, int $limit = 3): array
|
|
{
|
|
return $this->events->publicListing($group, $viewer, $limit);
|
|
}
|
|
|
|
public function publicProjectListing(Group $group, ?User $viewer = null, int $limit = 12): array
|
|
{
|
|
return $this->projects->publicListing($group, $viewer, $limit);
|
|
}
|
|
|
|
public function publicReleaseListing(Group $group, ?User $viewer = null, int $limit = 12): array
|
|
{
|
|
return $this->releases->publicListing($group, $viewer, $limit);
|
|
}
|
|
|
|
public function publicChallengeListing(Group $group, ?User $viewer = null, int $limit = 12): array
|
|
{
|
|
return $this->challenges->publicListing($group, $viewer, $limit);
|
|
}
|
|
|
|
public function publicEventListing(Group $group, ?User $viewer = null, int $limit = 12): array
|
|
{
|
|
return $this->events->publicListing($group, $viewer, $limit);
|
|
}
|
|
|
|
public function publicAssetListing(Group $group, int $limit = 12): array
|
|
{
|
|
return $this->assets->publicListing($group, $limit);
|
|
}
|
|
|
|
public function publicActivityFeed(Group $group, int $limit = 8): array
|
|
{
|
|
return $this->activity->publicFeed($group, $limit);
|
|
}
|
|
|
|
public function studioActivityFeed(Group $group, User $viewer, int $limit = 20): array
|
|
{
|
|
return $this->activity->studioFeed($group, $viewer, $limit);
|
|
}
|
|
|
|
public function publicPostListing(Group $group, int $limit = 12): array
|
|
{
|
|
return $this->posts->publicPosts($group, $limit);
|
|
}
|
|
|
|
public function recruitmentPayload(Group $group): ?array
|
|
{
|
|
return $this->recruitment->payloadForGroup($group);
|
|
}
|
|
|
|
public function recentHistory(Group $group, int $limit = 8): array
|
|
{
|
|
return $this->history->recentFor($group, $limit);
|
|
}
|
|
|
|
public function featuredArtworkCards(Group $group, int $limit = 4): array
|
|
{
|
|
$query = Artwork::query()
|
|
->with(['user.profile', 'group', 'primaryAuthor.profile'])
|
|
->where('group_id', $group->id)
|
|
->whereNull('deleted_at')
|
|
->where('is_public', true)
|
|
->where('is_approved', true)
|
|
->whereNotNull('published_at');
|
|
|
|
$this->maturity->applyViewerFilter($query, request()->user());
|
|
|
|
if ((int) ($group->featured_artwork_id ?? 0) > 0) {
|
|
$featuredArtwork = (clone $query)
|
|
->where('id', (int) $group->featured_artwork_id)
|
|
->first();
|
|
|
|
$remaining = (clone $query)
|
|
->where('id', '!=', (int) $group->featured_artwork_id)
|
|
->latest('published_at')
|
|
->limit(max($limit - ($featuredArtwork ? 1 : 0), 0))
|
|
->get();
|
|
|
|
return collect([$featuredArtwork])
|
|
->filter()
|
|
->concat($remaining)
|
|
->map(fn (Artwork $artwork): array => $this->mapPublicArtworkCard($artwork))
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
return (clone $query)
|
|
->latest('published_at')
|
|
->limit($limit)
|
|
->get()
|
|
->map(fn (Artwork $artwork): array => $this->mapPublicArtworkCard($artwork))
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
public function featuredCollectionCards(Group $group, ?User $viewer = null, int $limit = 3): array
|
|
{
|
|
$collections = Collection::query()
|
|
->with(['user.profile', 'group', 'coverArtwork'])
|
|
->where('group_id', $group->id)
|
|
->whereNull('deleted_at')
|
|
->where('is_featured', true)
|
|
->latest('featured_at')
|
|
->latest('updated_at')
|
|
->limit($limit)
|
|
->get()
|
|
->filter(fn (Collection $collection): bool => $collection->isPubliclyAccessible())
|
|
->values();
|
|
|
|
return $this->collections->mapCollectionCardPayloads($collections, false, $viewer);
|
|
}
|
|
|
|
public function mapLeadershipPreview(array $members, array $owner, int $limit = 4): array
|
|
{
|
|
$leadership = collect($members)
|
|
->filter(fn (array $member): bool => in_array((string) ($member['role'] ?? ''), [Group::ROLE_OWNER, Group::ROLE_ADMIN], true))
|
|
->map(function (array $member): array {
|
|
return [
|
|
'id' => (int) ($member['user']['id'] ?? 0),
|
|
'name' => $member['user']['name'] ?? null,
|
|
'username' => $member['user']['username'] ?? null,
|
|
'avatar_url' => $member['user']['avatar_url'] ?? null,
|
|
'profile_url' => $member['user']['profile_url'] ?? null,
|
|
'role' => (string) ($member['role'] ?? ''),
|
|
'role_label' => $member['role_label'] ?? Group::displayRole((string) ($member['role'] ?? '')),
|
|
];
|
|
})
|
|
->unique('id')
|
|
->values();
|
|
|
|
if ($leadership->isEmpty() && ! empty($owner['id'])) {
|
|
$leadership = collect([array_merge($owner, [
|
|
'role' => Group::ROLE_OWNER,
|
|
'role_label' => Group::displayRole(Group::ROLE_OWNER),
|
|
])]);
|
|
}
|
|
|
|
return $leadership->take($limit)->all();
|
|
}
|
|
|
|
public function archiveGroup(Group $group, User $actor): Group
|
|
{
|
|
if (! $group->canArchive($actor) && ! $actor->isAdmin()) {
|
|
abort(403);
|
|
}
|
|
|
|
$group->forceFill([
|
|
'status' => Group::LIFECYCLE_ARCHIVED,
|
|
'last_activity_at' => now(),
|
|
])->save();
|
|
|
|
return $group->fresh(['owner.profile', 'members']);
|
|
}
|
|
|
|
public function publicArtworkCards(Group $group, int $limit = 18): array
|
|
{
|
|
$query = Artwork::query()
|
|
->with(['user.profile', 'group', 'primaryAuthor.profile'])
|
|
->where('group_id', $group->id)
|
|
->whereNull('deleted_at')
|
|
->where('is_public', true)
|
|
->where('is_approved', true)
|
|
->whereNotNull('published_at')
|
|
->latest('published_at');
|
|
|
|
$this->maturity->applyViewerFilter($query, request()->user());
|
|
|
|
return $query
|
|
->limit($limit)
|
|
->get()
|
|
->map(fn (Artwork $artwork): array => $this->mapPublicArtworkCard($artwork))
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
public function publicCollectionCards(Group $group, ?User $viewer = null, int $limit = 12): array
|
|
{
|
|
$collections = Collection::query()
|
|
->with(['user.profile', 'group', 'coverArtwork'])
|
|
->where('group_id', $group->id)
|
|
->whereNull('deleted_at')
|
|
->latest('updated_at')
|
|
->limit($limit)
|
|
->get()
|
|
->filter(fn (Collection $collection): bool => $collection->isPubliclyAccessible())
|
|
->values();
|
|
|
|
return $this->collections->mapCollectionCardPayloads($collections, false, $viewer);
|
|
}
|
|
|
|
private function mapPublicArtworkCard(Artwork $artwork): array
|
|
{
|
|
return $this->maturity->decoratePayload([
|
|
'id' => (int) $artwork->id,
|
|
'title' => (string) $artwork->title,
|
|
'url' => route('art.show', ['id' => $artwork->id, 'slug' => Str::slug((string) ($artwork->slug ?: $artwork->title)) ?: $artwork->id]),
|
|
'thumb' => ThumbnailPresenter::present($artwork, 'md')['url'] ?? $artwork->thumbUrl('md'),
|
|
'thumb_srcset' => ThumbnailPresenter::srcsetForArtwork($artwork),
|
|
'author' => $artwork->primaryAuthor?->name ?: $artwork->primaryAuthor?->username ?: $artwork->user?->name ?: $artwork->user?->username ?: 'Artist',
|
|
'published_at' => $artwork->published_at?->toISOString(),
|
|
], $artwork, request()->user());
|
|
}
|
|
|
|
public function studioDashboardSummary(Group $group): array
|
|
{
|
|
$artworkQuery = Artwork::query()
|
|
->where('group_id', $group->id)
|
|
->whereNull('deleted_at');
|
|
|
|
$collectionQuery = Collection::query()
|
|
->where('group_id', $group->id)
|
|
->whereNull('deleted_at');
|
|
|
|
return [
|
|
'draft_artworks_count' => (clone $artworkQuery)->where('artwork_status', 'draft')->count(),
|
|
'scheduled_artworks_count' => (clone $artworkQuery)->where('artwork_status', 'scheduled')->count(),
|
|
'published_artworks_count' => (clone $artworkQuery)->where('artwork_status', 'published')->count(),
|
|
'pending_reviews_count' => (clone $artworkQuery)->where('group_review_status', 'submitted')->count(),
|
|
'draft_collections_count' => (clone $collectionQuery)
|
|
->where(function ($builder): void {
|
|
$builder->where('lifecycle_state', Collection::LIFECYCLE_DRAFT)
|
|
->orWhere('workflow_state', Collection::WORKFLOW_DRAFT)
|
|
->orWhere('workflow_state', Collection::WORKFLOW_IN_REVIEW);
|
|
})
|
|
->count(),
|
|
'active_members_count' => (int) $group->members()->where('status', Group::STATUS_ACTIVE)->count(),
|
|
'pending_invites_count' => $this->memberships->pendingInviteCount($group),
|
|
'pending_join_requests_count' => $this->joinRequests->pendingCount($group),
|
|
'published_posts_count' => (int) $group->posts()->where('status', \App\Models\GroupPost::STATUS_PUBLISHED)->count(),
|
|
'is_recruiting' => (bool) ($this->recruitment->payloadForGroup($group)['is_recruiting'] ?? false),
|
|
'projects_count' => (int) $group->projects()->count(),
|
|
'releases_count' => (int) $group->releases()->count(),
|
|
'published_releases_count' => (int) $group->releases()->where('status', \App\Models\GroupRelease::STATUS_RELEASED)->count(),
|
|
'active_challenges_count' => (int) $group->challenges()->whereIn('status', ['published', 'active'])->count(),
|
|
'events_count' => (int) $group->events()->count(),
|
|
'assets_count' => (int) $group->assets()->count(),
|
|
'activity_count' => (int) $group->activityItems()->count(),
|
|
'group_badges_count' => (int) $group->badges()->count(),
|
|
'member_badges_count' => (int) $group->memberBadges()->count(),
|
|
'trust_score' => (float) ($group->discoveryMetric?->trust_score ?? 0),
|
|
];
|
|
}
|
|
|
|
public function studioArtworkPreviewItems(Group $group, string $bucket = 'all', int $limit = 6): array
|
|
{
|
|
$query = Artwork::query()
|
|
->with(['user.profile', 'primaryAuthor.profile', 'stats'])
|
|
->where('group_id', $group->id)
|
|
->whereNull('deleted_at');
|
|
|
|
if ($bucket === 'drafts') {
|
|
$query->where('artwork_status', 'draft');
|
|
} elseif ($bucket === 'scheduled') {
|
|
$query->where('artwork_status', 'scheduled');
|
|
} elseif ($bucket === 'published') {
|
|
$query->where('artwork_status', 'published');
|
|
}
|
|
|
|
return $query->latest('updated_at')
|
|
->limit($limit)
|
|
->get()
|
|
->map(fn (Artwork $artwork): array => $this->mapStudioArtworkItem($artwork))
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
public function studioFeaturedArtworkOptions(Group $group, int $limit = 24): array
|
|
{
|
|
return Artwork::query()
|
|
->with(['user.profile', 'primaryAuthor.profile'])
|
|
->where('group_id', $group->id)
|
|
->whereNull('deleted_at')
|
|
->where('artwork_status', 'published')
|
|
->where('is_public', true)
|
|
->where('is_approved', true)
|
|
->whereNotNull('published_at')
|
|
->latest('published_at')
|
|
->limit($limit)
|
|
->get()
|
|
->map(fn (Artwork $artwork): array => [
|
|
'id' => (int) $artwork->id,
|
|
'title' => (string) $artwork->title,
|
|
'author' => $artwork->primaryAuthor?->name ?: $artwork->primaryAuthor?->username ?: $artwork->user?->name ?: $artwork->user?->username,
|
|
'thumb' => ThumbnailPresenter::present($artwork, 'md')['url'] ?? $artwork->thumbUrl('md'),
|
|
'published_at' => $artwork->published_at?->toISOString(),
|
|
])
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
public function studioCollectionPreviewItems(Group $group, int $limit = 6): array
|
|
{
|
|
return Collection::query()
|
|
->with(['user.profile', 'group', 'coverArtwork'])
|
|
->where('group_id', $group->id)
|
|
->whereNull('deleted_at')
|
|
->latest('updated_at')
|
|
->limit($limit)
|
|
->get()
|
|
->map(fn (Collection $collection): array => $this->mapStudioCollectionItem($collection))
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
public function studioArtworkListing(Group $group, array $filters = []): array
|
|
{
|
|
$bucket = (string) ($filters['bucket'] ?? 'all');
|
|
$search = trim((string) ($filters['q'] ?? ''));
|
|
$page = max(1, (int) ($filters['page'] ?? 1));
|
|
$perPage = min(max((int) ($filters['per_page'] ?? 24), 12), 48);
|
|
|
|
$query = Artwork::query()
|
|
->with(['user.profile', 'primaryAuthor.profile'])
|
|
->where('group_id', $group->id)
|
|
->whereNull('deleted_at');
|
|
|
|
if ($bucket === 'drafts') {
|
|
$query->where('artwork_status', 'draft');
|
|
} elseif ($bucket === 'scheduled') {
|
|
$query->where('artwork_status', 'scheduled');
|
|
} elseif ($bucket === 'published') {
|
|
$query->where('artwork_status', 'published');
|
|
}
|
|
|
|
if ($search !== '') {
|
|
$query->where(function ($builder) use ($search): void {
|
|
$builder->where('title', 'like', '%' . $search . '%')
|
|
->orWhere('description', 'like', '%' . $search . '%');
|
|
});
|
|
}
|
|
|
|
$paginator = $query->latest('updated_at')->paginate($perPage, ['*'], 'page', $page);
|
|
|
|
return $this->mapStudioListing(
|
|
$paginator,
|
|
fn (Artwork $artwork): array => $this->mapStudioArtworkItem($artwork),
|
|
[
|
|
['value' => 'all', 'label' => 'All'],
|
|
['value' => 'published', 'label' => 'Published'],
|
|
['value' => 'drafts', 'label' => 'Drafts'],
|
|
['value' => 'scheduled', 'label' => 'Scheduled'],
|
|
],
|
|
$bucket,
|
|
$search
|
|
);
|
|
}
|
|
|
|
public function studioCollectionListing(Group $group, array $filters = []): array
|
|
{
|
|
$bucket = (string) ($filters['bucket'] ?? 'all');
|
|
$search = trim((string) ($filters['q'] ?? ''));
|
|
$page = max(1, (int) ($filters['page'] ?? 1));
|
|
$perPage = min(max((int) ($filters['per_page'] ?? 24), 12), 48);
|
|
|
|
$query = Collection::query()
|
|
->with(['user.profile', 'group', 'coverArtwork'])
|
|
->where('group_id', $group->id)
|
|
->whereNull('deleted_at');
|
|
|
|
if ($bucket === 'drafts') {
|
|
$query->where(function ($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->where('lifecycle_state', Collection::LIFECYCLE_SCHEDULED);
|
|
} elseif ($bucket === 'published') {
|
|
$query->whereIn('lifecycle_state', [Collection::LIFECYCLE_PUBLISHED, Collection::LIFECYCLE_FEATURED, Collection::LIFECYCLE_SCHEDULED]);
|
|
}
|
|
|
|
if ($search !== '') {
|
|
$query->where(function ($builder) use ($search): void {
|
|
$builder->where('title', 'like', '%' . $search . '%')
|
|
->orWhere('description', 'like', '%' . $search . '%');
|
|
});
|
|
}
|
|
|
|
$paginator = $query->latest('updated_at')->paginate($perPage, ['*'], 'page', $page);
|
|
|
|
return $this->mapStudioListing(
|
|
$paginator,
|
|
fn (Collection $collection): array => $this->mapStudioCollectionItem($collection),
|
|
[
|
|
['value' => 'all', 'label' => 'All'],
|
|
['value' => 'published', 'label' => 'Published'],
|
|
['value' => 'drafts', 'label' => 'Drafts'],
|
|
['value' => 'scheduled', 'label' => 'Scheduled'],
|
|
],
|
|
$bucket,
|
|
$search
|
|
);
|
|
}
|
|
|
|
private function mapStudioListing(LengthAwarePaginator $paginator, callable $mapper, array $bucketOptions, string $bucket, string $search): array
|
|
{
|
|
return [
|
|
'items' => collect($paginator->items())->map($mapper)->values()->all(),
|
|
'meta' => [
|
|
'current_page' => $paginator->currentPage(),
|
|
'last_page' => $paginator->lastPage(),
|
|
'per_page' => $paginator->perPage(),
|
|
'total' => $paginator->total(),
|
|
],
|
|
'filters' => [
|
|
'bucket' => $bucket,
|
|
'q' => $search,
|
|
'sort' => 'updated_desc',
|
|
],
|
|
'module_options' => [],
|
|
'bucket_options' => $bucketOptions,
|
|
'sort_options' => [
|
|
['value' => 'updated_desc', 'label' => 'Recently updated'],
|
|
],
|
|
'advanced_filters' => [],
|
|
'default_view' => 'grid',
|
|
];
|
|
}
|
|
|
|
private function mapStudioArtworkItem(Artwork $artwork): array
|
|
{
|
|
$status = (string) ($artwork->artwork_status ?: ($artwork->published_at ? 'published' : 'draft'));
|
|
|
|
return [
|
|
'id' => 'artworks:' . (int) $artwork->id,
|
|
'numeric_id' => (int) $artwork->id,
|
|
'module' => 'artworks',
|
|
'module_label' => 'Artworks',
|
|
'module_icon' => 'fa-solid fa-images',
|
|
'title' => (string) $artwork->title,
|
|
'subtitle' => $artwork->primaryAuthor?->name ?: $artwork->primaryAuthor?->username ?: $artwork->user?->name ?: $artwork->user?->username,
|
|
'description' => $artwork->description,
|
|
'status' => $status,
|
|
'visibility' => (string) ($artwork->visibility ?: Artwork::VISIBILITY_PRIVATE),
|
|
'image_url' => ThumbnailPresenter::present($artwork, 'md')['url'] ?? $artwork->thumbUrl('md'),
|
|
'preview_url' => route('art.show', ['id' => $artwork->id, 'slug' => Str::slug((string) ($artwork->slug ?: $artwork->title)) ?: $artwork->id]),
|
|
'view_url' => route('art.show', ['id' => $artwork->id, 'slug' => Str::slug((string) ($artwork->slug ?: $artwork->title)) ?: $artwork->id]),
|
|
'edit_url' => route('studio.artworks.edit', ['id' => $artwork->id]),
|
|
'manage_url' => route('studio.artworks.edit', ['id' => $artwork->id]),
|
|
'analytics_url' => route('studio.artworks.analytics', ['id' => $artwork->id]),
|
|
'created_at' => $artwork->created_at?->toISOString(),
|
|
'updated_at' => $artwork->updated_at?->toISOString(),
|
|
'published_at' => $artwork->published_at?->toISOString(),
|
|
'metrics' => [
|
|
'views' => (int) ($artwork->stats?->views ?? 0),
|
|
'appreciation' => (int) ($artwork->stats?->favorites ?? 0),
|
|
'comments' => (int) $artwork->comments()->count(),
|
|
],
|
|
'actions' => [],
|
|
];
|
|
}
|
|
|
|
private function mapStudioCollectionItem(Collection $collection): array
|
|
{
|
|
$mapped = $this->collections->mapCollectionCardPayloads([$collection->loadMissing(['user.profile', 'group', 'coverArtwork'])], true)[0];
|
|
$status = $mapped['lifecycle_state'] === Collection::LIFECYCLE_FEATURED ? 'published' : ($mapped['lifecycle_state'] ?? 'draft');
|
|
|
|
return [
|
|
'id' => 'collections:' . (int) $collection->id,
|
|
'numeric_id' => (int) $collection->id,
|
|
'module' => 'collections',
|
|
'module_label' => 'Collections',
|
|
'module_icon' => 'fa-solid fa-layer-group',
|
|
'title' => (string) $mapped['title'],
|
|
'subtitle' => $mapped['subtitle'] ?: ucfirst((string) ($mapped['type'] ?? 'collection')),
|
|
'description' => $mapped['summary'] ?: $mapped['description'],
|
|
'status' => $status,
|
|
'visibility' => (string) $mapped['visibility'],
|
|
'image_url' => $mapped['cover_image'],
|
|
'preview_url' => $mapped['url'],
|
|
'view_url' => $mapped['url'],
|
|
'edit_url' => $mapped['edit_url'] ?: $mapped['manage_url'],
|
|
'manage_url' => $mapped['manage_url'],
|
|
'analytics_url' => route('settings.collections.analytics', ['collection' => $collection->id]),
|
|
'created_at' => ($mapped['published_at'] ?? null) ?: ($mapped['updated_at'] ?? null),
|
|
'updated_at' => $mapped['updated_at'] ?? null,
|
|
'published_at' => $mapped['published_at'] ?? null,
|
|
'metrics' => [
|
|
'views' => (int) ($mapped['views_count'] ?? 0),
|
|
'appreciation' => (int) (($mapped['likes_count'] ?? 0) + ($mapped['followers_count'] ?? 0)),
|
|
'comments' => (int) ($mapped['comments_count'] ?? 0),
|
|
],
|
|
'actions' => [],
|
|
];
|
|
}
|
|
|
|
private function normalizeLinks(mixed $links): array
|
|
{
|
|
$items = is_array($links) ? $links : [];
|
|
|
|
return collect($items)
|
|
->filter(fn ($item): bool => is_array($item))
|
|
->map(function (array $item): array {
|
|
return [
|
|
'label' => trim((string) ($item['label'] ?? '')),
|
|
'url' => trim((string) ($item['url'] ?? '')),
|
|
];
|
|
})
|
|
->filter(fn (array $item): bool => $item['label'] !== '' && $item['url'] !== '')
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
private function normalizeMediaPath(mixed $path): ?string
|
|
{
|
|
$trimmed = trim((string) $path);
|
|
|
|
return $trimmed !== '' ? $trimmed : null;
|
|
}
|
|
|
|
private function normalizeFeaturedArtworkId(Group $group, mixed $featuredArtworkId): ?int
|
|
{
|
|
$id = (int) $featuredArtworkId;
|
|
|
|
if ($id <= 0) {
|
|
return null;
|
|
}
|
|
|
|
$exists = Artwork::query()
|
|
->where('id', $id)
|
|
->where('group_id', $group->id)
|
|
->whereNull('deleted_at')
|
|
->where('artwork_status', 'published')
|
|
->where('is_public', true)
|
|
->where('is_approved', true)
|
|
->whereNotNull('published_at')
|
|
->exists();
|
|
|
|
return $exists ? $id : null;
|
|
}
|
|
} |