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:
@@ -7,13 +7,19 @@ namespace App\Filament\Resources\Applications\Pages;
|
||||
use App\Filament\Resources\Applications\ApplicationResource;
|
||||
use App\Filament\Resources\Applications\Tables\ApplicationsTable;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationReply;
|
||||
use App\Models\ReplyTemplate;
|
||||
use App\Support\Inbox\Replier;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\ForceDeleteAction;
|
||||
use Filament\Actions\RestoreAction;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class ViewApplication extends ViewRecord
|
||||
{
|
||||
@@ -24,10 +30,67 @@ class ViewApplication extends ViewRecord
|
||||
return [
|
||||
// Answering is the point of the record, so the reply is the first
|
||||
// button rather than something to copy out of the page.
|
||||
// Answering is the point of the record, so the reply is the first
|
||||
// button: a template to start from, edited here, and sent — by
|
||||
// this application when it can send mail, or handed to the
|
||||
// person's own mail program with the text already written.
|
||||
Action::make('reply')
|
||||
->label(__('panel.applications.actions.reply'))
|
||||
->icon('heroicon-o-envelope')
|
||||
->color('primary')
|
||||
->authorize('update')
|
||||
->modalHeading(fn (Application $record) => __('panel.replies.heading', ['name' => $record->name]))
|
||||
->modalDescription(fn () => Replier::canSendMail() ? __('panel.replies.sends') : __('panel.replies.hands_over'))
|
||||
->modalSubmitActionLabel(fn () => Replier::canSendMail() ? __('panel.replies.send') : __('panel.replies.open_mail'))
|
||||
->schema([
|
||||
Select::make('template_id')
|
||||
->label(__('panel.replies.fields.template'))
|
||||
->options(fn (Application $record) => ReplyTemplate::query()->for($record->kind)->pluck('name', 'id')->all())
|
||||
->placeholder(__('panel.replies.no_template'))
|
||||
->live()
|
||||
->afterStateUpdated(function (?string $state, callable $set, Application $record): void {
|
||||
$template = $state ? ReplyTemplate::find($state) : null;
|
||||
|
||||
if ($template !== null) {
|
||||
$text = $template->renderFor($record, Auth::user());
|
||||
$set('subject', $text['subject']);
|
||||
$set('body', $text['body']);
|
||||
}
|
||||
}),
|
||||
|
||||
TextInput::make('subject')
|
||||
->label(__('panel.replies.fields.subject'))
|
||||
->default(fn (Application $record) => __('panel.applications.reply_subject', ['subject' => $record->subject()], $record->locale))
|
||||
->required()
|
||||
->maxLength(255),
|
||||
|
||||
Textarea::make('body')
|
||||
->label(__('panel.replies.fields.body'))
|
||||
->helperText(fn (Application $record) => __('panel.replies.in_language', [
|
||||
'language' => config("locales.supported.{$record->locale}.native", $record->locale),
|
||||
]))
|
||||
->required()
|
||||
->rows(12),
|
||||
])
|
||||
->action(function (Application $record, array $data, $livewire): void {
|
||||
$reply = Replier::reply($record, $data['subject'], $data['body'], Auth::user());
|
||||
|
||||
if ($reply->channel === ApplicationReply::MAILTO) {
|
||||
$livewire->js('window.location.href = '.json_encode(Replier::mailto($record, $data['subject'], $data['body'])));
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title($reply->channel === ApplicationReply::MAIL ? __('panel.replies.sent') : __('panel.replies.recorded'))
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
|
||||
// The way it always worked: an empty message in your own mail
|
||||
// program, for an answer that fits no template.
|
||||
Action::make('reply_elsewhere')
|
||||
->label(__('panel.replies.elsewhere'))
|
||||
->icon('heroicon-o-arrow-top-right-on-square')
|
||||
->color('gray')
|
||||
->url(fn (Application $record) => 'mailto:'.$record->email.'?subject='
|
||||
.rawurlencode(__('panel.applications.reply_subject', [
|
||||
'subject' => $record->subject(),
|
||||
|
||||
@@ -5,7 +5,9 @@ declare(strict_types=1);
|
||||
namespace App\Filament\Resources\Applications\Schemas;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationReply;
|
||||
use Filament\Infolists\Components\KeyValueEntry;
|
||||
use Filament\Infolists\Components\RepeatableEntry;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
@@ -69,6 +71,36 @@ class ApplicationInfolist
|
||||
->visible(fn (Application $record) => filled($record->links)),
|
||||
]),
|
||||
|
||||
// What was already said, so the next person to open this does not
|
||||
// say it again.
|
||||
Section::make(__('panel.replies.history'))
|
||||
->visible(fn (Application $record) => $record->replies()->exists())
|
||||
->schema([
|
||||
RepeatableEntry::make('replies')
|
||||
->hiddenLabel()
|
||||
->schema([
|
||||
TextEntry::make('subject')
|
||||
->hiddenLabel()
|
||||
->weight('bold'),
|
||||
|
||||
TextEntry::make('created_at')
|
||||
->hiddenLabel()
|
||||
->state(fn (ApplicationReply $record) => __('panel.replies.by', [
|
||||
'name' => $record->user_name ?? '—',
|
||||
'when' => $record->created_at?->isoFormat('LLL'),
|
||||
]).' · '.__('panel.replies.channels.'.$record->channel))
|
||||
->color('gray'),
|
||||
|
||||
TextEntry::make('body')
|
||||
->hiddenLabel()
|
||||
->prose()
|
||||
->formatStateUsing(fn (string $state) => nl2br(e($state)))
|
||||
->html()
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->columns(2),
|
||||
]),
|
||||
|
||||
Section::make(__('panel.applications.sections.internal'))
|
||||
->description(__('panel.applications.sections.internal_hint'))
|
||||
->columns(4)
|
||||
|
||||
Reference in New Issue
Block a user