CluPilotCloud/app/Livewire/Admin/MailTemplates.php

150 lines
5.4 KiB
PHP

<?php
namespace App\Livewire\Admin;
use App\Models\MailTemplate;
use App\Services\Mail\MailTemplateRenderer;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Validate;
use Livewire\Component;
/**
* The answers an operator gives over and over, written once.
*
* Most customer questions are the same five questions, and typing the answer
* again every time is how two customers get told two different things about the
* same subject.
*
* A template is not sent from here and never sent by itself: it is inserted into
* the compose field on a customer's page, placeholders already filled from that
* customer's record, and the last two sentences are still the operator's to
* write. What goes out is what they saw and edited.
*
* Creating happens in the form on this page — R20 exempts a create form that IS
* the page. Editing happens in a modal (Admin\EditMailTemplate), because a
* textarea growing inside a table row is the row-height jump R20 exists to stop.
*/
#[Layout('layouts.admin')]
class MailTemplates extends Component
{
#[Validate('required|string|max:255')]
public string $name = '';
#[Validate('required|string|max:255')]
public string $subject = '';
#[Validate('required|string|max:5000')]
public string $body = '';
public function mount(): void
{
$this->authorize('customers.manage');
}
public function create(): void
{
$this->authorize('customers.manage');
$data = $this->validate();
MailTemplate::create([
'name' => trim($data['name']),
'subject' => trim($data['subject']),
'body' => $data['body'],
// At the end of the list, where a new one belongs until somebody
// decides otherwise.
'sort' => (int) MailTemplate::query()->max('sort') + 10,
'active' => true,
]);
$this->reset(['name', 'subject', 'body']);
$this->dispatch('notify', message: __('templates.created'));
}
/**
* Retire one, or bring it back.
*
* Not deleted: an answer that is no longer given is still the answer
* somebody was given last year, and the register of sent mail refers to it.
*/
public function toggleActive(string $uuid): void
{
$this->authorize('customers.manage');
$template = MailTemplate::query()->where('uuid', $uuid)->first();
if ($template !== null) {
$template->update(['active' => ! $template->active]);
}
}
/**
* Swap one with its neighbour.
*
* It used to add or subtract a fixed amount, which is not the same thing:
* with the seeded values ten apart, ±15 usually landed correctly — and
* sometimes landed on a value another template already had, or on the 0
* floor along with three others. Equal `sort` values fall back to the name,
* so a click could reorder something else, or visibly nothing, which is
* indistinguishable from a click that never registered.
*
* A swap always moves exactly one place, or does nothing at the ends, and
* both of those are what the operator expects to see.
*/
public function move(string $uuid, int $direction): void
{
$this->authorize('customers.manage');
$template = MailTemplate::query()->where('uuid', $uuid)->first();
if ($template === null) {
return;
}
// The one immediately above or below in the order the page shows —
// read through the same ordering, so the swap agrees with the list.
$neighbour = MailTemplate::query()
->when($direction < 0, fn ($q) => $q
->where(fn ($w) => $w->where('sort', '<', $template->sort)
->orWhere(fn ($t) => $t->where('sort', $template->sort)->where('name', '<', $template->name)))
->orderByDesc('sort')->orderByDesc('name'))
->when($direction > 0, fn ($q) => $q
->where(fn ($w) => $w->where('sort', '>', $template->sort)
->orWhere(fn ($t) => $t->where('sort', $template->sort)->where('name', '>', $template->name)))
->orderBy('sort')->orderBy('name'))
->first();
if ($neighbour === null) {
return; // already at the end; nothing to swap with
}
// Distinct values, even where the two were equal before: leaving them
// equal would put the order back in the hands of the name.
$mine = $template->sort;
$theirs = $neighbour->sort;
if ($mine === $theirs) {
// Moving UP means taking the lower value, so the one the mover
// receives is below the neighbour's — not above it. The other way
// round swaps them into the order they were already in, which looks
// exactly like the bug this method was rewritten to fix.
$theirs = $mine + ($direction < 0 ? -1 : 1);
}
$template->update(['sort' => $theirs]);
$neighbour->update(['sort' => $mine]);
}
public function render()
{
$this->authorize('customers.manage');
return view('livewire.admin.mail-templates', [
'templates' => MailTemplate::query()->orderBy('sort')->orderBy('name')->get(),
// The list IS the documentation: a token added to the renderer
// appears here without anybody remembering to write it down twice.
'placeholders' => MailTemplateRenderer::PLACEHOLDERS,
]);
}
}