Files
SkinbaseNova/app/Services/Studio/CreatorStudioPreferenceService.php

260 lines
10 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Studio;
use App\Models\DashboardPreference;
use App\Models\User;
final class CreatorStudioPreferenceService
{
/**
* @return array{
* default_content_view: string,
* analytics_range_days: int,
* dashboard_shortcuts: array<int, string>,
* featured_modules: array<int, string>,
* featured_content: array<string, int>,
* draft_behavior: string,
* default_landing_page: string,
* widget_visibility: array<string, bool>,
* widget_order: array<int, string>,
* card_density: string,
* scheduling_timezone: string|null,
* activity_last_read_at: string|null
* }
*/
public function forUser(User $user): array
{
$record = DashboardPreference::query()->find($user->id);
$stored = is_array($record?->studio_preferences) ? $record->studio_preferences : [];
return [
'default_content_view' => $this->normalizeView((string) ($stored['default_content_view'] ?? 'grid')),
'analytics_range_days' => $this->normalizeRange((int) ($stored['analytics_range_days'] ?? 30)),
'dashboard_shortcuts' => DashboardPreference::pinnedSpacesForUser($user),
'featured_modules' => $this->normalizeModules($stored['featured_modules'] ?? []),
'featured_content' => $this->normalizeFeaturedContent($stored['featured_content'] ?? []),
'draft_behavior' => $this->normalizeDraftBehavior((string) ($stored['draft_behavior'] ?? 'resume-last')),
'default_landing_page' => $this->normalizeLandingPage((string) ($stored['default_landing_page'] ?? 'overview')),
'widget_visibility' => $this->normalizeWidgetVisibility($stored['widget_visibility'] ?? []),
'widget_order' => $this->normalizeWidgetOrder($stored['widget_order'] ?? []),
'card_density' => $this->normalizeDensity((string) ($stored['card_density'] ?? 'comfortable')),
'scheduling_timezone' => $this->normalizeTimezone($stored['scheduling_timezone'] ?? null),
'activity_last_read_at' => $this->normalizeActivityLastReadAt($stored['activity_last_read_at'] ?? null),
];
}
/**
* @param array<string, mixed> $attributes
* @return array{
* default_content_view: string,
* analytics_range_days: int,
* dashboard_shortcuts: array<int, string>,
* featured_modules: array<int, string>,
* featured_content: array<string, int>,
* draft_behavior: string,
* default_landing_page: string,
* widget_visibility: array<string, bool>,
* widget_order: array<int, string>,
* card_density: string,
* scheduling_timezone: string|null,
* activity_last_read_at: string|null
* }
*/
public function update(User $user, array $attributes): array
{
$current = $this->forUser($user);
$payload = [
'default_content_view' => array_key_exists('default_content_view', $attributes)
? $this->normalizeView((string) $attributes['default_content_view'])
: $current['default_content_view'],
'analytics_range_days' => array_key_exists('analytics_range_days', $attributes)
? $this->normalizeRange((int) $attributes['analytics_range_days'])
: $current['analytics_range_days'],
'featured_modules' => array_key_exists('featured_modules', $attributes)
? $this->normalizeModules($attributes['featured_modules'])
: $current['featured_modules'],
'featured_content' => array_key_exists('featured_content', $attributes)
? $this->normalizeFeaturedContent($attributes['featured_content'])
: $current['featured_content'],
'draft_behavior' => array_key_exists('draft_behavior', $attributes)
? $this->normalizeDraftBehavior((string) $attributes['draft_behavior'])
: $current['draft_behavior'],
'default_landing_page' => array_key_exists('default_landing_page', $attributes)
? $this->normalizeLandingPage((string) $attributes['default_landing_page'])
: $current['default_landing_page'],
'widget_visibility' => array_key_exists('widget_visibility', $attributes)
? $this->normalizeWidgetVisibility($attributes['widget_visibility'])
: $current['widget_visibility'],
'widget_order' => array_key_exists('widget_order', $attributes)
? $this->normalizeWidgetOrder($attributes['widget_order'])
: $current['widget_order'],
'card_density' => array_key_exists('card_density', $attributes)
? $this->normalizeDensity((string) $attributes['card_density'])
: $current['card_density'],
'scheduling_timezone' => array_key_exists('scheduling_timezone', $attributes)
? $this->normalizeTimezone($attributes['scheduling_timezone'])
: $current['scheduling_timezone'],
'activity_last_read_at' => array_key_exists('activity_last_read_at', $attributes)
? $this->normalizeActivityLastReadAt($attributes['activity_last_read_at'])
: $current['activity_last_read_at'],
];
$record = DashboardPreference::query()->firstOrNew(['user_id' => $user->id]);
$record->pinned_spaces = array_key_exists('dashboard_shortcuts', $attributes)
? DashboardPreference::sanitizePinnedSpaces(is_array($attributes['dashboard_shortcuts']) ? $attributes['dashboard_shortcuts'] : [])
: $current['dashboard_shortcuts'];
$record->studio_preferences = $payload;
$record->save();
return $this->forUser($user);
}
private function normalizeView(string $view): string
{
return in_array($view, ['grid', 'list'], true) ? $view : 'grid';
}
private function normalizeRange(int $days): int
{
return in_array($days, [7, 14, 30, 60, 90], true) ? $days : 30;
}
/**
* @param mixed $modules
* @return array<int, string>
*/
private function normalizeModules(mixed $modules): array
{
$allowed = ['artworks', 'cards', 'collections', 'stories'];
return collect(is_array($modules) ? $modules : [])
->map(fn ($module): string => (string) $module)
->filter(fn (string $module): bool => in_array($module, $allowed, true))
->unique()
->values()
->all();
}
/**
* @param mixed $featuredContent
* @return array<string, int>
*/
private function normalizeFeaturedContent(mixed $featuredContent): array
{
$allowed = ['artworks', 'cards', 'collections', 'stories'];
return collect(is_array($featuredContent) ? $featuredContent : [])
->mapWithKeys(function ($id, $module) use ($allowed): array {
$moduleKey = (string) $module;
$normalizedId = (int) $id;
if (! in_array($moduleKey, $allowed, true) || $normalizedId < 1) {
return [];
}
return [$moduleKey => $normalizedId];
})
->all();
}
private function normalizeDraftBehavior(string $value): string
{
return in_array($value, ['resume-last', 'open-drafts', 'focus-published'], true)
? $value
: 'resume-last';
}
private function normalizeLandingPage(string $value): string
{
return in_array($value, ['overview', 'content', 'drafts', 'scheduled', 'analytics', 'activity', 'calendar', 'inbox', 'search', 'growth', 'challenges', 'preferences'], true)
? $value
: 'overview';
}
private function normalizeDensity(string $value): string
{
return in_array($value, ['compact', 'comfortable'], true)
? $value
: 'comfortable';
}
private function normalizeTimezone(mixed $value): ?string
{
if (! is_string($value)) {
return null;
}
$value = trim($value);
return $value !== '' ? $value : null;
}
private function normalizeActivityLastReadAt(mixed $value): ?string
{
if (! is_string($value)) {
return null;
}
$value = trim($value);
return $value !== '' ? $value : null;
}
/**
* @param mixed $value
* @return array<string, bool>
*/
private function normalizeWidgetVisibility(mixed $value): array
{
$defaults = collect($this->allowedWidgets())->mapWithKeys(fn (string $widget): array => [$widget => true]);
return $defaults->merge(
collect(is_array($value) ? $value : [])
->filter(fn ($enabled, $widget): bool => in_array((string) $widget, $this->allowedWidgets(), true))
->map(fn ($enabled): bool => (bool) $enabled)
)->all();
}
/**
* @param mixed $value
* @return array<int, string>
*/
private function normalizeWidgetOrder(mixed $value): array
{
$requested = collect(is_array($value) ? $value : [])
->map(fn ($widget): string => (string) $widget)
->filter(fn (string $widget): bool => in_array($widget, $this->allowedWidgets(), true))
->unique()
->values();
return $requested
->concat(collect($this->allowedWidgets())->reject(fn (string $widget): bool => $requested->contains($widget)))
->values()
->all();
}
/**
* @return array<int, string>
*/
private function allowedWidgets(): array
{
return [
'quick_stats',
'continue_working',
'scheduled_items',
'recent_activity',
'top_performers',
'draft_reminders',
'module_summaries',
'growth_hints',
'active_challenges',
'creator_health',
'featured_status',
'comments_snapshot',
'stale_drafts',
];
}
}