Decision now support auto mailing
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\EmailLog;
|
||||
use App\Models\EmailTemplate;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class EmailLogController extends Controller
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$this->authorize('viewAny', EmailTemplate::class); // reuse same permission gate for admin area
|
||||
|
||||
$query = EmailLog::query()
|
||||
->with(['template:id,name'])
|
||||
->orderByDesc('created_at');
|
||||
|
||||
$status = trim((string) $request->input('status', ''));
|
||||
if ($status !== '') {
|
||||
$query->where('status', $status);
|
||||
}
|
||||
if ($email = trim((string) $request->input('to'))) {
|
||||
$query->where('to_email', 'like', '%'.str_replace(['%', '_'], ['\%', '\_'], $email).'%');
|
||||
}
|
||||
if ($subject = trim((string) $request->input('subject'))) {
|
||||
$query->where('subject', 'like', '%'.str_replace(['%', '_'], ['\%', '\_'], $subject).'%');
|
||||
}
|
||||
if ($templateId = (int) $request->input('template_id')) {
|
||||
$query->where('template_id', $templateId);
|
||||
}
|
||||
if ($from = $request->date('date_from')) {
|
||||
$query->whereDate('created_at', '>=', $from);
|
||||
}
|
||||
if ($to = $request->date('date_to')) {
|
||||
$query->whereDate('created_at', '<=', $to);
|
||||
}
|
||||
|
||||
$logs = $query->paginate(20)->withQueryString();
|
||||
$templates = EmailTemplate::query()->orderBy('name')->get(['id', 'name']);
|
||||
|
||||
return Inertia::render('Admin/EmailLogs/Index', [
|
||||
'logs' => $logs,
|
||||
'filters' => [
|
||||
'status' => $status,
|
||||
'to' => $email ?? '',
|
||||
'subject' => $subject ?? '',
|
||||
'template_id' => $templateId ?: null,
|
||||
'date_from' => $request->input('date_from'),
|
||||
'date_to' => $request->input('date_to'),
|
||||
],
|
||||
'templates' => $templates,
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(EmailLog $emailLog): Response
|
||||
{
|
||||
$this->authorize('viewAny', EmailTemplate::class);
|
||||
|
||||
$emailLog->load(['template:id,name', 'body']);
|
||||
|
||||
return Inertia::render('Admin/EmailLogs/Show', [
|
||||
'log' => $emailLog,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -4,26 +4,24 @@
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StoreEmailTemplateRequest;
|
||||
use App\Http\Requests\UpdateEmailTemplateRequest;
|
||||
use App\Jobs\SendEmailTemplateJob;
|
||||
use App\Models\Activity;
|
||||
use App\Models\Client;
|
||||
use App\Models\ClientCase;
|
||||
use App\Models\Contract;
|
||||
use App\Models\Document;
|
||||
use App\Models\EmailLog;
|
||||
use App\Models\EmailLogStatus;
|
||||
use App\Models\EmailTemplate;
|
||||
use App\Models\MailProfile;
|
||||
use App\Models\Person\Person;
|
||||
use App\Services\EmailTemplateRenderer;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Symfony\Component\Mailer\Mailer as SymfonyMailer;
|
||||
use Symfony\Component\Mailer\Transport;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use TijsVerkoyen\CssToInlineStyles\CssToInlineStyles;
|
||||
|
||||
@@ -31,6 +29,19 @@ class EmailTemplateController extends Controller
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public function update(\App\Http\Requests\UpdateEmailTemplateRequest $request, EmailTemplate $emailTemplate)
|
||||
{
|
||||
$this->authorize('update', $emailTemplate);
|
||||
$data = $request->validated();
|
||||
$emailTemplate->fill($data)->save();
|
||||
// Move any tmp images referenced in HTML into permanent storage and attach as documents
|
||||
$this->adoptTmpImages($emailTemplate);
|
||||
|
||||
return redirect()->route('admin.email-templates.edit', $emailTemplate)->with('success', 'Template updated');
|
||||
}
|
||||
|
||||
use AuthorizesRequests;
|
||||
|
||||
public function index(): Response
|
||||
{
|
||||
$this->authorize('viewAny', EmailTemplate::class);
|
||||
@@ -59,126 +70,45 @@ public function store(StoreEmailTemplateRequest $request)
|
||||
return redirect()->route('admin.email-templates.edit', $tpl)->with('success', 'Template created');
|
||||
}
|
||||
|
||||
public function edit(EmailTemplate $emailTemplate): Response
|
||||
{
|
||||
$this->authorize('update', $emailTemplate);
|
||||
$emailTemplate->load(['documents' => function ($q) {
|
||||
$q->select(['id', 'documentable_id', 'documentable_type', 'name', 'path', 'size', 'created_at']);
|
||||
}]);
|
||||
|
||||
return Inertia::render('Admin/EmailTemplates/Edit', [
|
||||
'template' => $emailTemplate,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UpdateEmailTemplateRequest $request, EmailTemplate $emailTemplate)
|
||||
{
|
||||
$data = $request->validated();
|
||||
$emailTemplate->update($data);
|
||||
$this->adoptTmpImages($emailTemplate);
|
||||
|
||||
return back()->with('success', 'Template updated');
|
||||
}
|
||||
|
||||
public function destroy(EmailTemplate $emailTemplate)
|
||||
{
|
||||
$this->authorize('delete', $emailTemplate);
|
||||
$emailTemplate->delete();
|
||||
|
||||
return redirect()->route('admin.email-templates.index')->with('success', 'Template deleted');
|
||||
}
|
||||
|
||||
public function preview(Request $request, EmailTemplate $emailTemplate)
|
||||
/**
|
||||
* Render a quick preview of the email template with the provided context.
|
||||
* Does not persist any changes or inline CSS; intended for fast editor feedback.
|
||||
*/
|
||||
public function preview(Request $request, EmailTemplate $emailTemplate): JsonResponse
|
||||
{
|
||||
$this->authorize('view', $emailTemplate);
|
||||
|
||||
$renderer = app(EmailTemplateRenderer::class);
|
||||
$subject = (string) $request->input('subject', $emailTemplate->subject_template);
|
||||
$html = (string) $request->input('html', $emailTemplate->html_template);
|
||||
$text = (string) $request->input('text', $emailTemplate->text_template);
|
||||
|
||||
// Resolve sample entities by ID if given
|
||||
$ctx = [];
|
||||
// Prefer contract -> case -> client for deriving person
|
||||
if ($id = $request->integer('contract_id')) {
|
||||
$contract = Contract::query()->with(['clientCase.client.person'])->find($id);
|
||||
if ($contract) {
|
||||
$ctx['contract'] = $contract;
|
||||
if ($contract->clientCase) {
|
||||
$ctx['client_case'] = $contract->clientCase;
|
||||
if ($contract->clientCase->client) {
|
||||
$ctx['client'] = $contract->clientCase->client;
|
||||
$ctx['person'] = optional($contract->clientCase->client)->person;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (! isset($ctx['client_case']) && ($id = $request->integer('case_id'))) {
|
||||
$case = ClientCase::query()->with(['client.person'])->find($id);
|
||||
if ($case) {
|
||||
$ctx['client_case'] = $case;
|
||||
if ($case->client) {
|
||||
$ctx['client'] = $case->client;
|
||||
$ctx['person'] = optional($case->client)->person;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (! isset($ctx['client']) && ($id = $request->integer('client_id'))) {
|
||||
$client = Client::query()->with(['person'])->find($id);
|
||||
if ($client) {
|
||||
$ctx['client'] = $client;
|
||||
$ctx['person'] = optional($client)->person;
|
||||
}
|
||||
}
|
||||
$ctx['extra'] = (array) $request->input('extra', []);
|
||||
|
||||
$result = $renderer->render([
|
||||
'subject' => $subject,
|
||||
'html' => $html,
|
||||
'text' => $text,
|
||||
], $ctx);
|
||||
|
||||
// Repair and attach images, then embed as requested
|
||||
if (! empty($result['html'])) {
|
||||
$result['html'] = $this->repairImgWithoutSrc($result['html']);
|
||||
$result['html'] = $this->attachSrcFromTemplateDocuments($emailTemplate, $result['html']);
|
||||
|
||||
$embed = (string) $request->input('embed', 'base64'); // hosted | base64
|
||||
if ($embed === 'base64') {
|
||||
try {
|
||||
$imageInliner = app(\App\Services\EmailImageInliner::class);
|
||||
$result['html'] = $imageInliner->inline($result['html']);
|
||||
} catch (\Throwable $e) {
|
||||
// ignore preview image inlining errors
|
||||
}
|
||||
} else {
|
||||
$result['html'] = $this->absolutizeStorageUrls($request, $result['html']);
|
||||
}
|
||||
|
||||
try {
|
||||
$inliner = new CssToInlineStyles;
|
||||
$result['html'] = $inliner->convert($result['html']);
|
||||
} catch (\Throwable $e) {
|
||||
// ignore preview inlining errors
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json($result);
|
||||
}
|
||||
|
||||
public function sendTest(Request $request, EmailTemplate $emailTemplate)
|
||||
{
|
||||
$this->authorize('send', $emailTemplate);
|
||||
|
||||
$renderer = app(EmailTemplateRenderer::class);
|
||||
$subject = (string) ($request->input('subject') ?? $emailTemplate->subject_template);
|
||||
$html = (string) ($request->input('html') ?? $emailTemplate->html_template);
|
||||
$text = (string) ($request->input('text') ?? $emailTemplate->text_template);
|
||||
|
||||
// Adopt tmp images (tmp/email-images) so test email can display images; also persist
|
||||
$html = $this->adoptTmpImagesInHtml($emailTemplate, $html, true);
|
||||
// Do not persist tmp images for preview, but allow showing them if already accessible
|
||||
// Optionally repair missing img src and attach from template documents for a better preview
|
||||
if (! empty($html)) {
|
||||
$html = $this->repairImgWithoutSrc($html);
|
||||
$html = $this->attachSrcFromTemplateDocuments($emailTemplate, $html);
|
||||
}
|
||||
|
||||
// Context resolution (shared logic with renderFinalHtml)
|
||||
$ctx = [];
|
||||
if ($id = $request->integer('activity_id')) {
|
||||
$activity = Activity::query()->with(['action', 'decision', 'contract.clientCase.client.person', 'clientCase.client.person'])->find($id);
|
||||
if ($activity) {
|
||||
$ctx['activity'] = $activity;
|
||||
// Derive base entities from activity when not explicitly provided
|
||||
if ($activity->contract && ! isset($ctx['contract'])) {
|
||||
$ctx['contract'] = $activity->contract;
|
||||
}
|
||||
if ($activity->clientCase && ! isset($ctx['client_case'])) {
|
||||
$ctx['client_case'] = $activity->clientCase;
|
||||
}
|
||||
if (($ctx['client_case'] ?? null) && ! isset($ctx['client'])) {
|
||||
$ctx['client'] = $ctx['client_case']->client;
|
||||
$ctx['person'] = optional($ctx['client'])->person;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($id = $request->integer('contract_id')) {
|
||||
$contract = Contract::query()->with(['clientCase.client.person'])->find($id);
|
||||
if ($contract) {
|
||||
@@ -217,107 +147,131 @@ public function sendTest(Request $request, EmailTemplate $emailTemplate)
|
||||
'text' => $text,
|
||||
], $ctx);
|
||||
|
||||
return response()->json([
|
||||
'subject' => $rendered['subject'] ?? $subject,
|
||||
'html' => (string) ($rendered['html'] ?? $html ?? ''),
|
||||
'text' => (string) ($rendered['text'] ?? $text ?? ''),
|
||||
]);
|
||||
}
|
||||
|
||||
public function edit(EmailTemplate $emailTemplate): Response
|
||||
{
|
||||
$this->authorize('update', $emailTemplate);
|
||||
$emailTemplate->load(['documents' => function ($q) {
|
||||
$q->select(['id', 'documentable_id', 'documentable_type', 'name', 'path', 'size', 'created_at']);
|
||||
}]);
|
||||
|
||||
return Inertia::render('Admin/EmailTemplates/Edit', [
|
||||
'template' => $emailTemplate,
|
||||
]);
|
||||
}
|
||||
|
||||
public function sendTest(Request $request, EmailTemplate $emailTemplate)
|
||||
{
|
||||
$this->authorize('send', $emailTemplate);
|
||||
|
||||
$renderer = app(EmailTemplateRenderer::class);
|
||||
$subject = (string) ($request->input('subject') ?? $emailTemplate->subject_template);
|
||||
$html = (string) ($request->input('html') ?? $emailTemplate->html_template);
|
||||
$text = (string) ($request->input('text') ?? $emailTemplate->text_template);
|
||||
|
||||
// Adopt tmp images (tmp/email-images) so test email can display images; also persist
|
||||
$html = $this->adoptTmpImagesInHtml($emailTemplate, $html, true);
|
||||
|
||||
// Context resolution
|
||||
$ctx = [];
|
||||
if ($id = $request->integer('activity_id')) {
|
||||
$activity = Activity::query()->with(['action', 'decision', 'contract.clientCase.client.person', 'clientCase.client.person'])->find($id);
|
||||
if ($activity) {
|
||||
$ctx['activity'] = $activity;
|
||||
if ($activity->contract && ! isset($ctx['contract'])) {
|
||||
$ctx['contract'] = $activity->contract;
|
||||
}
|
||||
if ($activity->clientCase && ! isset($ctx['client_case'])) {
|
||||
$ctx['client_case'] = $activity->clientCase;
|
||||
}
|
||||
if (($ctx['client_case'] ?? null) && ! isset($ctx['client'])) {
|
||||
$ctx['client'] = $ctx['client_case']->client;
|
||||
$ctx['person'] = optional($ctx['client'])->person;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($id = $request->integer('contract_id')) {
|
||||
$contract = Contract::query()->with(['clientCase.client.person'])->find($id);
|
||||
if ($contract) {
|
||||
$ctx['contract'] = $contract;
|
||||
if ($contract->clientCase) {
|
||||
$ctx['client_case'] = $contract->clientCase;
|
||||
if ($contract->clientCase->client) {
|
||||
$ctx['client'] = $contract->clientCase->client;
|
||||
$ctx['person'] = optional($contract->clientCase->client)->person;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (! isset($ctx['client_case']) && ($id = $request->integer('case_id'))) {
|
||||
$case = ClientCase::query()->with(['client.person'])->find($id);
|
||||
if ($case) {
|
||||
$ctx['client_case'] = $case;
|
||||
if ($case->client) {
|
||||
$ctx['client'] = $case->client;
|
||||
$ctx['person'] = optional($case->client)->person;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (! isset($ctx['client']) && ($id = $request->integer('client_id'))) {
|
||||
$client = Client::query()->with(['person'])->find($id);
|
||||
if ($client) {
|
||||
$ctx['client'] = $client;
|
||||
$ctx['person'] = optional($client)->person;
|
||||
}
|
||||
}
|
||||
$ctx['extra'] = (array) $request->input('extra', []);
|
||||
|
||||
// Render preview values; we store a minimal snapshot on the log
|
||||
$rendered = $renderer->render([
|
||||
'subject' => $subject,
|
||||
'html' => $html,
|
||||
'text' => $text,
|
||||
], $ctx);
|
||||
|
||||
$to = (string) $request->input('to');
|
||||
if ($to === '' || ! filter_var($to, FILTER_VALIDATE_EMAIL)) {
|
||||
return back()->with('error', 'Invalid target email');
|
||||
}
|
||||
|
||||
// First repair images missing src if they are followed by a URL (editor artifact)
|
||||
if (! empty($rendered['html'])) {
|
||||
$rendered['html'] = $this->repairImgWithoutSrc($rendered['html']);
|
||||
$rendered['html'] = $this->attachSrcFromTemplateDocuments($emailTemplate, $rendered['html']);
|
||||
}
|
||||
// Embed images as requested (default hosted for Gmail compatibility)
|
||||
$htmlForSend = $rendered['html'] ?? '';
|
||||
$embed = (string) $request->input('embed', 'base64');
|
||||
// Prepare EmailLog record with queued status
|
||||
$log = new EmailLog;
|
||||
$log->fill([
|
||||
'uuid' => (string) \Str::uuid(),
|
||||
'template_id' => $emailTemplate->id,
|
||||
'to_email' => $to,
|
||||
'to_name' => null,
|
||||
'subject' => (string) ($rendered['subject'] ?? $subject ?? ''),
|
||||
'body_html_hash' => $rendered['html'] ? hash('sha256', $rendered['html']) : null,
|
||||
'body_text_preview' => isset($rendered['text']) ? mb_strimwidth((string) $rendered['text'], 0, 4096) : null,
|
||||
'embed_mode' => (string) $request->input('embed', 'base64'),
|
||||
'status' => EmailLogStatus::Queued,
|
||||
'queued_at' => now(),
|
||||
'client_id' => $ctx['client']->id ?? null,
|
||||
'client_case_id' => $ctx['client_case']->id ?? null,
|
||||
'contract_id' => $ctx['contract']->id ?? null,
|
||||
'extra_context' => $ctx['extra'] ?? null,
|
||||
'ip' => $request->ip(),
|
||||
]);
|
||||
$log->save();
|
||||
|
||||
// Prefer the active Mail Profile for sending test emails
|
||||
$subject = $rendered['subject'] ?? '';
|
||||
$profile = MailProfile::query()
|
||||
->where('active', true)
|
||||
->orderBy('priority')
|
||||
->orderBy('id')
|
||||
->first();
|
||||
// Store bodies in companion table (optional, enabled here)
|
||||
$log->body()->create([
|
||||
'body_html' => (string) ($rendered['html'] ?? ''),
|
||||
'body_text' => (string) ($rendered['text'] ?? ''),
|
||||
'inline_css' => true,
|
||||
]);
|
||||
|
||||
try {
|
||||
if ($profile) {
|
||||
$host = $profile->host;
|
||||
$port = (int) ($profile->port ?: 587);
|
||||
$encryption = $profile->encryption ?: 'tls';
|
||||
$username = $profile->username ?: '';
|
||||
$password = (string) ($profile->decryptPassword() ?? '');
|
||||
// Dispatch the queued job
|
||||
dispatch(new SendEmailTemplateJob($log->id));
|
||||
|
||||
$scheme = $encryption === 'ssl' ? 'smtps' : 'smtp';
|
||||
$query = $encryption === 'tls' ? '?encryption=tls' : '';
|
||||
$dsn = sprintf('%s://%s:%s@%s:%d%s', $scheme, rawurlencode($username), rawurlencode($password), $host, $port, $query);
|
||||
|
||||
$transport = Transport::fromDsn($dsn);
|
||||
$mailer = new SymfonyMailer($transport);
|
||||
|
||||
$fromAddr = $profile->from_address ?: ($username ?: (config('mail.from.address') ?? ''));
|
||||
$fromName = $profile->from_name ?: (config('mail.from.name') ?? config('app.name'));
|
||||
|
||||
$email = (new Email)
|
||||
->from(new Address($fromAddr ?: $to, $fromName ?: null))
|
||||
->to($to)
|
||||
->subject($subject);
|
||||
|
||||
if (! empty($rendered['text'])) {
|
||||
$email->text($rendered['text']);
|
||||
}
|
||||
if (! empty($htmlForSend)) {
|
||||
if ($embed === 'base64') {
|
||||
try {
|
||||
$imageInliner = app(\App\Services\EmailImageInliner::class);
|
||||
$htmlForSend = $imageInliner->inline($htmlForSend);
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
} else {
|
||||
$htmlForSend = $this->absolutizeStorageUrls($request, $htmlForSend);
|
||||
}
|
||||
try {
|
||||
$inliner = new CssToInlineStyles;
|
||||
$htmlForSend = $inliner->convert($htmlForSend);
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
$email->html($htmlForSend);
|
||||
}
|
||||
|
||||
$mailer->send($email);
|
||||
} else {
|
||||
// Fallback to default Laravel mailer
|
||||
if (! empty($htmlForSend)) {
|
||||
if ($embed === 'base64') {
|
||||
try {
|
||||
$imageInliner = app(\App\Services\EmailImageInliner::class);
|
||||
$htmlForSend = $imageInliner->inline($htmlForSend);
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
} else {
|
||||
$htmlForSend = $this->absolutizeStorageUrls($request, $htmlForSend);
|
||||
}
|
||||
try {
|
||||
$inliner = new CssToInlineStyles;
|
||||
$htmlForSend = $inliner->convert($htmlForSend);
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
Mail::html($htmlForSend, function ($message) use ($to, $subject, $rendered) {
|
||||
$message->to($to)->subject($subject);
|
||||
if (! empty($rendered['text'])) {
|
||||
$message->text('mail::raw', ['slot' => $rendered['text']]);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Mail::raw($rendered['text'] ?? '', function ($message) use ($to, $subject) {
|
||||
$message->to($to)->subject($subject);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return back()->with('success', 'Test email sent to '.$to);
|
||||
} catch (\Throwable $e) {
|
||||
return back()->with('error', 'Failed to send test email: '.$e->getMessage());
|
||||
}
|
||||
return back()->with('success', 'Test email queued for '.$to);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -338,6 +292,22 @@ public function renderFinalHtml(Request $request, EmailTemplate $emailTemplate)
|
||||
|
||||
// Context resolution (same as sendTest)
|
||||
$ctx = [];
|
||||
if ($id = $request->integer('activity_id')) {
|
||||
$activity = Activity::query()->with(['action', 'decision', 'contract.clientCase.client.person', 'clientCase.client.person'])->find($id);
|
||||
if ($activity) {
|
||||
$ctx['activity'] = $activity;
|
||||
if ($activity->contract && ! isset($ctx['contract'])) {
|
||||
$ctx['contract'] = $activity->contract;
|
||||
}
|
||||
if ($activity->clientCase && ! isset($ctx['client_case'])) {
|
||||
$ctx['client_case'] = $activity->clientCase;
|
||||
}
|
||||
if (($ctx['client_case'] ?? null) && ! isset($ctx['client'])) {
|
||||
$ctx['client'] = $ctx['client_case']->client;
|
||||
$ctx['person'] = optional($ctx['client'])->person;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($id = $request->integer('contract_id')) {
|
||||
$contract = Contract::query()->with(['clientCase.client.person'])->find($id);
|
||||
if ($contract) {
|
||||
@@ -712,6 +682,28 @@ public function replaceImage(Request $request, EmailTemplate $emailTemplate)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an attached image Document from the given email template.
|
||||
*/
|
||||
public function deleteImage(Request $request, EmailTemplate $emailTemplate, Document $document)
|
||||
{
|
||||
$this->authorize('update', $emailTemplate);
|
||||
|
||||
// Ensure the document belongs to this template (polymorphic relation)
|
||||
if ((int) $document->documentable_id !== (int) $emailTemplate->id || $document->documentable_type !== EmailTemplate::class) {
|
||||
return response()->json(['message' => 'Document does not belong to this template.'], 422);
|
||||
}
|
||||
|
||||
try {
|
||||
// Force delete to remove underlying file as well (Document model handles file deletion on force delete)
|
||||
$document->forceDelete();
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json(['message' => 'Failed to delete image: '.$e->getMessage()], 500);
|
||||
}
|
||||
|
||||
return response()->json(['deleted' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan HTML for images stored in /storage/tmp/email-images and move them into a permanent
|
||||
* location under /storage/email-images, create Document records and update the HTML.
|
||||
|
||||
@@ -126,14 +126,15 @@ public function sendTest(Request $request, MailProfile $mailProfile)
|
||||
$mailer = new SymfonyMailer($transport);
|
||||
|
||||
$fromAddr = $mailProfile->from_address ?: $username;
|
||||
$fromName = $mailProfile->from_name ?: config('app.name');
|
||||
$fromName = (string) ($mailProfile->from_name ?: (config('mail.from.name') ?? config('app.name') ?? ''));
|
||||
|
||||
$html = '<p>This is a <strong>test email</strong> from profile <code>'.e($mailProfile->name).'</code> at '.e(now()->toDateTimeString()).'.</p>';
|
||||
$text = 'This is a test email from profile "'.$mailProfile->name.'" at '.now()->toDateTimeString().'.';
|
||||
|
||||
// Build email
|
||||
$fromAddress = $fromName !== '' ? new Address($fromAddr, $fromName) : new Address($fromAddr);
|
||||
$email = (new Email)
|
||||
->from(new Address($fromAddr, $fromName))
|
||||
->from($fromAddress)
|
||||
->to($to)
|
||||
->subject('Test email - '.$mailProfile->name)
|
||||
->text($text)
|
||||
|
||||
@@ -21,7 +21,7 @@ class ClientCaseContoller extends Controller
|
||||
public function index(ClientCase $clientCase, Request $request)
|
||||
{
|
||||
$query = $clientCase::query()
|
||||
->with(['person', 'client.person'])
|
||||
->with(['person.client', 'client.person'])
|
||||
->where('active', 1)
|
||||
->when($request->input('search'), function ($que, $search) {
|
||||
$que->whereHas('person', function ($q) use ($search) {
|
||||
@@ -251,6 +251,7 @@ public function storeActivity(ClientCase $clientCase, Request $request)
|
||||
'action_id' => 'exists:\App\Models\Action,id',
|
||||
'decision_id' => 'exists:\App\Models\Decision,id',
|
||||
'contract_uuid' => 'nullable|uuid',
|
||||
'send_auto_mail' => 'sometimes|boolean',
|
||||
]);
|
||||
|
||||
// Map contract_uuid to contract_id within the same client case, if provided
|
||||
@@ -279,6 +280,23 @@ public function storeActivity(ClientCase $clientCase, Request $request)
|
||||
|
||||
logger()->info('Activity successfully inserted', $attributes);
|
||||
|
||||
// Auto mail dispatch (best-effort)
|
||||
try {
|
||||
$sendFlag = (bool) ($attributes['send_auto_mail'] ?? true);
|
||||
$row->load(['decision', 'clientCase.client.person', 'clientCase.person', 'contract']);
|
||||
$result = app(\App\Services\AutoMailDispatcher::class)->maybeQueue($row, $sendFlag);
|
||||
if (($result['skipped'] ?? null) === 'missing-contract' && $sendFlag) {
|
||||
// If template requires contract and user attempted to send, surface a validation message
|
||||
return back()->with('warning', 'Email not queued: required contract is missing for the selected template.');
|
||||
}
|
||||
if (($result['skipped'] ?? null) === 'no-recipients' && $sendFlag) {
|
||||
return back()->with('warning', 'Email not queued: no eligible client emails to receive auto mails.');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Do not fail activity creation due to mailing issues
|
||||
logger()->warning('Auto mail dispatch failed: '.$e->getMessage());
|
||||
}
|
||||
|
||||
// Stay on the current page (desktop or phone) instead of forcing a redirect to the desktop route.
|
||||
// Use 303 to align with Inertia's recommended POST/Redirect/GET behavior.
|
||||
return back(303)->with('success', 'Successful created!');
|
||||
@@ -1020,7 +1038,7 @@ protected function streamDocumentForDisk(Document $document, bool $inline = true
|
||||
public function show(ClientCase $clientCase)
|
||||
{
|
||||
$case = $clientCase::with([
|
||||
'person' => fn ($que) => $que->with(['addresses', 'phones', 'emails', 'bankAccounts']),
|
||||
'person' => fn ($que) => $que->with(['addresses', 'phones', 'emails', 'bankAccounts', 'client']),
|
||||
])->where('active', 1)->findOrFail($clientCase->id);
|
||||
|
||||
$types = [
|
||||
@@ -1174,7 +1192,7 @@ public function show(ClientCase $clientCase)
|
||||
}
|
||||
|
||||
return Inertia::render('Cases/Show', [
|
||||
'client' => $case->client()->with('person', fn ($q) => $q->with(['addresses', 'phones', 'emails', 'bankAccounts']))->firstOrFail(),
|
||||
'client' => $case->client()->with('person', fn ($q) => $q->with(['addresses', 'phones', 'emails', 'bankAccounts', 'client']))->firstOrFail(),
|
||||
'client_case' => $case,
|
||||
'contracts' => $contracts,
|
||||
'archive_meta' => [
|
||||
@@ -1209,11 +1227,17 @@ function ($p) {
|
||||
'documents' => $mergedDocs,
|
||||
'contract_types' => \App\Models\ContractType::whereNull('deleted_at')->get(),
|
||||
'account_types' => \App\Models\AccountType::all(),
|
||||
'actions' => \App\Models\Action::with('decisions')
|
||||
/*->when($segmentId, function($q) use($segmentId) {
|
||||
$q->where('segment_id', $segmentId)->orWhereNull('segment_id');
|
||||
})*/
|
||||
->get(),
|
||||
// Include decisions with auto-mail metadata and the linked email template entity_types for UI logic
|
||||
'actions' => \App\Models\Action::query()
|
||||
->with([
|
||||
'decisions' => function ($q) {
|
||||
$q->select('decisions.id', 'decisions.name', 'decisions.color_tag', 'decisions.auto_mail', 'decisions.email_template_id');
|
||||
},
|
||||
'decisions.emailTemplate' => function ($q) {
|
||||
$q->select('id', 'name', 'entity_types');
|
||||
},
|
||||
])
|
||||
->get(['id', 'name', 'color_tag', 'segment_id']),
|
||||
'types' => $types,
|
||||
'segments' => $case->segments()->wherePivot('active', true)->get(['segments.id', 'segments.name']),
|
||||
'all_segments' => \App\Models\Segment::query()->where('active', true)->get(['id', 'name']),
|
||||
|
||||
@@ -63,7 +63,7 @@ public function show(Client $client, Request $request)
|
||||
{
|
||||
|
||||
$data = $client::query()
|
||||
->with(['person' => fn ($que) => $que->with(['addresses', 'phones', 'bankAccounts', 'emails'])])
|
||||
->with(['person' => fn ($que) => $que->with(['addresses', 'phones', 'bankAccounts', 'emails', 'client'])])
|
||||
->findOrFail($client->id);
|
||||
|
||||
$types = [
|
||||
|
||||
@@ -2,32 +2,26 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Person\Person;
|
||||
use App\Models\BankAccount;
|
||||
use App\Models\Person\Person;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class PersonController extends Controller
|
||||
{
|
||||
//
|
||||
public function show(Person $person){
|
||||
|
||||
}
|
||||
public function show(Person $person) {}
|
||||
|
||||
public function create(Request $request){
|
||||
public function create(Request $request) {}
|
||||
|
||||
}
|
||||
public function store(Request $request) {}
|
||||
|
||||
public function store(Request $request){
|
||||
|
||||
}
|
||||
|
||||
public function update(Person $person, Request $request){
|
||||
public function update(Person $person, Request $request)
|
||||
{
|
||||
$attributes = $request->validate([
|
||||
'full_name' => 'string|max:255',
|
||||
'tax_number' => 'nullable|integer',
|
||||
'social_security_number' => 'nullable|integer',
|
||||
'description' => 'nullable|string|max:500'
|
||||
'description' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
$person->update($attributes);
|
||||
@@ -37,17 +31,18 @@ public function update(Person $person, Request $request){
|
||||
'full_name' => $person->full_name,
|
||||
'tax_number' => $person->tax_number,
|
||||
'social_security_number' => $person->social_security_number,
|
||||
'description' => $person->description
|
||||
]
|
||||
'description' => $person->description,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function createAddress(Person $person, Request $request){
|
||||
public function createAddress(Person $person, Request $request)
|
||||
{
|
||||
$attributes = $request->validate([
|
||||
'address' => 'required|string|max:150',
|
||||
'country' => 'nullable|string',
|
||||
'type_id' => 'required|integer|exists:address_types,id',
|
||||
'description' => 'nullable|string|max:125'
|
||||
'description' => 'nullable|string|max:125',
|
||||
]);
|
||||
|
||||
// Dedup: avoid duplicate address per person by (address, country)
|
||||
@@ -57,7 +52,7 @@ public function createAddress(Person $person, Request $request){
|
||||
], $attributes);
|
||||
|
||||
return response()->json([
|
||||
'address' => \App\Models\Person\PersonAddress::with(['type'])->findOrFail($address->id)
|
||||
'address' => \App\Models\Person\PersonAddress::with(['type'])->findOrFail($address->id),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -67,7 +62,7 @@ public function updateAddress(Person $person, int $address_id, Request $request)
|
||||
'address' => 'required|string|max:150',
|
||||
'country' => 'nullable|string',
|
||||
'type_id' => 'required|integer|exists:address_types,id',
|
||||
'description' => 'nullable|string|max:125'
|
||||
'description' => 'nullable|string|max:125',
|
||||
]);
|
||||
|
||||
$address = $person->addresses()->with(['type'])->findOrFail($address_id);
|
||||
@@ -75,7 +70,7 @@ public function updateAddress(Person $person, int $address_id, Request $request)
|
||||
$address->update($attributes);
|
||||
|
||||
return response()->json([
|
||||
'address' => $address
|
||||
'address' => $address,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -83,6 +78,7 @@ public function deleteAddress(Person $person, int $address_id, Request $request)
|
||||
{
|
||||
$address = $person->addresses()->findOrFail($address_id);
|
||||
$address->delete(); // soft delete
|
||||
|
||||
return response()->json(['status' => 'ok']);
|
||||
}
|
||||
|
||||
@@ -92,7 +88,7 @@ public function createPhone(Person $person, Request $request)
|
||||
'nu' => 'required|string|max:50',
|
||||
'country_code' => 'nullable|integer',
|
||||
'type_id' => 'required|integer|exists:phone_types,id',
|
||||
'description' => 'nullable|string|max:125'
|
||||
'description' => 'nullable|string|max:125',
|
||||
]);
|
||||
|
||||
// Dedup: avoid duplicate phone per person by (nu, country_code)
|
||||
@@ -102,7 +98,7 @@ public function createPhone(Person $person, Request $request)
|
||||
], $attributes);
|
||||
|
||||
return response()->json([
|
||||
'phone' => \App\Models\Person\PersonPhone::with(['type'])->findOrFail($phone->id)
|
||||
'phone' => \App\Models\Person\PersonPhone::with(['type'])->findOrFail($phone->id),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -112,7 +108,7 @@ public function updatePhone(Person $person, int $phone_id, Request $request)
|
||||
'nu' => 'required|string|max:50',
|
||||
'country_code' => 'nullable|integer',
|
||||
'type_id' => 'required|integer|exists:phone_types,id',
|
||||
'description' => 'nullable|string|max:125'
|
||||
'description' => 'nullable|string|max:125',
|
||||
]);
|
||||
|
||||
$phone = $person->phones()->with(['type'])->findOrFail($phone_id);
|
||||
@@ -120,7 +116,7 @@ public function updatePhone(Person $person, int $phone_id, Request $request)
|
||||
$phone->update($attributes);
|
||||
|
||||
return response()->json([
|
||||
'phone' => $phone
|
||||
'phone' => $phone,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -128,6 +124,7 @@ public function deletePhone(Person $person, int $phone_id, Request $request)
|
||||
{
|
||||
$phone = $person->phones()->findOrFail($phone_id);
|
||||
$phone->delete(); // soft delete
|
||||
|
||||
return response()->json(['status' => 'ok']);
|
||||
}
|
||||
|
||||
@@ -139,6 +136,7 @@ public function createEmail(Person $person, Request $request)
|
||||
'is_primary' => 'boolean',
|
||||
'is_active' => 'boolean',
|
||||
'valid' => 'boolean',
|
||||
'receive_auto_mails' => 'sometimes|boolean',
|
||||
'verified_at' => 'nullable|date',
|
||||
'preferences' => 'nullable|array',
|
||||
'meta' => 'nullable|array',
|
||||
@@ -149,9 +147,7 @@ public function createEmail(Person $person, Request $request)
|
||||
'value' => $attributes['value'],
|
||||
], $attributes);
|
||||
|
||||
return response()->json([
|
||||
'email' => \App\Models\Email::findOrFail($email->id)
|
||||
]);
|
||||
return back()->with('success', 'Email added successfully');
|
||||
}
|
||||
|
||||
public function updateEmail(Person $person, int $email_id, Request $request)
|
||||
@@ -162,6 +158,7 @@ public function updateEmail(Person $person, int $email_id, Request $request)
|
||||
'is_primary' => 'boolean',
|
||||
'is_active' => 'boolean',
|
||||
'valid' => 'boolean',
|
||||
'receive_auto_mails' => 'sometimes|boolean',
|
||||
'verified_at' => 'nullable|date',
|
||||
'preferences' => 'nullable|array',
|
||||
'meta' => 'nullable|array',
|
||||
@@ -171,15 +168,14 @@ public function updateEmail(Person $person, int $email_id, Request $request)
|
||||
|
||||
$email->update($attributes);
|
||||
|
||||
return response()->json([
|
||||
'email' => $email
|
||||
]);
|
||||
return back()->with('success', 'Email updated successfully');
|
||||
}
|
||||
|
||||
public function deleteEmail(Person $person, int $email_id, Request $request)
|
||||
{
|
||||
$email = $person->emails()->findOrFail($email_id);
|
||||
$email->delete();
|
||||
|
||||
return response()->json(['status' => 'ok']);
|
||||
}
|
||||
|
||||
@@ -203,7 +199,7 @@ public function createTrr(Person $person, Request $request)
|
||||
$trr = $person->bankAccounts()->create($attributes);
|
||||
|
||||
return response()->json([
|
||||
'trr' => BankAccount::findOrFail($trr->id)
|
||||
'trr' => BankAccount::findOrFail($trr->id),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -227,7 +223,7 @@ public function updateTrr(Person $person, int $trr_id, Request $request)
|
||||
$trr->update($attributes);
|
||||
|
||||
return response()->json([
|
||||
'trr' => $trr
|
||||
'trr' => $trr,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -235,6 +231,7 @@ public function deleteTrr(Person $person, int $trr_id, Request $request)
|
||||
{
|
||||
$trr = $person->bankAccounts()->findOrFail($trr_id);
|
||||
$trr->delete();
|
||||
|
||||
return response()->json(['status' => 'ok']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
use App\Models\Action;
|
||||
use App\Models\Decision;
|
||||
use App\Models\EmailTemplate;
|
||||
use App\Models\Segment;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class WorkflowController extends Controller
|
||||
@@ -17,6 +17,7 @@ public function index(Request $request)
|
||||
'actions' => Action::query()->with(['decisions', 'segment'])->withCount('activities')->get(),
|
||||
'decisions' => Decision::query()->with('actions')->withCount('activities')->get(),
|
||||
'segments' => Segment::query()->get(),
|
||||
'email_templates' => EmailTemplate::query()->where('active', true)->get(['id', 'name', 'entity_types']),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -41,7 +42,7 @@ public function storeAction(Request $request)
|
||||
'segment_id' => $attributes['segment_id'] ?? null,
|
||||
]);
|
||||
|
||||
if (!empty($decisionIds)) {
|
||||
if (! empty($decisionIds)) {
|
||||
$row->decisions()->sync($decisionIds);
|
||||
}
|
||||
});
|
||||
@@ -59,12 +60,12 @@ public function updateAction(int $id, Request $request)
|
||||
'segment_id' => 'nullable|integer|exists:segments,id',
|
||||
'decisions' => 'nullable|array',
|
||||
'decisions.*.id' => 'required_with:decisions.*|integer|exists:decisions,id',
|
||||
'decisions.*.name' => 'required_with:decisions.*|string|max:50'
|
||||
'decisions.*.name' => 'required_with:decisions.*|string|max:50',
|
||||
]);
|
||||
|
||||
$decisionIds = collect($attributes['decisions'] ?? [])->pluck('id')->toArray();
|
||||
|
||||
\DB::transaction(function() use ($attributes, $decisionIds, $row) {
|
||||
\DB::transaction(function () use ($attributes, $decisionIds, $row) {
|
||||
$row->update([
|
||||
'name' => $attributes['name'],
|
||||
'color_tag' => $attributes['color_tag'],
|
||||
@@ -81,6 +82,8 @@ public function storeDecision(Request $request)
|
||||
$attributes = $request->validate([
|
||||
'name' => 'required|string|max:50',
|
||||
'color_tag' => 'nullable|string|max:25',
|
||||
'auto_mail' => 'sometimes|boolean',
|
||||
'email_template_id' => 'nullable|integer|exists:email_templates,id',
|
||||
'actions' => 'nullable|array',
|
||||
'actions.*.id' => 'required_with:actions.*|integer|exists:actions,id',
|
||||
'actions.*.name' => 'required_with:actions.*|string|max:50',
|
||||
@@ -93,9 +96,11 @@ public function storeDecision(Request $request)
|
||||
$row = Decision::create([
|
||||
'name' => $attributes['name'],
|
||||
'color_tag' => $attributes['color_tag'] ?? null,
|
||||
'auto_mail' => (bool) ($attributes['auto_mail'] ?? false),
|
||||
'email_template_id' => $attributes['email_template_id'] ?? null,
|
||||
]);
|
||||
|
||||
if (!empty($actionIds)) {
|
||||
if (! empty($actionIds)) {
|
||||
$row->actions()->sync($actionIds);
|
||||
}
|
||||
});
|
||||
@@ -110,6 +115,8 @@ public function updateDecision(int $id, Request $request)
|
||||
$attributes = $request->validate([
|
||||
'name' => 'required|string|max:50',
|
||||
'color_tag' => 'nullable|string|max:25',
|
||||
'auto_mail' => 'sometimes|boolean',
|
||||
'email_template_id' => 'nullable|integer|exists:email_templates,id',
|
||||
'actions' => 'nullable|array',
|
||||
'actions.*.id' => 'required_with:actions.*|integer|exists:actions,id',
|
||||
'actions.*.name' => 'required_with:actions.*|string|max:50',
|
||||
@@ -121,6 +128,8 @@ public function updateDecision(int $id, Request $request)
|
||||
$row->update([
|
||||
'name' => $attributes['name'],
|
||||
'color_tag' => $attributes['color_tag'] ?? null,
|
||||
'auto_mail' => (bool) ($attributes['auto_mail'] ?? false),
|
||||
'email_template_id' => $attributes['email_template_id'] ?? null,
|
||||
]);
|
||||
$row->actions()->sync($actionIds);
|
||||
});
|
||||
@@ -139,6 +148,7 @@ public function destroyAction(int $id)
|
||||
$row->decisions()->detach();
|
||||
$row->delete();
|
||||
});
|
||||
|
||||
return back()->with('success', 'Action deleted successfully!');
|
||||
}
|
||||
|
||||
@@ -153,6 +163,7 @@ public function destroyDecision(int $id)
|
||||
$row->actions()->detach();
|
||||
$row->delete();
|
||||
});
|
||||
|
||||
return back()->with('success', 'Decision deleted successfully!');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user