Files
2026-04-18 17:02:56 +02:00

868 lines
28 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Laravel\Scout\Searchable;
class Group extends Model
{
use HasFactory;
use SoftDeletes;
use Searchable;
public const VISIBILITY_PUBLIC = 'public';
public const VISIBILITY_PRIVATE = 'private';
public const VISIBILITY_UNLISTED = 'unlisted';
public const LIFECYCLE_ACTIVE = 'active';
public const LIFECYCLE_ARCHIVED = 'archived';
public const LIFECYCLE_SUSPENDED = 'suspended';
public const MEMBERSHIP_INVITE_ONLY = 'invite_only';
public const MEMBERSHIP_REQUEST_TO_JOIN = 'request_to_join';
public const MEMBERSHIP_OPEN = 'open';
public const ROLE_OWNER = 'owner';
public const ROLE_ADMIN = 'admin';
public const ROLE_EDITOR = 'editor';
public const ROLE_MEMBER = 'member';
public const ROLE_CONTRIBUTOR = 'contributor';
public const PERMISSION_REVIEW_JOIN_REQUESTS = 'review_join_requests';
public const PERMISSION_REVIEW_SUBMISSIONS = 'review_submissions';
public const PERMISSION_MANAGE_RECRUITMENT = 'manage_recruitment';
public const PERMISSION_MANAGE_POSTS = 'manage_posts';
public const PERMISSION_PUBLISH_POSTS = 'publish_posts';
public const PERMISSION_PIN_POSTS = 'pin_posts';
public const PERMISSION_MANAGE_MEMBER_PERMISSIONS = 'manage_member_permissions';
public const PERMISSION_MANAGE_EVENTS = 'manage_events';
public const PERMISSION_MANAGE_CHALLENGES = 'manage_challenges';
public const PERMISSION_MANAGE_PROJECTS = 'manage_projects';
public const PERMISSION_MANAGE_RELEASES = 'manage_releases';
public const PERMISSION_PUBLISH_RELEASES = 'publish_releases';
public const PERMISSION_MANAGE_MILESTONES = 'manage_milestones';
public const PERMISSION_VIEW_REPUTATION_DASHBOARD = 'view_reputation_dashboard';
public const PERMISSION_MANAGE_BADGES = 'manage_badges';
public const PERMISSION_VIEW_INTERNAL_TRUST_METRICS = 'view_internal_trust_metrics';
public const PERMISSION_FEATURE_RELEASES = 'feature_releases';
public const PERMISSION_ASSIGN_RELEASE_LEAD = 'assign_release_lead';
public const PERMISSION_MANAGE_ASSETS = 'manage_assets';
public const PERMISSION_FEATURE_CHALLENGE_ENTRIES = 'feature_challenge_entries';
public const PERMISSION_PUBLISH_EVENT_UPDATES = 'publish_event_updates';
public const PERMISSION_ATTACH_ASSETS_TO_PROJECTS = 'attach_assets_to_projects';
public const PERMISSION_VIEW_INTERNAL_ASSETS = 'view_internal_assets';
public const PERMISSION_MANAGE_ACTIVITY_PINS = 'manage_activity_pins';
public const STATUS_PENDING = 'pending';
public const STATUS_ACTIVE = 'active';
public const STATUS_REVOKED = 'revoked';
protected $fillable = [
'owner_user_id',
'featured_artwork_id',
'is_verified',
'founded_at',
'name',
'slug',
'headline',
'bio',
'type',
'visibility',
'status',
'membership_policy',
'website_url',
'links_json',
'avatar_path',
'banner_path',
'artworks_count',
'collections_count',
'followers_count',
'last_activity_at',
];
protected $casts = [
'links_json' => 'array',
'is_verified' => 'boolean',
'artworks_count' => 'integer',
'collections_count' => 'integer',
'followers_count' => 'integer',
'founded_at' => 'datetime',
'last_activity_at' => 'datetime',
];
public function owner(): BelongsTo
{
return $this->belongsTo(User::class, 'owner_user_id');
}
public function getRouteKeyName(): string
{
return 'slug';
}
public function members(): HasMany
{
return $this->hasMany(GroupMember::class);
}
public function invitations(): HasMany
{
return $this->hasMany(GroupInvitation::class);
}
public function follows(): HasMany
{
return $this->hasMany(GroupFollow::class);
}
public function artworks(): HasMany
{
return $this->hasMany(Artwork::class);
}
public function collections(): HasMany
{
return $this->hasMany(Collection::class);
}
public function joinRequests(): HasMany
{
return $this->hasMany(GroupJoinRequest::class);
}
public function posts(): HasMany
{
return $this->hasMany(GroupPost::class);
}
public function recruitmentProfile(): HasOne
{
return $this->hasOne(GroupRecruitmentProfile::class);
}
public function projects(): HasMany
{
return $this->hasMany(GroupProject::class);
}
public function releases(): HasMany
{
return $this->hasMany(GroupRelease::class);
}
public function challenges(): HasMany
{
return $this->hasMany(GroupChallenge::class);
}
public function events(): HasMany
{
return $this->hasMany(GroupEvent::class);
}
public function assets(): HasMany
{
return $this->hasMany(GroupAsset::class);
}
public function activityItems(): HasMany
{
return $this->hasMany(GroupActivityItem::class);
}
public function contributorStats(): HasMany
{
return $this->hasMany(GroupContributorStat::class);
}
public function badges(): HasMany
{
return $this->hasMany(GroupBadge::class);
}
public function memberBadges(): HasMany
{
return $this->hasMany(GroupMemberBadge::class);
}
public function discoveryMetric(): HasOne
{
return $this->hasOne(GroupDiscoveryMetric::class);
}
public function historyEntries(): HasMany
{
return $this->hasMany(GroupHistory::class);
}
public function scopePublic(Builder $query): Builder
{
return $query
->where('visibility', self::VISIBILITY_PUBLIC)
->where('status', self::LIFECYCLE_ACTIVE);
}
public static function acceptedVisibilityValues(): array
{
return [self::VISIBILITY_PUBLIC, self::VISIBILITY_PRIVATE, self::VISIBILITY_UNLISTED];
}
public static function acceptedMembershipPolicies(): array
{
return [self::MEMBERSHIP_INVITE_ONLY, self::MEMBERSHIP_REQUEST_TO_JOIN, self::MEMBERSHIP_OPEN];
}
public static function normalizeMemberRole(string $role): string
{
$normalized = strtolower(trim($role));
return $normalized === self::ROLE_CONTRIBUTOR ? self::ROLE_MEMBER : $normalized;
}
public static function displayRole(?string $role): ?string
{
if ($role === null) {
return null;
}
return self::normalizeMemberRole($role) === self::ROLE_MEMBER ? self::ROLE_CONTRIBUTOR : self::normalizeMemberRole($role);
}
public function isOwnedBy(User|int|null $user): bool
{
$userId = $user instanceof User ? $user->id : $user;
return $userId !== null && (int) $userId === (int) $this->owner_user_id;
}
public function isPubliclyVisible(): bool
{
return in_array($this->visibility, [self::VISIBILITY_PUBLIC, self::VISIBILITY_UNLISTED], true)
&& $this->status !== self::LIFECYCLE_SUSPENDED;
}
public function isOperational(): bool
{
return $this->status === self::LIFECYCLE_ACTIVE;
}
public function canArchive(User $user): bool
{
return $this->isOwnedBy($user);
}
public function canViewStudio(User $user): bool
{
if ($this->status === self::LIFECYCLE_SUSPENDED) {
return false;
}
return $this->hasActiveMember($user);
}
public function activeRoleFor(User|int|null $user): ?string
{
$userId = $user instanceof User ? $user->id : $user;
if ($userId === null) {
return null;
}
if ($this->isOwnedBy($userId)) {
return self::ROLE_OWNER;
}
$members = $this->relationLoaded('members')
? $this->members
: $this->members()->where('status', self::STATUS_ACTIVE)->get();
return $members
->first(fn (GroupMember $member): bool => (int) $member->user_id === (int) $userId && $member->status === self::STATUS_ACTIVE)
?->role;
}
public function hasActiveMember(User|int|null $user): bool
{
return $this->activeRoleFor($user) !== null;
}
public function activeMembershipFor(User|int|null $user): ?GroupMember
{
$userId = $user instanceof User ? $user->id : $user;
if ($userId === null || $this->isOwnedBy($userId)) {
return null;
}
$members = $this->relationLoaded('members')
? $this->members
: $this->members()->where('status', self::STATUS_ACTIVE)->get();
return $members->first(
fn (GroupMember $member): bool => (int) $member->user_id === (int) $userId && $member->status === self::STATUS_ACTIVE
);
}
public function permissionOverridesFor(User|int|null $user): array
{
if ($this->isOwnedBy($user)) {
return collect(self::allowedPermissionOverrides())
->mapWithKeys(fn (string $permission): array => [$permission => true])
->all();
}
$member = $this->activeMembershipFor($user);
if (! $member) {
return [];
}
return collect($member->permission_overrides_json ?? [])
->mapWithKeys(function ($override): array {
if (is_array($override)) {
$key = trim((string) ($override['key'] ?? ''));
if ($key === '' || ! in_array($key, self::allowedPermissionOverrides(), true)) {
return [];
}
return [$key => (bool) ($override['is_allowed'] ?? false)];
}
$key = trim((string) $override);
if ($key === '' || ! in_array($key, self::allowedPermissionOverrides(), true)) {
return [];
}
return [$key => true];
})
->all();
}
public function hasPermission(User|int|null $user, string $permission): bool
{
return $this->permissionOverridesFor($user)[$permission] ?? false;
}
public function hasDeniedPermission(User|int|null $user, string $permission): bool
{
$overrides = $this->permissionOverridesFor($user);
return array_key_exists($permission, $overrides) && $overrides[$permission] === false;
}
public static function permissionKeys(): array
{
return self::allowedPermissionOverrides();
}
public function canBeViewedBy(?User $user): bool
{
if ($this->isPubliclyVisible()) {
return true;
}
return $user !== null && $this->hasActiveMember($user);
}
public function canManage(User $user): bool
{
return in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN], true);
}
public function canManageMembers(User $user): bool
{
return in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN], true);
}
public function canPublishArtworks(User $user): bool
{
return $this->isOperational()
&& in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN, self::ROLE_EDITOR], true);
}
public function canCreateArtworkDrafts(User $user): bool
{
return $this->isOperational() && $this->hasActiveMember($user);
}
public function canSubmitArtworkForReview(User $user): bool
{
return $this->isOperational() && $this->hasActiveMember($user);
}
public function canReviewSubmissions(User $user): bool
{
if (! $this->isOperational()) {
return false;
}
if ($this->hasDeniedPermission($user, self::PERMISSION_REVIEW_SUBMISSIONS)) {
return false;
}
$role = $this->activeRoleFor($user);
return in_array($role, [self::ROLE_OWNER, self::ROLE_ADMIN, self::ROLE_EDITOR], true)
|| $this->hasPermission($user, self::PERMISSION_REVIEW_SUBMISSIONS);
}
public function canRequestJoin(?User $user): bool
{
if (! $this->isOperational() || $user === null || $this->hasActiveMember($user)) {
return false;
}
return in_array($this->membership_policy, [self::MEMBERSHIP_REQUEST_TO_JOIN, self::MEMBERSHIP_OPEN], true);
}
public function canReviewJoinRequests(User $user): bool
{
if (! $this->isOperational()) {
return false;
}
if ($this->hasDeniedPermission($user, self::PERMISSION_REVIEW_JOIN_REQUESTS)) {
return false;
}
$role = $this->activeRoleFor($user);
return in_array($role, [self::ROLE_OWNER, self::ROLE_ADMIN], true)
|| $this->hasPermission($user, self::PERMISSION_REVIEW_JOIN_REQUESTS);
}
public function canManageRecruitment(User $user): bool
{
if (! $this->isOperational()) {
return false;
}
if ($this->hasDeniedPermission($user, self::PERMISSION_MANAGE_RECRUITMENT)) {
return false;
}
$role = $this->activeRoleFor($user);
return in_array($role, [self::ROLE_OWNER, self::ROLE_ADMIN, self::ROLE_EDITOR], true)
|| $this->hasPermission($user, self::PERMISSION_MANAGE_RECRUITMENT);
}
public function canManagePosts(User $user): bool
{
if (! $this->isOperational()) {
return false;
}
if ($this->hasDeniedPermission($user, self::PERMISSION_MANAGE_POSTS)) {
return false;
}
$role = $this->activeRoleFor($user);
return in_array($role, [self::ROLE_OWNER, self::ROLE_ADMIN, self::ROLE_EDITOR], true)
|| $this->hasPermission($user, self::PERMISSION_MANAGE_POSTS);
}
public function canPublishPosts(User $user): bool
{
if (! $this->isOperational()) {
return false;
}
if ($this->hasDeniedPermission($user, self::PERMISSION_PUBLISH_POSTS)) {
return false;
}
$role = $this->activeRoleFor($user);
return in_array($role, [self::ROLE_OWNER, self::ROLE_ADMIN, self::ROLE_EDITOR], true)
|| $this->hasPermission($user, self::PERMISSION_PUBLISH_POSTS)
|| $this->canManagePosts($user);
}
public function canPinPosts(User $user): bool
{
if (! $this->isOperational()) {
return false;
}
if ($this->hasDeniedPermission($user, self::PERMISSION_PIN_POSTS)) {
return false;
}
return in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN], true)
|| $this->hasPermission($user, self::PERMISSION_PIN_POSTS);
}
public function canManageMemberPermissions(User $user): bool
{
if (! $this->isOperational()) {
return false;
}
if ($this->hasDeniedPermission($user, self::PERMISSION_MANAGE_MEMBER_PERMISSIONS)) {
return false;
}
return in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN], true)
|| $this->hasPermission($user, self::PERMISSION_MANAGE_MEMBER_PERMISSIONS);
}
public function canManageEvents(User $user): bool
{
if (! $this->isOperational()) {
return false;
}
if ($this->hasDeniedPermission($user, self::PERMISSION_MANAGE_EVENTS)) {
return false;
}
return in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN, self::ROLE_EDITOR], true)
|| $this->hasPermission($user, self::PERMISSION_MANAGE_EVENTS);
}
public function canManageChallenges(User $user): bool
{
if (! $this->isOperational()) {
return false;
}
if ($this->hasDeniedPermission($user, self::PERMISSION_MANAGE_CHALLENGES)) {
return false;
}
return in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN, self::ROLE_EDITOR], true)
|| $this->hasPermission($user, self::PERMISSION_MANAGE_CHALLENGES);
}
public function canManageProjects(User $user): bool
{
if (! $this->isOperational()) {
return false;
}
if ($this->hasDeniedPermission($user, self::PERMISSION_MANAGE_PROJECTS)) {
return false;
}
return in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN, self::ROLE_EDITOR], true)
|| $this->hasPermission($user, self::PERMISSION_MANAGE_PROJECTS);
}
public function canManageReleases(User $user): bool
{
if (! $this->isOperational()) {
return false;
}
if ($this->hasDeniedPermission($user, self::PERMISSION_MANAGE_RELEASES)) {
return false;
}
return $this->canManageProjects($user)
|| $this->hasPermission($user, self::PERMISSION_MANAGE_RELEASES);
}
public function canPublishReleases(User $user): bool
{
if (! $this->isOperational()) {
return false;
}
if ($this->hasDeniedPermission($user, self::PERMISSION_PUBLISH_RELEASES)) {
return false;
}
return in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN], true)
|| $this->hasPermission($user, self::PERMISSION_PUBLISH_RELEASES)
|| $this->canManageReleases($user);
}
public function canManageMilestones(User $user): bool
{
if (! $this->isOperational()) {
return false;
}
if ($this->hasDeniedPermission($user, self::PERMISSION_MANAGE_MILESTONES)) {
return false;
}
return $this->canManageProjects($user)
|| $this->canManageReleases($user)
|| $this->hasPermission($user, self::PERMISSION_MANAGE_MILESTONES);
}
public function canViewReputationDashboard(User $user): bool
{
if (! $this->isOperational()) {
return false;
}
if ($this->hasDeniedPermission($user, self::PERMISSION_VIEW_REPUTATION_DASHBOARD)) {
return false;
}
return in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN], true)
|| $this->hasPermission($user, self::PERMISSION_VIEW_REPUTATION_DASHBOARD);
}
public function canManageBadges(User $user): bool
{
if (! $this->isOperational()) {
return false;
}
if ($this->hasDeniedPermission($user, self::PERMISSION_MANAGE_BADGES)) {
return false;
}
return in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN], true)
|| $this->hasPermission($user, self::PERMISSION_MANAGE_BADGES);
}
public function canViewInternalTrustMetrics(User $user): bool
{
if (! $this->isOperational()) {
return false;
}
if ($this->hasDeniedPermission($user, self::PERMISSION_VIEW_INTERNAL_TRUST_METRICS)) {
return false;
}
return in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN], true)
|| $this->hasPermission($user, self::PERMISSION_VIEW_INTERNAL_TRUST_METRICS);
}
public function canFeatureReleases(User $user): bool
{
if (! $this->isOperational()) {
return false;
}
if ($this->hasDeniedPermission($user, self::PERMISSION_FEATURE_RELEASES)) {
return false;
}
return in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN], true)
|| $this->hasPermission($user, self::PERMISSION_FEATURE_RELEASES);
}
public function canAssignReleaseLead(User $user): bool
{
if (! $this->isOperational()) {
return false;
}
if ($this->hasDeniedPermission($user, self::PERMISSION_ASSIGN_RELEASE_LEAD)) {
return false;
}
return $this->canManageReleases($user)
|| $this->hasPermission($user, self::PERMISSION_ASSIGN_RELEASE_LEAD);
}
public function canManageAssets(User $user): bool
{
if (! $this->isOperational()) {
return false;
}
if ($this->hasDeniedPermission($user, self::PERMISSION_MANAGE_ASSETS)) {
return false;
}
return in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN, self::ROLE_EDITOR], true)
|| $this->hasPermission($user, self::PERMISSION_MANAGE_ASSETS);
}
public function canFeatureChallengeEntries(User $user): bool
{
if (! $this->isOperational()) {
return false;
}
if ($this->hasDeniedPermission($user, self::PERMISSION_FEATURE_CHALLENGE_ENTRIES)) {
return false;
}
return in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN, self::ROLE_EDITOR], true)
|| $this->hasPermission($user, self::PERMISSION_FEATURE_CHALLENGE_ENTRIES);
}
public function canPublishEventUpdates(User $user): bool
{
if (! $this->isOperational()) {
return false;
}
if ($this->hasDeniedPermission($user, self::PERMISSION_PUBLISH_EVENT_UPDATES)) {
return false;
}
return $this->canManageEvents($user)
|| $this->hasPermission($user, self::PERMISSION_PUBLISH_EVENT_UPDATES);
}
public function canAttachAssetsToProjects(User $user): bool
{
if (! $this->isOperational()) {
return false;
}
if ($this->hasDeniedPermission($user, self::PERMISSION_ATTACH_ASSETS_TO_PROJECTS)) {
return false;
}
return $this->canManageProjects($user)
|| $this->hasPermission($user, self::PERMISSION_ATTACH_ASSETS_TO_PROJECTS);
}
public function canViewInternalAssets(User $user): bool
{
if (! $this->isOperational()) {
return false;
}
if ($this->hasDeniedPermission($user, self::PERMISSION_VIEW_INTERNAL_ASSETS)) {
return false;
}
return in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN, self::ROLE_EDITOR], true)
|| $this->hasPermission($user, self::PERMISSION_VIEW_INTERNAL_ASSETS);
}
public function canPinActivity(User $user): bool
{
if (! $this->isOperational()) {
return false;
}
if ($this->hasDeniedPermission($user, self::PERMISSION_MANAGE_ACTIVITY_PINS)) {
return false;
}
return in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN], true)
|| $this->hasPermission($user, self::PERMISSION_MANAGE_ACTIVITY_PINS);
}
public static function allowedPermissionOverrides(): array
{
return [
self::PERMISSION_REVIEW_JOIN_REQUESTS,
self::PERMISSION_REVIEW_SUBMISSIONS,
self::PERMISSION_MANAGE_RECRUITMENT,
self::PERMISSION_MANAGE_POSTS,
self::PERMISSION_PUBLISH_POSTS,
self::PERMISSION_PIN_POSTS,
self::PERMISSION_MANAGE_MEMBER_PERMISSIONS,
self::PERMISSION_MANAGE_EVENTS,
self::PERMISSION_MANAGE_CHALLENGES,
self::PERMISSION_MANAGE_PROJECTS,
self::PERMISSION_MANAGE_RELEASES,
self::PERMISSION_PUBLISH_RELEASES,
self::PERMISSION_MANAGE_MILESTONES,
self::PERMISSION_VIEW_REPUTATION_DASHBOARD,
self::PERMISSION_MANAGE_BADGES,
self::PERMISSION_VIEW_INTERNAL_TRUST_METRICS,
self::PERMISSION_FEATURE_RELEASES,
self::PERMISSION_ASSIGN_RELEASE_LEAD,
self::PERMISSION_MANAGE_ASSETS,
self::PERMISSION_FEATURE_CHALLENGE_ENTRIES,
self::PERMISSION_PUBLISH_EVENT_UPDATES,
self::PERMISSION_ATTACH_ASSETS_TO_PROJECTS,
self::PERMISSION_VIEW_INTERNAL_ASSETS,
self::PERMISSION_MANAGE_ACTIVITY_PINS,
];
}
public function canManageCollections(User $user): bool
{
return $this->isOperational() && $this->canPublishArtworks($user);
}
public function avatarUrl(): ?string
{
return $this->assetUrl($this->avatar_path);
}
public function bannerUrl(): ?string
{
return $this->assetUrl($this->banner_path);
}
public function publicUrl(): string
{
return route('groups.show', ['group' => $this->slug]);
}
public function shouldBeSearchable(): bool
{
return $this->visibility === self::VISIBILITY_PUBLIC && $this->status === self::LIFECYCLE_ACTIVE;
}
public function toSearchableArray(): array
{
$recruitment = $this->relationLoaded('recruitmentProfile')
? $this->recruitmentProfile
: $this->recruitmentProfile()->first();
$memberNames = $this->members()
->with('user:id,name,username')
->where('status', self::STATUS_ACTIVE)
->limit(12)
->get()
->map(fn (GroupMember $member): string => (string) ($member->user?->name ?: $member->user?->username ?: ''))
->filter()
->values()
->all();
return [
'id' => (int) $this->id,
'name' => (string) $this->name,
'slug' => (string) $this->slug,
'headline' => (string) ($this->headline ?? ''),
'bio' => (string) ($this->bio ?? ''),
'type' => (string) ($this->type ?? ''),
'visibility' => (string) $this->visibility,
'status' => (string) ($this->status ?? self::LIFECYCLE_ACTIVE),
'artworks_count' => (int) ($this->artworks_count ?? 0),
'followers_count' => (int) ($this->followers_count ?? 0),
'is_recruiting' => (bool) ($recruitment?->is_recruiting ?? false),
'recruitment_headline' => (string) ($recruitment?->headline ?? ''),
'recruitment_roles' => array_values(array_filter($recruitment?->roles_json ?? [])),
'recruitment_skills' => array_values(array_filter($recruitment?->skills_json ?? [])),
'release_titles' => $this->releases()->where('visibility', GroupRelease::VISIBILITY_PUBLIC)->latest('published_at')->limit(6)->pluck('title')->filter()->values()->all(),
'project_titles' => $this->projects()->where('visibility', GroupProject::VISIBILITY_PUBLIC)->latest('updated_at')->limit(6)->pluck('title')->filter()->values()->all(),
'challenge_titles' => $this->challenges()->where('visibility', GroupChallenge::VISIBILITY_PUBLIC)->latest('updated_at')->limit(6)->pluck('title')->filter()->values()->all(),
'event_titles' => $this->events()->where('visibility', GroupEvent::VISIBILITY_PUBLIC)->latest('start_at')->limit(6)->pluck('title')->filter()->values()->all(),
'badge_keys' => $this->badges()->latest('awarded_at')->limit(6)->pluck('badge_key')->filter()->values()->all(),
'member_names' => $memberNames,
];
}
private function assetUrl(?string $path): ?string
{
$trimmed = trim((string) $path);
if ($trimmed === '') {
return null;
}
if (str_starts_with($trimmed, 'http://') || str_starts_with($trimmed, 'https://')) {
return $trimmed;
}
return rtrim((string) config('cdn.files_url', 'https://files.skinbase.org'), '/') . '/' . ltrim($trimmed, '/');
}
}