From 218515c76c8757406c3855284803520397065041 Mon Sep 17 00:00:00 2001 From: Sven Date: Fri, 7 Feb 2025 02:53:05 +0100 Subject: [PATCH] Fixes #93 --- .../views/components/incident-item.blade.php | 68 ++++++++++++++++++ resources/views/components/incident.blade.php | 66 ++---------------- .../views/components/schedule-item.blade.php | 69 +++++++++++++++++++ .../schedule-update-status.blade.php | 12 ++++ .../views/components/timeline-badge.blade.php | 15 ++++ src/Collections/TimelineCollection.php | 25 +++++++ src/Contracts/Support/Sequencable.php | 10 +++ src/Models/Incident.php | 10 ++- src/Models/Schedule.php | 41 ++++++++++- src/View/Components/IncidentTimeline.php | 61 ++++++++++++---- src/View/Components/ScheduleUpdateStatus.php | 31 +++++++++ src/View/Components/TimelineBadge.php | 24 +++++++ 12 files changed, 357 insertions(+), 75 deletions(-) create mode 100644 resources/views/components/incident-item.blade.php create mode 100644 resources/views/components/schedule-item.blade.php create mode 100644 resources/views/components/schedule-update-status.blade.php create mode 100644 resources/views/components/timeline-badge.blade.php create mode 100644 src/Collections/TimelineCollection.php create mode 100644 src/Contracts/Support/Sequencable.php create mode 100644 src/View/Components/ScheduleUpdateStatus.php create mode 100644 src/View/Components/TimelineBadge.php diff --git a/resources/views/components/incident-item.blade.php b/resources/views/components/incident-item.blade.php new file mode 100644 index 00000000..804e488e --- /dev/null +++ b/resources/views/components/incident-item.blade.php @@ -0,0 +1,68 @@ +@use('Cachet\Enums\IncidentStatusEnum') +@props([ + 'incident', +]) + + +
+
$incident->updates->isNotEmpty(), + 'rounded-lg' => $incident->updates->isEmpty(), + ])> + @if ($incident->components()->exists()) +
+ {{ $incident->components->pluck('name')->join(', ', ' and ') }} +
+ @endif +
+
+ +
+

+ {{ $incident->name}} +

+ @auth + + + + @endauth +
+ + {{ $incident->timestamp->diffForHumans() }} — + +
+
+ +
+
+
+ +
+
+
+
+
+
+
+ @foreach ($incident->updates as $update) +
+ +

{{ $update->status->getLabel() }}

+ + {{ $update->created_at->diffForHumans() }} — + +
{!! $update->formattedMessage() !!}
+
+ @endforeach +
+ + + + {{ $incident->timestamp->diffForHumans() }} — + +
{!! $incident->formattedMessage() !!}
+
+
+
+
diff --git a/resources/views/components/incident.blade.php b/resources/views/components/incident.blade.php index 75eaa3c5..b1fcc42e 100644 --- a/resources/views/components/incident.blade.php +++ b/resources/views/components/incident.blade.php @@ -8,67 +8,11 @@

@forelse($incidents as $incident) -
-
$incident->updates->isNotEmpty(), - 'rounded-lg' => $incident->updates->isEmpty(), - ])> - @if ($incident->components()->exists()) -
- {{ $incident->components->pluck('name')->join(', ', ' and ') }} -
- @endif -
-
-
-

- {{ $incident->name}} -

- @auth - - - - @endauth -
- - {{ $incident->timestamp->diffForHumans() }} — - -
-
- -
-
-
- -
-
-
-
-
-
-
- @foreach ($incident->updates as $update) -
- -

{{ $update->status->getLabel() }}

- - {{ $update->created_at->diffForHumans() }} — - -
{!! $update->formattedMessage() !!}
-
- @endforeach -
- - - - {{ $incident->timestamp->diffForHumans() }} — - -
{!! $incident->formattedMessage() !!}
-
-
-
-
+ @if($incident instanceof \Cachet\Models\Incident) + + @elseif($incident instanceof \Cachet\Models\Schedule) + + @endif @empty
diff --git a/resources/views/components/schedule-item.blade.php b/resources/views/components/schedule-item.blade.php new file mode 100644 index 00000000..45edd166 --- /dev/null +++ b/resources/views/components/schedule-item.blade.php @@ -0,0 +1,69 @@ +@use('Cachet\Enums\ScheduleStatusEnum') +@props([ + 'schedule', +]) + + +
+
$schedule->updates->isNotEmpty(), + 'rounded-lg' => $schedule->updates->isEmpty(), + ])> + @if ($schedule->components()->exists()) +
+ {{ $schedule->components->pluck('name')->join(', ', ' and ') }} +
+ @endif +
+
+ +
+

+ {{ $schedule->name}} +

+ @auth + + + + @endauth +
+ + {{ $schedule->timestamp->diffForHumans() }} — + +
+
+ + +
+
+
+ +
+
+
+
+
+
+
+ @foreach ($schedule->updates as $update) +
+ +

{{ $update->status->getLabel() }}

+ + {{ $update->created_at->diffForHumans() }} — + +
{!! $update->formattedMessage() !!}
+
+ @endforeach +
+ + + + {{ $schedule->timestamp->diffForHumans() }} — + +
{!! $schedule->formattedMessage() !!}
+
+
+
+
diff --git a/resources/views/components/schedule-update-status.blade.php b/resources/views/components/schedule-update-status.blade.php new file mode 100644 index 00000000..64590b46 --- /dev/null +++ b/resources/views/components/schedule-update-status.blade.php @@ -0,0 +1,12 @@ +
style([ + Illuminate\Support\Arr::toCssStyles([ + \Filament\Support\get_color_css_variables( + $color, + shades: [200, 400, 700, 900], + ), + ]), +])->merge(['title' => $title]) }}> +
+ @svg($icon, 'size-5') +
+
diff --git a/resources/views/components/timeline-badge.blade.php b/resources/views/components/timeline-badge.blade.php new file mode 100644 index 00000000..2322b310 --- /dev/null +++ b/resources/views/components/timeline-badge.blade.php @@ -0,0 +1,15 @@ +
style([ + Illuminate\Support\Arr::toCssStyles([ + \Filament\Support\get_color_css_variables( + $color, + shades: [200, 400, 700, 900], + ), + ]), +])->merge(['title' => $label]) }}> +
+ @svg($icon, 'size-5') +
+
+{{-- --}} +{{-- {{ $label }}--}} +{{-- --}} diff --git a/src/Collections/TimelineCollection.php b/src/Collections/TimelineCollection.php new file mode 100644 index 00000000..fcac9dc9 --- /dev/null +++ b/src/Collections/TimelineCollection.php @@ -0,0 +1,25 @@ +sortByDesc(fn (Sequencable $item) => $item->getSequenceTimestamp()) + ->groupBy(fn (Sequencable $item) => $item->getSequenceTimestamp()->toDateString()) + ->union( + // Back-fill any missing dates... + collect($endDate->toPeriod($startDate)) + ->keyBy(fn ($period) => $period->toDateString()) + ->map(fn ($period) => collect()) + ) + ->when($onlyDisruptedDays, fn ($collection) => $collection->filter(fn ($items) => $items->isNotEmpty())) + ->sortKeysDesc(); + } +} diff --git a/src/Contracts/Support/Sequencable.php b/src/Contracts/Support/Sequencable.php new file mode 100644 index 00000000..6b9cf061 --- /dev/null +++ b/src/Contracts/Support/Sequencable.php @@ -0,0 +1,10 @@ +|static unresolved() * @method static Builder|static stickied() */ -class Incident extends Model +class Incident extends Model implements Sequencable { /** @use HasFactory */ use HasFactory; @@ -181,6 +183,12 @@ protected function timestamp(): Attribute ); } + public function getSequenceTimestamp(): CarbonInterface + { + return $this->timestamp; + } + + /** * Determine the latest status of the incident. * diff --git a/src/Models/Schedule.php b/src/Models/Schedule.php index ee759013..2978ee9e 100644 --- a/src/Models/Schedule.php +++ b/src/Models/Schedule.php @@ -2,8 +2,12 @@ namespace Cachet\Models; +use Cachet\Contracts\Support\Sequencable; use Cachet\Database\Factories\ScheduleFactory; use Cachet\Enums\ScheduleStatusEnum; +use Cachet\Filament\Resources\ScheduleResource; +use Cachet\QueryBuilders\ScheduleBuilder; +use Carbon\CarbonInterface; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\Factory; @@ -35,7 +39,7 @@ * @method static Builder|static inTheFuture() * @method static Builder|static inThePast() */ -class Schedule extends Model +class Schedule extends Model implements Sequencable { /** @use HasFactory */ use HasFactory; @@ -107,6 +111,41 @@ public function formattedMessage(): string return Str::of($this->message)->markdown(); } + /** + * Determine the latest status of the incident. + * + * @return Attribute + */ + protected function latestStatus(): Attribute + { + return Attribute::make( + get: function ($value) { + return $this->updates()->latest()->first()->status ?? $this->status; + } + ); + } + + /** + * @return Attribute + */ + protected function timestamp(): Attribute + { + return Attribute::make( + get: fn () => $this->completed_at ?? $this->scheduled_at + ); + } + + public function getSequenceTimestamp(): CarbonInterface + { + return $this->timestamp; + } + + public function filamentDashboardEditUrl(): string + { + return ScheduleResource::getUrl(name: 'edit', parameters: ['record' => $this->id]); + } + + /** * Scope schedules that are incomplete. * diff --git a/src/View/Components/IncidentTimeline.php b/src/View/Components/IncidentTimeline.php index 9cd69961..e1ad9b8d 100644 --- a/src/View/Components/IncidentTimeline.php +++ b/src/View/Components/IncidentTimeline.php @@ -2,7 +2,9 @@ namespace Cachet\View\Components; +use Cachet\Collections\TimelineCollection; use Cachet\Models\Incident; +use Cachet\Models\Schedule; use Cachet\Settings\AppSettings; use Illuminate\Contracts\View\View; use Illuminate\Database\Eloquent\Builder; @@ -26,7 +28,7 @@ public function render(): View $endDate = $startDate->clone()->subDays($incidentDays); return view('cachet::components.incident-timeline', [ - 'incidents' => $this->incidents( + 'incidents' => $this->timeline( $startDate, $endDate, $this->appSettings->only_disrupted_days @@ -41,6 +43,14 @@ public function render(): View ]); } + + private function timeline(Carbon $startDate, Carbon $endDate, bool $onlyDisruptedDays = false): Collection + { + return TimelineCollection::make($this->incidents($startDate, $endDate, $onlyDisruptedDays)) + ->merge($this->schedules($startDate, $endDate, $onlyDisruptedDays)) + ->getSorted($startDate, $endDate, $onlyDisruptedDays); + } + /** * Fetch the incidents that occurred between the given start and end date. * Incidents will be grouped by days. @@ -81,16 +91,43 @@ private function incidents(Carbon $startDate, Carbon $endDate, bool $onlyDisrupt }); }); }) - ->get() - ->sortByDesc(fn (Incident $incident) => $incident->timestamp) - ->groupBy(fn (Incident $incident) => $incident->timestamp->toDateString()) - ->union( - // Back-fill any missing dates... - collect($endDate->toPeriod($startDate)) - ->keyBy(fn ($period) => $period->toDateString()) - ->map(fn ($period) => collect()) - ) - ->when($onlyDisruptedDays, fn ($collection) => $collection->filter(fn ($incidents) => $incidents->isNotEmpty())) - ->sortKeysDesc(); + ->get(); + } + private function schedules(Carbon $startDate, Carbon $endDate, bool $onlyDisruptedDays = false): Collection + { + return Schedule::query() + ->with([ + 'components', + 'updates' => fn ($query) => $query->orderByDesc('created_at'), + ]) + ->when($this->appSettings->recent_incidents_only, function ($query) { + $query->where(function ($query) { + $query->whereDate( + 'completed_at', + '>', + Carbon::now()->subDays($this->appSettings->recent_incidents_days)->format('Y-m-d') + )->orWhere(function ($query) { + $query->whereNull('completed_at')->whereDate( + 'scheduled_at', + '>', + Carbon::now()->subDays($this->appSettings->recent_incidents_days)->format('Y-m-d') + ); + }); + }); + }) + ->when(! $this->appSettings->recent_incidents_only, function ($query) use ($endDate, $startDate) { + $query->where(function (Builder $query) use ($endDate, $startDate) { + $query->whereBetween('completed_at', [ + $endDate->startOfDay()->toDateTimeString(), + $startDate->endofDay()->toDateTimeString(), + ])->orWhere(function (Builder $query) use ($endDate, $startDate) { + $query->whereNull('completed_at')->whereBetween('scheduled_at', [ + $endDate->startOfDay()->toDateTimeString(), + $startDate->endofDay()->toDateTimeString(), + ]); + }); + }); + }) + ->get(); } } diff --git a/src/View/Components/ScheduleUpdateStatus.php b/src/View/Components/ScheduleUpdateStatus.php new file mode 100644 index 00000000..09462190 --- /dev/null +++ b/src/View/Components/ScheduleUpdateStatus.php @@ -0,0 +1,31 @@ + $this->status?->getLabel(), + 'color' => $this->status?->getColor() ?? 'gray', + 'icon' => $this->status?->getIcon(), + ]); + } +} diff --git a/src/View/Components/TimelineBadge.php b/src/View/Components/TimelineBadge.php new file mode 100644 index 00000000..bbae1631 --- /dev/null +++ b/src/View/Components/TimelineBadge.php @@ -0,0 +1,24 @@ + $this->color ?? 'gray', + 'label' => $this->label, + 'icon' => $this->icon, + ]); + } +}