Answer the inbox from the panel, with templates

A reply starts from a template filled in the language the person wrote in,
is sent by the application when it has a mailer that delivers or handed to
the replier's mail program otherwise, and is recorded on the submission.
The same is available through the API.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 10:45:47 +02:00
parent 697d333ad4
commit 76d022053b
27 changed files with 1096 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Models\Application;
use App\Models\ApplicationReply;
use App\Models\ReplyTemplate;
use App\Support\Inbox\Replier;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
/**
* Answering the inbox through the API: what was already said, the templates
* to start from, and sending an answer — the panel's "Reply".
*/
class ApplicationReplyController extends Controller
{
public function index(Application $application): JsonResponse
{
$this->authorize('view', $application);
return response()->json([
'data' => $application->replies()->get()->map(fn (ApplicationReply $reply) => $this->present($reply))->values(),
]);
}
/**
* `subject` and `body`, or a `template_id` to fill them from — in the
* language the person wrote in. Sent when this installation can send
* mail; otherwise recorded, with a `mailto` link to send it by hand.
*/
public function store(Request $request, Application $application): JsonResponse
{
$this->authorize('update', $application);
$data = $request->validate([
'template_id' => ['sometimes', 'nullable', 'integer', 'exists:reply_templates,id'],
'subject' => ['required_without:template_id', 'nullable', 'string', 'max:255'],
'body' => ['required_without:template_id', 'nullable', 'string', 'max:20000'],
]);
$text = ['subject' => $data['subject'] ?? null, 'body' => $data['body'] ?? null];
if (filled($data['template_id'] ?? null)) {
$rendered = ReplyTemplate::query()->findOrFail($data['template_id'])->renderFor($application, $request->user());
$text = ['subject' => $text['subject'] ?: $rendered['subject'], 'body' => $text['body'] ?: $rendered['body']];
}
if (blank($text['subject']) || blank($text['body'])) {
throw ValidationException::withMessages(['template_id' => [__('panel.replies.empty_template')]]);
}
$reply = Replier::reply($application, $text['subject'], $text['body'], $request->user());
return response()->json([
'data' => [
...$this->present($reply),
'mailto' => $reply->channel === ApplicationReply::MAILTO
? Replier::mailto($application, $text['subject'], $text['body'])
: null,
],
], 201);
}
public function templates(Request $request): JsonResponse
{
$this->authorize('viewAny', ReplyTemplate::class);
return response()->json([
'data' => ReplyTemplate::query()->orderBy('position')->orderBy('name')->get()->map(fn (ReplyTemplate $template) => [
'id' => $template->id,
'name' => $template->name,
'kind' => $template->kind,
'subject' => (object) $template->getTranslations('subject'),
'body' => (object) $template->getTranslations('body'),
])->values(),
]);
}
/**
* @return array<string, mixed>
*/
protected function present(ApplicationReply $reply): array
{
return [
'id' => $reply->id,
'subject' => $reply->subject,
'body' => $reply->body,
'channel' => $reply->channel,
'author' => ['id' => $reply->user_id, 'name' => $reply->user_name],
'created_at' => $reply->created_at?->toIso8601String(),
];
}
}