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

224 lines
8.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Studio;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Support\Collection;
final class CreatorStudioCalendarService
{
public function __construct(
private readonly CreatorStudioContentService $content,
private readonly CreatorStudioScheduledService $scheduled,
) {
}
public function build(User $user, array $filters = []): array
{
$view = $this->normalizeView((string) ($filters['view'] ?? 'month'));
$module = $this->normalizeModule((string) ($filters['module'] ?? 'all'));
$status = $this->normalizeStatus((string) ($filters['status'] ?? 'scheduled'));
$query = trim((string) ($filters['q'] ?? ''));
$focusDate = $this->normalizeFocusDate((string) ($filters['focus_date'] ?? ''));
$scheduledItems = $this->scheduledItems($user, $module, $query);
$unscheduledItems = $this->unscheduledItems($user, $module, $query);
return [
'filters' => [
'view' => $view,
'module' => $module,
'status' => $status,
'q' => $query,
'focus_date' => $focusDate->toDateString(),
],
'view_options' => [
['value' => 'month', 'label' => 'Month'],
['value' => 'week', 'label' => 'Week'],
['value' => 'agenda', 'label' => 'Agenda'],
],
'status_options' => [
['value' => 'scheduled', 'label' => 'Scheduled only'],
['value' => 'unscheduled', 'label' => 'Unscheduled queue'],
['value' => 'all', 'label' => 'Everything'],
],
'module_options' => array_merge([
['value' => 'all', 'label' => 'All content'],
], collect($this->content->moduleSummaries($user))->map(fn (array $summary): array => [
'value' => $summary['key'],
'label' => $summary['label'],
])->all()),
'summary' => $this->summary($scheduledItems, $unscheduledItems),
'month' => $this->monthGrid($scheduledItems, $focusDate),
'week' => $this->weekGrid($scheduledItems, $focusDate),
'agenda' => $this->agenda($scheduledItems),
'scheduled_items' => $status === 'unscheduled' ? [] : $scheduledItems->take(18)->values()->all(),
'unscheduled_items' => $status === 'scheduled' ? [] : $unscheduledItems->take(12)->values()->all(),
'gaps' => $this->gaps($scheduledItems, $focusDate),
];
}
private function scheduledItems(User $user, string $module, string $query): Collection
{
$items = $module === 'all'
? collect($this->content->providers())->flatMap(fn ($provider) => $provider->scheduledItems($user, 320))
: ($this->content->provider($module)?->scheduledItems($user, 320) ?? collect());
if ($query !== '') {
$needle = mb_strtolower($query);
$items = $items->filter(fn (array $item): bool => str_contains(mb_strtolower((string) ($item['title'] ?? '')), $needle));
}
return $items
->sortBy(fn (array $item): int => strtotime((string) ($item['scheduled_at'] ?? $item['published_at'] ?? '')) ?: PHP_INT_MAX)
->values();
}
private function unscheduledItems(User $user, string $module, string $query): Collection
{
$items = $module === 'all'
? collect($this->content->providers())->flatMap(fn ($provider) => $provider->items($user, 'all', 240))
: ($this->content->provider($module)?->items($user, 'all', 240) ?? collect());
return $items
->filter(function (array $item) use ($query): bool {
if (filled($item['scheduled_at'] ?? null)) {
return false;
}
if (in_array((string) ($item['status'] ?? ''), ['archived', 'hidden', 'rejected'], true)) {
return false;
}
if ($query === '') {
return true;
}
return str_contains(mb_strtolower((string) ($item['title'] ?? '')), mb_strtolower($query));
})
->sortByDesc(fn (array $item): int => strtotime((string) ($item['updated_at'] ?? $item['created_at'] ?? '')) ?: 0)
->values();
}
private function summary(Collection $scheduledItems, Collection $unscheduledItems): array
{
$days = $scheduledItems
->groupBy(fn (array $item): string => date('Y-m-d', strtotime((string) ($item['scheduled_at'] ?? $item['published_at'] ?? now()->toIso8601String()))))
->map(fn (Collection $items): int => $items->count());
return [
'scheduled_total' => $scheduledItems->count(),
'unscheduled_total' => $unscheduledItems->count(),
'overloaded_days' => $days->filter(fn (int $count): bool => $count >= 3)->count(),
'next_publish_at' => $scheduledItems->first()['scheduled_at'] ?? null,
];
}
private function monthGrid(Collection $scheduledItems, Carbon $focusDate): array
{
$start = $focusDate->copy()->startOfMonth()->startOfWeek();
$end = $focusDate->copy()->endOfMonth()->endOfWeek();
$days = [];
for ($date = $start->copy(); $date->lte($end); $date->addDay()) {
$key = $date->toDateString();
$items = $scheduledItems->filter(fn (array $item): bool => str_starts_with((string) ($item['scheduled_at'] ?? ''), $key))->values();
$days[] = [
'date' => $key,
'day' => $date->day,
'is_current_month' => $date->month === $focusDate->month,
'count' => $items->count(),
'items' => $items->take(3)->all(),
];
}
return [
'label' => $focusDate->format('F Y'),
'days' => $days,
];
}
private function weekGrid(Collection $scheduledItems, Carbon $focusDate): array
{
$start = $focusDate->copy()->startOfWeek();
return [
'label' => $start->format('M j') . ' - ' . $start->copy()->endOfWeek()->format('M j'),
'days' => collect(range(0, 6))->map(function (int $offset) use ($start, $scheduledItems): array {
$date = $start->copy()->addDays($offset);
$key = $date->toDateString();
$items = $scheduledItems->filter(fn (array $item): bool => str_starts_with((string) ($item['scheduled_at'] ?? ''), $key))->values();
return [
'date' => $key,
'label' => $date->format('D j'),
'items' => $items->all(),
];
})->all(),
];
}
private function agenda(Collection $scheduledItems): array
{
return $scheduledItems
->groupBy(fn (array $item): string => date('Y-m-d', strtotime((string) ($item['scheduled_at'] ?? $item['published_at'] ?? now()->toIso8601String()))))
->map(fn (Collection $items, string $date): array => [
'date' => $date,
'label' => Carbon::parse($date)->format('M j'),
'count' => $items->count(),
'items' => $items->values()->all(),
])
->values()
->all();
}
private function gaps(Collection $scheduledItems, Carbon $focusDate): array
{
return collect(range(0, 13))
->map(function (int $offset) use ($focusDate, $scheduledItems): ?array {
$date = $focusDate->copy()->startOfDay()->addDays($offset);
$key = $date->toDateString();
$count = $scheduledItems->filter(fn (array $item): bool => str_starts_with((string) ($item['scheduled_at'] ?? ''), $key))->count();
if ($count > 0) {
return null;
}
return [
'date' => $key,
'label' => $date->format('D, M j'),
];
})
->filter()
->take(6)
->values()
->all();
}
private function normalizeView(string $view): string
{
return in_array($view, ['month', 'week', 'agenda'], true) ? $view : 'month';
}
private function normalizeStatus(string $status): string
{
return in_array($status, ['scheduled', 'unscheduled', 'all'], true) ? $status : 'scheduled';
}
private function normalizeModule(string $module): string
{
return in_array($module, ['all', 'artworks', 'cards', 'collections', 'stories'], true) ? $module : 'all';
}
private function normalizeFocusDate(string $value): Carbon
{
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value) === 1) {
return Carbon::parse($value);
}
return now();
}
}