setupTenantConnection(); } public function mount() { if (auth()->user()?->level != env('LEVEL_ADMIN', 0)) { return redirect()->to('/dashboard'); } $this->categories = []; $this->getCategories(\App\Models\Category::select('id', 'name')->where('parent_id', null)->orderBy('name')->get(), 0); $this->courses = []; $this->getCourses(\App\Models\Course::select('id', 'name')->where('parent_id', null)->orderBy('name', 'ASC')->get(), 0); $this->schedule_at = now($this->timezone)->addHour()->format('Y-m-d\TH:i'); } public function render() { if (!$this->showForm) { $this->records = EmailMessage::withCount(['attachments', 'recipients']) ->orderBy('created_at', 'desc') ->get(); } else { $this->categories = []; $this->getCategories(\App\Models\Category::select('id', 'name')->where('parent_id', null)->orderBy('name')->get(), 0); $this->courses = []; $this->getCourses(\App\Models\Course::select('id', 'name')->where('parent_id', null)->orderBy('name', 'ASC')->get(), 0); } return view('livewire.email_comunications'); } protected function baseRules(): array { return [ 'subject' => 'required|string|max:255', 'content_html' => 'required|string', 'recipients' => 'required|array|min:1', 'recipients.*.email_address' => 'required|email', 'newAttachments' => 'nullable', 'newAttachments.*' => 'file|max:20480', ]; } protected function validateDraft(): void { $rules = []; // $rules = $this->baseRules(); $rules['subject'] = 'required|string|max:255'; $this->validate($rules); } protected function validateSend(): void { $this->validate($this->baseRules()); } protected function validateSchedule(): void { $rules = $this->baseRules(); $rules['schedule_at'] = 'required|date|after:now'; $this->validate($rules); } public function add() { $this->reset(['messageId', 'subject', 'content_html', 'recipients', 'newAttachments', 'mode', 'schedule_at']); $this->mode = 'now'; $this->schedule_at = now($this->timezone)->addHour()->format('Y-m-d\TH:i'); $this->existingAttachments = []; $this->showForm = true; $this->dispatchBrowserEvent('load-editor', [ 'html' => $this->content_html ?? '', 'locked' => $this->locked, ]); $this->dispatchBrowserEvent('init-recipients-table', [ 'selected' => collect($this->recipients)->pluck('member_id')->filter()->values()->all(), ]); } public function edit($id) { try { $msg = EmailMessage::with(['recipients', 'attachments'])->findOrFail($id); $this->messageId = $msg->id; $this->subject = $msg->subject; $this->content_html = $msg->content_html; $this->recipients = $msg->recipients->map(fn($r) => [ 'member_id' => $r->member_id, 'email_address' => $r->email_address, 'first_name' => optional($r->member)->first_name, 'last_name' => optional($r->member)->last_name, ])->toArray(); usort($this->recipients, function($a, $b) { $last_name = strcmp($a['last_name'], $b['last_name']); $first_name = strcmp($a['first_name'], $b['first_name']); return $last_name == 0 ? $first_name : $last_name; }); $this->mode = $msg->status === 'scheduled' ? 'schedule' : 'now'; $this->schedule_at = optional($msg->schedule_at)?->setTimezone($this->timezone)?->format('Y-m-d\TH:i'); $this->existingAttachments = $msg->attachments->map(fn($a) => [ 'id' => $a->id, 'name' => $a->name ?: basename($a->path), 'size' => $a->size_human, 'url' => $a->public_url, 'img' => $a->is_image, ])->toArray(); $this->showForm = true; $this->locked = $msg->isLocked(); $this->dispatchBrowserEvent('load-editor', [ 'html' => $this->content_html ?? '', 'locked' => $this->locked, ]); $this->dispatchBrowserEvent('init-recipients-table', [ 'selected' => collect($this->recipients)->pluck('member_id')->filter()->values()->all(), ]); } catch (\Throwable $ex) { $this->error = 'Errore (' . $ex->getMessage() . ')'; } } public function duplicate($id, $withRecipients = true) { try { $copy = EmailMessage::with(['recipients', 'attachments'])->findOrFail($id)->duplicate($withRecipients); $this->edit($copy->id); $this->success = 'Bozza duplicata'; } catch (\Throwable $ex) { $this->error = 'Errore (' . $ex->getMessage() . ')'; } } public function saveDraft($html = null) { if ($html !== null) $this->content_html = $html; $this->validateDraft(); DB::transaction(function () { $msg = $this->upsertMessage(status: 'draft', scheduleAt: null); $this->upsertRecipients($msg); $this->upsertAttachments($msg); $this->messageId = $msg->id; $this->locked = $msg->isLocked(); $this->refreshAttachments($msg); }); $this->success = 'Bozza salvata'; $this->dispatchBrowserEvent('scroll-top'); $this->dispatchBrowserEvent('load-editor', [ 'html' => $this->content_html ?? '', 'locked' => $this->locked, ]); } public function sendNow($html = null) { if ($html !== null) $this->content_html = $html; $this->validateSend(); if ($this->messageId) { $existing = EmailMessage::findOrFail($this->messageId); if ($existing->isLocked()) { $this->error = 'Questa email è già in invio o inviata e non può essere modificata.'; return; } } DB::transaction(function () { $msg = $this->upsertMessage(status: 'processing', scheduleAt: null); $this->upsertRecipients($msg, true); $this->upsertAttachments($msg, true); $this->messageId = $msg->id; $this->locked = true; $this->refreshAttachments($msg); }); dispatch(new \App\Jobs\SendEmailMessage($this->messageId)); $this->success = 'Invio avviato'; $this->dispatchBrowserEvent('scroll-top'); $this->dispatchBrowserEvent('load-editor', [ 'html' => $this->content_html ?? '', 'locked' => $this->locked, ]); } public function scheduleMessage($html = null) { if ($html !== null) $this->content_html = $html; $this->validateSchedule(); if ($this->messageId) { $existing = EmailMessage::findOrFail($this->messageId); if ($existing->isLocked()) { $this->error = 'Questa email è già in invio o inviata e non può essere modificata.'; return; } } $scheduledAt = \Carbon\Carbon::parse($this->schedule_at, $this->timezone)->setTimezone('UTC'); DB::transaction(function () use ($scheduledAt) { $msg = $this->upsertMessage(status: 'scheduled', scheduleAt: $scheduledAt); $this->upsertRecipients($msg, true); $this->upsertAttachments($msg, true); $this->messageId = $msg->id; $this->locked = $msg->isLocked(); $this->refreshAttachments($msg); }); $this->success = 'Email programmata'; $this->dispatchBrowserEvent('scroll-top'); $this->dispatchBrowserEvent('load-editor', [ 'html' => $this->content_html ?? '', 'locked' => $this->locked, ]); } protected function upsertMessage(string $status, $scheduleAt): EmailMessage { return EmailMessage::updateOrCreate( ['id' => $this->messageId], [ 'subject' => $this->subject, 'content_html' => $this->content_html, 'status' => $status, 'schedule_at' => $scheduleAt, 'created_by' => auth()->id(), ] ); } protected function upsertRecipients(EmailMessage $msg, bool $force = false): void { if (!$force && $msg->isLocked()) return; $msg->recipients()->delete(); $rows = collect($this->recipients)->map(fn($r) => [ 'email_message_id' => $msg->id, 'member_id' => $r['member_id'] ?? null, 'email_address' => $r['email_address'], 'status' => 'pending', 'created_at' => now(), 'updated_at' => now(), ])->values()->all(); if ($rows) \App\Models\EmailMessageRecipient::insert($rows); } protected function upsertAttachments(EmailMessage $msg, bool $force = false): void { if (!$force && $msg->isLocked()) return; $files = is_array($this->newAttachments) ? $this->newAttachments : [$this->newAttachments]; foreach ($files as $upload) { if (!$upload) continue; $path = $upload->store('emails/' . \Illuminate\Support\Str::uuid(), 'public'); $msg->attachments()->create([ 'disk' => 'public', 'path' => $path, 'name' => $upload->getClientOriginalName(), 'size' => $upload->getSize(), ]); } } public function cancel() { $this->showForm = false; $this->reset(['messageId', 'subject', 'content_html', 'recipients', 'newAttachments', 'mode', 'schedule_at']); $this->mode = 'now'; $this->schedule_at = now($this->timezone)->addHour()->format('Y-m-d\TH:i'); $this->dispatchBrowserEvent('init-archive-table'); } public function getCategories($records, $indentation) { foreach ($records as $record) { $this->categories[] = array('id' => $record->id, 'name' => $record->getTree()); if (count($record->childs)) $this->getCategories($record->childs, $indentation + 1); } } public function getCourses($records, $indentation) { /** @var \App\Models\Course $record */ foreach ($records as $record) { $this->courses[] = array('id' => $record->id, 'name' => $record->getTree()); if (count($record->childs)) $this->getCourses($record->childs, $indentation + 1); } } public function toggleRecipient($id) { $id = (int)$id; $idx = collect($this->recipients)->search(fn($r) => (int)($r['member_id'] ?? 0) === $id); if ($idx !== false) { array_splice($this->recipients, $idx, 1); return; } $m = Member::select('id', 'email', 'first_name', 'last_name')->find($id); if (!$m || empty($m->email)) return; $this->recipients[] = [ 'member_id' => $m->id, 'email_address' => $m->email, 'first_name' => $m->first_name, 'last_name' => $m->last_name, ]; usort($this->recipients, function($a, $b) { $last_name = strcmp($a['last_name'], $b['last_name']); $first_name = strcmp($a['first_name'], $b['first_name']); return $last_name == 0 ? $first_name : $last_name; }); } public function removeNewAttachment(int $index): void { if ($this->locked) return; if (is_array($this->newAttachments) && array_key_exists($index, $this->newAttachments)) { array_splice($this->newAttachments, $index, 1); } } public function removeExistingAttachment(int $id): void { if ($this->locked || !$this->messageId) return; $att = \App\Models\EmailMessageAttachment::find($id); if (!$att || $att->email_message_id !== $this->messageId) return; try { if ($att->disk && $att->path) { $disk = Storage::disk($att->disk); $dir = dirname($att->path); $disk->delete($att->path); if (empty($disk->files($dir)) && empty($disk->directories($dir))) { $disk->deleteDirectory($dir); } } } catch (\Throwable $e) { } $att->delete(); $this->existingAttachments = array_values(array_filter( $this->existingAttachments, fn($a) => (int)$a['id'] !== (int)$id )); } protected function refreshAttachments(EmailMessage $msg): void { $this->existingAttachments = $msg->attachments() ->get() ->map(fn($a) => [ 'id' => $a->id, 'name' => $a->name ?: basename($a->path), 'size' => $a->size_human, 'url' => $a->public_url, 'img' => $a->is_image, ])->toArray(); $this->newAttachments = []; } public function deleteMessage(int $id) { $msg = \App\Models\EmailMessage::with(['attachments'])->findOrFail($id); if (! in_array($msg->status, ['draft', 'failed'], true)) { return; } foreach ($msg->attachments as $a) { try { Storage::disk($a->disk ?? 'public')->delete($a->path); } catch (\Throwable $e) { } } $msg->delete(); $this->dispatchBrowserEvent('email-deleted', ['id' => $id]); } }