150 lines
5.4 KiB
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,
|
|
]);
|
|
}
|
|
}
|