Added call later, option to limit auto mail so for a client person email you can limit which decision activity will be send to that specific email and moved SMS packages from admin panel to default app view
This commit is contained in:
@@ -12,7 +12,6 @@
|
||||
use App\Models\SmsTemplate;
|
||||
use App\Services\Contact\PhoneSelector;
|
||||
use App\Services\Sms\SmsService;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
@@ -30,7 +29,7 @@ public function index(Request $request): Response
|
||||
->latest('id')
|
||||
->paginate($perPage);
|
||||
|
||||
return Inertia::render('Admin/Packages/Index', [
|
||||
return Inertia::render('Packages/Index', [
|
||||
'packages' => $packages,
|
||||
]);
|
||||
}
|
||||
@@ -70,7 +69,7 @@ public function create(Request $request): Response
|
||||
})
|
||||
->values();
|
||||
|
||||
return Inertia::render('Admin/Packages/Create', [
|
||||
return Inertia::render('Packages/Create', [
|
||||
'profiles' => $profiles,
|
||||
'senders' => $senders,
|
||||
'templates' => $templates,
|
||||
@@ -213,7 +212,7 @@ public function show(Package $package, SmsService $sms): Response
|
||||
}
|
||||
}
|
||||
|
||||
return Inertia::render('Admin/Packages/Show', [
|
||||
return Inertia::render('Packages/Show', [
|
||||
'package' => $package,
|
||||
'items' => $items,
|
||||
'preview' => $preview,
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\CallLater;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class CallLaterController extends Controller
|
||||
{
|
||||
public function index(Request $request): \Inertia\Response
|
||||
{
|
||||
$query = CallLater::query()
|
||||
->with([
|
||||
'clientCase.person',
|
||||
'contract',
|
||||
'user',
|
||||
'activity',
|
||||
])
|
||||
->whereNull('completed_at')
|
||||
->orderBy('call_back_at', 'asc');
|
||||
|
||||
if ($request->filled('date_from')) {
|
||||
$query->whereDate('call_back_at', '>=', $request->date_from);
|
||||
}
|
||||
if ($request->filled('date_to')) {
|
||||
$query->whereDate('call_back_at', '<=', $request->date_to);
|
||||
}
|
||||
if ($request->filled('search')) {
|
||||
$term = '%'.$request->search.'%';
|
||||
$query->whereHas('clientCase.person', function ($q) use ($term) {
|
||||
$q->where('first_name', 'ilike', $term)
|
||||
->orWhere('last_name', 'ilike', $term)
|
||||
->orWhere('full_name', 'ilike', $term)
|
||||
->orWhereRaw("CONCAT(first_name, ' ', last_name) ILIKE ?", [$term]);
|
||||
});
|
||||
}
|
||||
|
||||
$callLaters = $query->paginate(50)->withQueryString();
|
||||
|
||||
return Inertia::render('CallLaters/Index', [
|
||||
'callLaters' => $callLaters,
|
||||
'filters' => $request->only(['date_from', 'date_to', 'search']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function complete(CallLater $callLater): \Illuminate\Http\RedirectResponse
|
||||
{
|
||||
$callLater->update(['completed_at' => now()]);
|
||||
|
||||
return back()->with('success', 'Klic označen kot opravljen.');
|
||||
}
|
||||
}
|
||||
@@ -306,6 +306,7 @@ public function storeActivity(ClientCase $clientCase, Request $request)
|
||||
try {
|
||||
$attributes = $request->validate([
|
||||
'due_date' => 'nullable|date',
|
||||
'call_back_at' => 'nullable|date_format:Y-m-d H:i:s|after_or_equal:now',
|
||||
'amount' => 'nullable|decimal:0,4',
|
||||
'note' => 'nullable|string',
|
||||
'action_id' => 'exists:\App\Models\Action,id',
|
||||
@@ -326,14 +327,14 @@ public function storeActivity(ClientCase $clientCase, Request $request)
|
||||
|
||||
// Determine which contracts to process
|
||||
$contractIds = [];
|
||||
if ($createForAll && !empty($contractUuids)) {
|
||||
if ($createForAll && ! empty($contractUuids)) {
|
||||
// Get all contract IDs from the provided UUIDs
|
||||
$contracts = Contract::withTrashed()
|
||||
->whereIn('uuid', $contractUuids)
|
||||
->where('client_case_id', $clientCase->id)
|
||||
->get();
|
||||
$contractIds = $contracts->pluck('id')->toArray();
|
||||
} elseif (!empty($contractUuids) && isset($contractUuids[0])) {
|
||||
} elseif (! empty($contractUuids) && isset($contractUuids[0])) {
|
||||
// Single contract mode
|
||||
$contract = Contract::withTrashed()
|
||||
->where('uuid', $contractUuids[0])
|
||||
@@ -342,7 +343,7 @@ public function storeActivity(ClientCase $clientCase, Request $request)
|
||||
if ($contract) {
|
||||
$contractIds = [$contract->id];
|
||||
}
|
||||
} elseif (!empty($attributes['contract_uuid'])) {
|
||||
} elseif (! empty($attributes['contract_uuid'])) {
|
||||
// Legacy single contract_uuid support
|
||||
$contract = Contract::withTrashed()
|
||||
->where('uuid', $attributes['contract_uuid'])
|
||||
@@ -360,7 +361,7 @@ public function storeActivity(ClientCase $clientCase, Request $request)
|
||||
|
||||
$createdActivities = [];
|
||||
$sendFlag = (bool) ($attributes['send_auto_mail'] ?? true);
|
||||
|
||||
|
||||
// Disable auto mail if creating activities for multiple contracts
|
||||
if ($sendFlag && count($contractIds) > 1) {
|
||||
$sendFlag = false;
|
||||
@@ -371,6 +372,7 @@ public function storeActivity(ClientCase $clientCase, Request $request)
|
||||
// Create activity
|
||||
$row = $clientCase->activities()->create([
|
||||
'due_date' => $attributes['due_date'] ?? null,
|
||||
'call_back_at' => $attributes['call_back_at'] ?? null,
|
||||
'amount' => $attributes['amount'] ?? null,
|
||||
'note' => $attributes['note'] ?? null,
|
||||
'action_id' => $attributes['action_id'],
|
||||
@@ -417,29 +419,29 @@ public function storeActivity(ClientCase $clientCase, Request $request)
|
||||
->whereIn('id', $attachmentIds)
|
||||
->pluck('id');
|
||||
$validAttachmentIds = Document::query()
|
||||
->where('documentable_type', Contract::class)
|
||||
->where('documentable_id', $contractId)
|
||||
->whereIn('id', $attachmentIds)
|
||||
->pluck('id');
|
||||
->where('documentable_type', Contract::class)
|
||||
->where('documentable_id', $contractId)
|
||||
->whereIn('id', $attachmentIds)
|
||||
->pluck('id');
|
||||
}
|
||||
$result = app(\App\Services\AutoMailDispatcher::class)->maybeQueue($row, $sendFlag, [
|
||||
'attachment_ids' => $validAttachmentIds->all(),
|
||||
]);
|
||||
if (($result['skipped'] ?? null) === 'missing-contract' && $sendFlag) {
|
||||
// If template requires contract and user attempted to send, surface a validation message
|
||||
logger()->warning('Email not queued: required contract is missing for the selected template.');
|
||||
}
|
||||
if (($result['skipped'] ?? null) === 'no-recipients' && $sendFlag) {
|
||||
logger()->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());
|
||||
}
|
||||
$result = app(\App\Services\AutoMailDispatcher::class)->maybeQueue($row, $sendFlag, [
|
||||
'attachment_ids' => $validAttachmentIds->all(),
|
||||
]);
|
||||
if (($result['skipped'] ?? null) === 'missing-contract' && $sendFlag) {
|
||||
// If template requires contract and user attempted to send, surface a validation message
|
||||
logger()->warning('Email not queued: required contract is missing for the selected template.');
|
||||
}
|
||||
if (($result['skipped'] ?? null) === 'no-recipients' && $sendFlag) {
|
||||
logger()->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());
|
||||
}
|
||||
}
|
||||
|
||||
$activityCount = count($createdActivities);
|
||||
$successMessage = $activityCount > 1
|
||||
$successMessage = $activityCount > 1
|
||||
? "Successfully created {$activityCount} activities!"
|
||||
: 'Successfully created activity!';
|
||||
|
||||
@@ -867,6 +869,9 @@ public function show(ClientCase $clientCase)
|
||||
'decisions.emailTemplate' => function ($q) {
|
||||
$q->select('id', 'name', 'entity_types', 'allow_attachments');
|
||||
},
|
||||
'decisions.events' => function ($q) {
|
||||
$q->select('events.id', 'events.key', 'events.name');
|
||||
},
|
||||
])
|
||||
->get(['id', 'name', 'color_tag', 'segment_id']),
|
||||
'types' => $types,
|
||||
@@ -888,6 +893,7 @@ public function show(ClientCase $clientCase)
|
||||
->select(['id', 'name', 'content', 'allow_custom_body'])
|
||||
->orderBy('name')
|
||||
->get(),
|
||||
'auto_mail_decisions' => \App\Models\Decision::query()->where('auto_mail', true)->orderBy('name')->get(['id', 'name']),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1101,6 +1107,7 @@ public function archiveBatch(Request $request)
|
||||
|
||||
if (! $setting) {
|
||||
\Log::warning('No archive settings found for batch archive');
|
||||
|
||||
return back()->with('flash', [
|
||||
'error' => 'No archive settings found',
|
||||
]);
|
||||
@@ -1114,13 +1121,14 @@ public function archiveBatch(Request $request)
|
||||
foreach ($validated['contracts'] as $contractUuid) {
|
||||
try {
|
||||
$contract = Contract::where('uuid', $contractUuid)->firstOrFail();
|
||||
|
||||
|
||||
// Skip if contract is already archived (active = 0)
|
||||
if (!$contract->active) {
|
||||
if (! $contract->active) {
|
||||
$skippedCount++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
$clientCase = $contract->clientCase;
|
||||
|
||||
$context = [
|
||||
@@ -1207,8 +1215,8 @@ public function archiveBatch(Request $request)
|
||||
if ($skippedCount > 0) {
|
||||
$message .= ", skipped $skippedCount already archived";
|
||||
}
|
||||
$message .= ", " . count($errors) . " failed";
|
||||
|
||||
$message .= ', '.count($errors).' failed';
|
||||
|
||||
return back()->with('flash', [
|
||||
'error' => $message,
|
||||
'details' => $errors,
|
||||
@@ -1218,7 +1226,7 @@ public function archiveBatch(Request $request)
|
||||
$message = $reactivate
|
||||
? "Successfully reactivated $successCount contracts"
|
||||
: "Successfully archived $successCount contracts";
|
||||
|
||||
|
||||
if ($skippedCount > 0) {
|
||||
$message .= " ($skippedCount already archived)";
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ public function index(Client $client, Request $request)
|
||||
->where('person.full_name', 'ilike', '%'.$search.'%')
|
||||
->groupBy('clients.id');
|
||||
})
|
||||
//->where('clients.active', 1)
|
||||
// ->where('clients.active', 1)
|
||||
// Use LEFT JOINs for aggregated data to avoid subqueries
|
||||
->leftJoin('client_cases', 'client_cases.client_id', '=', 'clients.id')
|
||||
->leftJoin('contracts', function ($join) {
|
||||
@@ -71,6 +71,7 @@ public function show(Client $client, Request $request)
|
||||
|
||||
return Inertia::render('Client/Show', [
|
||||
'client' => $data,
|
||||
'auto_mail_decisions' => \App\Models\Decision::query()->where('auto_mail', true)->orderBy('name')->get(['id', 'name']),
|
||||
'client_cases' => $data->clientCases()
|
||||
->select('client_cases.*')
|
||||
->when($request->input('search'), function ($que, $search) {
|
||||
@@ -162,6 +163,7 @@ public function contracts(Client $client, Request $request)
|
||||
|
||||
return Inertia::render('Client/Contracts', [
|
||||
'client' => $data,
|
||||
'auto_mail_decisions' => \App\Models\Decision::query()->where('auto_mail', true)->orderBy('name')->get(['id', 'name']),
|
||||
'contracts' => $contractsQuery
|
||||
->paginate($perPage, ['*'], 'contracts_page', $pageNumber)
|
||||
->withQueryString(),
|
||||
@@ -175,7 +177,7 @@ public function exportContracts(ExportClientContractsRequest $request, Client $c
|
||||
{
|
||||
$data = $request->validated();
|
||||
$columns = array_values(array_unique($data['columns']));
|
||||
|
||||
|
||||
$from = $data['from'] ?? null;
|
||||
$to = $data['to'] ?? null;
|
||||
$search = $data['search'] ?? null;
|
||||
@@ -236,7 +238,7 @@ private function buildExportFilename(Client $client): string
|
||||
{
|
||||
$datePrefix = now()->format('dmy');
|
||||
$clientName = $this->slugify($client->person?->full_name ?? 'stranka');
|
||||
|
||||
|
||||
return sprintf('%s_%s-Pogodbe.xlsx', $datePrefix, $clientName);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\BankAccount;
|
||||
use App\Models\Person\Person;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@@ -27,9 +26,7 @@ public function update(Person $person, Request $request)
|
||||
$person->update($attributes);
|
||||
|
||||
return back()->with('success', 'Person updated')->with('flash_method', 'PUT');
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
public function createAddress(Person $person, Request $request)
|
||||
@@ -72,7 +69,7 @@ public function updateAddress(Person $person, int $address_id, Request $request)
|
||||
$address->update($attributes);
|
||||
|
||||
return back()->with('success', 'Address updated')->with('flash_method', 'PUT');
|
||||
|
||||
|
||||
}
|
||||
|
||||
public function deleteAddress(Person $person, int $address_id, Request $request)
|
||||
@@ -80,7 +77,6 @@ public function deleteAddress(Person $person, int $address_id, Request $request)
|
||||
$address = $person->addresses()->findOrFail($address_id);
|
||||
$address->delete(); // soft delete
|
||||
|
||||
|
||||
return back()->with('success', 'Address deleted')->with('flash_method', 'DELETE');
|
||||
}
|
||||
|
||||
@@ -142,8 +138,14 @@ public function createEmail(Person $person, Request $request)
|
||||
'verified_at' => 'nullable|date',
|
||||
'preferences' => 'nullable|array',
|
||||
'meta' => 'nullable|array',
|
||||
'decision_ids' => 'nullable|array',
|
||||
'decision_ids.*' => 'integer|exists:decisions,id',
|
||||
]);
|
||||
|
||||
$decisionIds = array_map('intval', $attributes['decision_ids'] ?? []);
|
||||
unset($attributes['decision_ids']);
|
||||
$attributes['preferences'] = array_merge($attributes['preferences'] ?? [], ['decision_ids' => $decisionIds]);
|
||||
|
||||
// Dedup: avoid duplicate email per person by value
|
||||
$email = $person->emails()->firstOrCreate([
|
||||
'value' => $attributes['value'],
|
||||
@@ -164,10 +166,16 @@ public function updateEmail(Person $person, int $email_id, Request $request)
|
||||
'verified_at' => 'nullable|date',
|
||||
'preferences' => 'nullable|array',
|
||||
'meta' => 'nullable|array',
|
||||
'decision_ids' => 'nullable|array',
|
||||
'decision_ids.*' => 'integer|exists:decisions,id',
|
||||
]);
|
||||
|
||||
$email = $person->emails()->findOrFail($email_id);
|
||||
|
||||
$decisionIds = array_map('intval', $attributes['decision_ids'] ?? []);
|
||||
unset($attributes['decision_ids']);
|
||||
$attributes['preferences'] = array_merge($email->preferences ?? [], $attributes['preferences'] ?? [], ['decision_ids' => $decisionIds]);
|
||||
|
||||
$email->update($attributes);
|
||||
|
||||
return back()->with('success', 'Email updated successfully')->with('flash_method', 'PUT');
|
||||
@@ -204,10 +212,8 @@ public function createTrr(Person $person, Request $request)
|
||||
// Create without dedup (IBAN may be null or vary); could dedup by IBAN if provided
|
||||
$trr = $person->bankAccounts()->create($attributes);
|
||||
|
||||
|
||||
return back()->with('success', 'TRR added successfully')->with('flash_method', 'POST');
|
||||
|
||||
|
||||
}
|
||||
|
||||
public function updateTrr(Person $person, int $trr_id, Request $request)
|
||||
@@ -238,8 +244,7 @@ public function deleteTrr(Person $person, int $trr_id, Request $request)
|
||||
$trr = $person->bankAccounts()->findOrFail($trr_id);
|
||||
$trr->delete();
|
||||
|
||||
|
||||
return back()->with('success', 'TRR deleted')->with('flash_method', 'DELETE');
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ class Activity extends Model
|
||||
|
||||
protected $fillable = [
|
||||
'due_date',
|
||||
'call_back_at',
|
||||
'amount',
|
||||
'note',
|
||||
'action_id',
|
||||
@@ -27,6 +28,13 @@ class Activity extends Model
|
||||
'client_case_id',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'call_back_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
protected $hidden = [
|
||||
'action_id',
|
||||
'decision_id',
|
||||
@@ -146,4 +154,9 @@ public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\User::class);
|
||||
}
|
||||
|
||||
public function callLaters(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(\App\Models\CallLater::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class CallLater extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'activity_id',
|
||||
'client_case_id',
|
||||
'contract_id',
|
||||
'user_id',
|
||||
'call_back_at',
|
||||
'completed_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'call_back_at' => 'datetime',
|
||||
'completed_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function activity(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Activity::class);
|
||||
}
|
||||
|
||||
public function clientCase(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ClientCase::class);
|
||||
}
|
||||
|
||||
public function contract(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Contract::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@@ -59,10 +59,23 @@ public function maybeQueue(Activity $activity, bool $sendFlag = true, array $opt
|
||||
// Resolve eligible recipients: client's person emails with receive_auto_mails = true
|
||||
$recipients = [];
|
||||
if ($client && $client->person) {
|
||||
$recipients = Email::query()
|
||||
$emails = Email::query()
|
||||
->where('person_id', $client->person->id)
|
||||
->where('is_active', true)
|
||||
->where('receive_auto_mails', true)
|
||||
->get(['value', 'preferences']);
|
||||
|
||||
$recipients = $emails
|
||||
->filter(function (Email $email) use ($decision): bool {
|
||||
$decisionIds = $email->preferences['decision_ids'] ?? [];
|
||||
|
||||
// Empty list means "all decisions" — always receive
|
||||
if (empty($decisionIds)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return in_array((int) $decision->id, array_map('intval', $decisionIds), true);
|
||||
})
|
||||
->pluck('value')
|
||||
->map(fn ($v) => strtolower(trim((string) $v)))
|
||||
->filter(fn ($v) => filter_var($v, FILTER_VALIDATE_EMAIL))
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\DecisionEvents\Handlers;
|
||||
|
||||
use App\Models\CallLater;
|
||||
use App\Services\DecisionEvents\Contracts\DecisionEventHandler;
|
||||
use App\Services\DecisionEvents\DecisionEventContext;
|
||||
|
||||
class CallLaterHandler implements DecisionEventHandler
|
||||
{
|
||||
public function handle(DecisionEventContext $context, array $config = []): void
|
||||
{
|
||||
$activity = $context->activity;
|
||||
|
||||
if (empty($activity->call_back_at)) {
|
||||
return;
|
||||
}
|
||||
|
||||
CallLater::create([
|
||||
'activity_id' => $activity->id,
|
||||
'client_case_id' => $activity->client_case_id,
|
||||
'contract_id' => $activity->contract_id,
|
||||
'user_id' => $activity->user_id,
|
||||
'call_back_at' => $activity->call_back_at,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ class Registry
|
||||
'add_segment' => AddSegmentHandler::class,
|
||||
'archive_contract' => \App\Services\DecisionEvents\Handlers\ArchiveContractHandler::class,
|
||||
'end_field_job' => \App\Services\DecisionEvents\Handlers\EndFieldJobHandler::class,
|
||||
'add_call_later' => \App\Services\DecisionEvents\Handlers\CallLaterHandler::class,
|
||||
];
|
||||
|
||||
public static function resolve(string $key): DecisionEventHandler
|
||||
|
||||
Reference in New Issue
Block a user