*/ public array $hostIds = []; public function saveDraft(): void { $this->authorize('maintenance.manage'); $this->persist('draft'); } public function publish(): void { $this->authorize('maintenance.manage'); $window = $this->persist('scheduled'); if ($window !== null) { $this->sendAnnouncements($window); } } private function persist(string $state): ?MaintenanceWindow { $data = $this->validate(); $starts = Carbon::parse($data['startsAt']); $ends = Carbon::parse($data['endsAt']); if ($ends->lessThanOrEqualTo($starts)) { $this->addError('endsAt', __('maintenance.end_after_start')); return null; } if ($state === 'scheduled') { if (empty($this->hostIds)) { $this->addError('hostIds', __('maintenance.need_host')); return null; } if ($ends->isPast()) { $this->addError('endsAt', __('maintenance.end_future')); return null; } } $window = MaintenanceWindow::create([ 'title' => $data['title'], 'public_description' => $data['publicDescription'] ?: null, 'internal_notes' => $data['internalNotes'] ?: null, 'starts_at' => $starts, 'ends_at' => $ends, 'state' => $state, 'created_by' => auth()->id(), 'published_at' => $state === 'scheduled' ? now() : null, ]); $window->hosts()->sync(array_map('intval', $this->hostIds)); $this->reset('title', 'publicDescription', 'internalNotes', 'startsAt', 'endsAt', 'hostIds'); $this->dispatch('notify', message: __($state === 'scheduled' ? 'maintenance.published' : 'maintenance.draft_saved')); return $window; } public function publishExisting(string $uuid): void { $this->authorize('maintenance.manage'); $window = MaintenanceWindow::query()->where('uuid', $uuid)->first(); if ($window === null || $window->state !== 'draft') { return; } if ($window->hosts()->count() === 0 || $window->ends_at->isPast()) { $this->dispatch('notify', message: __('maintenance.need_host')); return; } $window->update(['state' => 'scheduled', 'published_at' => now()]); $this->sendAnnouncements($window); $this->dispatch('notify', message: __('maintenance.published')); } public function cancel(string $uuid): void { $this->authorize('maintenance.manage'); $window = MaintenanceWindow::query()->where('uuid', $uuid)->first(); if ($window === null || $window->state === 'cancelled') { return; } $window->update(['state' => 'cancelled', 'cancelled_at' => now()]); $this->dispatch('notify', message: __('maintenance.cancelled')); } /** Send the announcement to each affected customer exactly once (ledger-guarded). */ private function sendAnnouncements(MaintenanceWindow $window): void { foreach ($window->affectedCustomers() as $customer) { $delivery = MaintenanceNotification::query()->firstOrCreate( ['maintenance_window_id' => $window->id, 'customer_id' => $customer->id, 'event' => 'announcement'], ['email' => $customer->email], ); if ($delivery->wasRecentlyCreated) { Mail::to($customer->email) ->locale($customer->locale ?: config('app.locale')) ->queue(new MaintenanceAnnouncementMail($window, $customer)); $delivery->update(['sent_at' => now()]); } } } public function render() { $windows = MaintenanceWindow::query() ->withCount('hosts') ->orderByDesc('starts_at') ->get() ->map(fn (MaintenanceWindow $w) => [ 'uuid' => $w->uuid, 'title' => $w->title, 'starts_at' => $w->starts_at, 'ends_at' => $w->ends_at, 'state' => $w->derivedState(), 'hosts' => $w->hosts_count, 'affected' => $w->affectedCustomers()->count(), 'is_draft' => $w->state === 'draft', 'cancellable' => in_array($w->derivedState(), ['draft', 'upcoming', 'active'], true), ]); return view('livewire.admin.maintenance', [ 'windows' => $windows, 'datacenters' => Datacenter::query()->orderBy('name')->get(), 'hosts' => Host::query()->orderBy('datacenter')->orderBy('name')->get(), ]); } }