Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BCrwHHnGCB5XH968Nxokqw
110 lines
4.6 KiB
PHP
110 lines
4.6 KiB
PHP
<?php
|
||
|
||
namespace App\Http\Controllers;
|
||
|
||
use App\Enums\EventState;
|
||
use App\Http\Controllers\Api\EventsController as Presenter;
|
||
use App\Http\Controllers\Me\ChannelsController;
|
||
use App\Http\Controllers\Me\ProfileController;
|
||
use App\Models\Event;
|
||
use App\Models\User;
|
||
use App\Scheduling\DueResolver;
|
||
use Carbon\CarbonImmutable;
|
||
use Illuminate\Contracts\View\View;
|
||
use Illuminate\Http\Request;
|
||
|
||
class InboxController extends Controller
|
||
{
|
||
private const MONTHS = ['ЯНВ', 'ФЕВ', 'МАР', 'АПР', 'МАЙ', 'ИЮН', 'ИЮЛ', 'АВГ', 'СЕН', 'ОКТ', 'НОЯ', 'ДЕК'];
|
||
|
||
public function __construct(private readonly DueResolver $due) {}
|
||
|
||
public function __invoke(Request $request): View
|
||
{
|
||
/** @var User $user */
|
||
$user = $request->attributes->get('user');
|
||
$today = $user->now()->startOfDay();
|
||
|
||
$live = $user->events()->with('source')
|
||
->whereIn('state', EventState::nonTerminalValues())
|
||
->get()
|
||
->map(fn (Event $e) => $this->card($e, $user, $today))
|
||
->sortBy('sort')->values();
|
||
|
||
$groups = [];
|
||
foreach (['today' => ['今日', 'СЕГОДНЯ'], 'overdue' => ['遅延', 'ПРОСРОЧЕНО'], 'preparing' => ['近日', 'ГРЯДЁТ'], 'scheduled' => ['予定', 'ЗАПЛАНИРОВАНО']] as $state => [$jp, $ru]) {
|
||
$groups[] = ['state' => $state, 'jp' => $jp, 'ru' => $ru, 'events' => $live->where('state', $state)->values()->all()];
|
||
}
|
||
|
||
$archive = $user->events()->with('source')
|
||
->whereIn('state', [EventState::Done->value, EventState::Expired->value])
|
||
->orderByDesc('updated_at')->limit(100)->get()
|
||
->map(fn (Event $e) => [
|
||
'title' => $e->payload['title'],
|
||
'source' => self::sourceLabel($e->source->name),
|
||
'due' => Presenter::present($e)['due_at'],
|
||
'closed_as' => $e->state === EventState::Expired ? 'Истекло' : ($e->payload['done_label'] ?? 'Сделано'),
|
||
])->values()->all();
|
||
|
||
$profile = ProfileController::present($user);
|
||
|
||
return view('inbox', [
|
||
'profile' => $profile,
|
||
'title' => $profile['badge'] > 0 ? "({$profile['badge']}) Hado" : 'Hado',
|
||
'groups' => $groups,
|
||
'archive' => $archive,
|
||
'channels' => $user->channels()->orderBy('id')->get()->map(ChannelsController::present(...))->values()->all(),
|
||
'timezones' => \DateTimeZone::listIdentifiers(),
|
||
]);
|
||
}
|
||
|
||
/** @return array<string,mixed> */
|
||
private function card(Event $e, User $user, CarbonImmutable $today): array
|
||
{
|
||
$due = $this->due->resolve($e, $user);
|
||
$wholeDay = $due->moment->eq($due->day->addDay());
|
||
$diff = (int) round($today->diffInDays($due->day));
|
||
|
||
return [
|
||
'id' => $e->id,
|
||
'state' => $e->state->value,
|
||
'title' => $e->payload['title'],
|
||
'subtitle' => $e->payload['subtitle'] ?? null,
|
||
'link' => self::safeLink($e->payload['deep_link'] ?? null),
|
||
'done_label' => $e->payload['done_label'] ?? 'Сделано',
|
||
'can_remind' => $e->state !== EventState::Scheduled,
|
||
'source' => self::sourceLabel($e->source->name),
|
||
'due' => Presenter::present($e)['due_at'],
|
||
'due_month' => self::MONTHS[(int) $due->day->format('n') - 1],
|
||
'due_day' => $due->day->format('d'),
|
||
'due_time' => $wholeDay ? null : $due->moment->format('H:i'),
|
||
'rel' => self::relative($diff),
|
||
'sort' => $due->moment->getTimestamp(),
|
||
];
|
||
}
|
||
|
||
public static function sourceLabel(string $name): string
|
||
{
|
||
return $name === 'manual' ? 'вручную' : $name;
|
||
}
|
||
|
||
/** Пропускает только относительные пути и http(s)-ссылки; прочее (напр. javascript:) отбрасывает. */
|
||
public static function safeLink(?string $link): ?string
|
||
{
|
||
return $link !== null && preg_match('#^(/(?!/)|https?://)#i', $link) === 1 ? $link : null;
|
||
}
|
||
|
||
public static function relative(int $diff): string
|
||
{
|
||
$days = fn (int $n) => $n % 10 === 1 && $n % 100 !== 11 ? 'день' : ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 12 || $n % 100 > 14) ? 'дня' : 'дней');
|
||
|
||
return match (true) {
|
||
$diff === 0 => 'сегодня',
|
||
$diff === 1 => 'завтра',
|
||
$diff > 1 => "через {$diff} ".$days($diff),
|
||
$diff === -1 => 'вчера',
|
||
default => abs($diff).' '.$days(abs($diff)).' назад',
|
||
};
|
||
}
|
||
}
|