Decision now support auto mailing

This commit is contained in:
Simon Pocrnjič
2025-10-12 00:20:03 +02:00
parent 1b615163be
commit 3ab1c05fcc
33 changed files with 1862 additions and 548 deletions
@@ -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)
+32 -8
View File
@@ -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']),
+1 -1
View File
@@ -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 = [
+29 -32
View File
@@ -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']);
}
}
+16 -5
View File
@@ -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!');
}
}