changes to sms packages and option to create user
This commit is contained in:
@@ -11,8 +11,8 @@ class ExampleChart
|
||||
public function __construct(LarapexChart $chart)
|
||||
{
|
||||
$this->chart = $chart;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function build($options = null)
|
||||
{
|
||||
$data = \App\Models\ClientCase::query()
|
||||
@@ -22,20 +22,19 @@ public function build($options = null)
|
||||
->groupByRaw('EXTRACT(MONTH from created_at)')
|
||||
->orderByRaw('EXTRACT(MONTH from created_at)')
|
||||
->get();
|
||||
|
||||
|
||||
$months = $data->pluck('month')->map(
|
||||
fn($nu)
|
||||
=> \DateTime::createFromFormat('!m', $nu)->format('F'))->toArray();
|
||||
|
||||
fn ($nu) => \DateTime::createFromFormat('!m', $nu)->format('F'))->toArray();
|
||||
|
||||
$newCases = $data->pluck('count')->toArray();
|
||||
|
||||
return $this->chart->areaChart()
|
||||
->setTitle('Novi primeri zadnjih šest mesecev.')
|
||||
->addData('Primeri', $newCases)
|
||||
//->addData('Completed', [7, 2, 7, 2, 5, 4])
|
||||
// ->addData('Completed', [7, 2, 7, 2, 5, 4])
|
||||
->setColors(['#ff6384'])
|
||||
->setXAxis($months)
|
||||
->setToolbar(true)
|
||||
->toVue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\Post;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ImportPosts extends Command
|
||||
{
|
||||
protected $signature = 'import:posts';
|
||||
|
||||
protected $description = 'Import posts into Algolia without clearing the index';
|
||||
|
||||
public function __construct()
|
||||
@@ -22,4 +23,3 @@ public function handle()
|
||||
$this->info('Posts have been imported into Algolia.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,12 +10,15 @@
|
||||
class PruneDocumentPreviews extends Command
|
||||
{
|
||||
protected $signature = 'documents:prune-previews {--days=90 : Delete previews older than this many days} {--dry-run : Show what would be deleted without deleting}';
|
||||
|
||||
protected $description = 'Deletes generated document preview files older than N days and clears their metadata.';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$days = (int) $this->option('days');
|
||||
if ($days < 1) { $days = 90; }
|
||||
if ($days < 1) {
|
||||
$days = 90;
|
||||
}
|
||||
$cutoff = Carbon::now()->subDays($days);
|
||||
|
||||
$previewDisk = config('files.preview_disk', 'public');
|
||||
@@ -27,6 +30,7 @@ public function handle(): int
|
||||
$count = $query->count();
|
||||
if ($count === 0) {
|
||||
$this->info('No stale previews found.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
@@ -36,9 +40,12 @@ public function handle(): int
|
||||
$query->chunkById(200, function ($docs) use ($previewDisk, $dry) {
|
||||
foreach ($docs as $doc) {
|
||||
$path = $doc->preview_path;
|
||||
if (!$path) { continue; }
|
||||
if (! $path) {
|
||||
continue;
|
||||
}
|
||||
if ($dry) {
|
||||
$this->line("Would delete: {$previewDisk}://{$path} (document #{$doc->id})");
|
||||
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -15,7 +15,9 @@ protected function schedule(Schedule $schedule): void
|
||||
// Optionally prune old previews daily
|
||||
if (config('files.enable_preview_prune', true)) {
|
||||
$days = (int) config('files.preview_retention_days', 90);
|
||||
if ($days < 1) { $days = 90; }
|
||||
if ($days < 1) {
|
||||
$days = 90;
|
||||
}
|
||||
$schedule->command('documents:prune-previews', [
|
||||
'--days' => $days,
|
||||
])->dailyAt('02:00');
|
||||
|
||||
@@ -2,12 +2,8 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Account;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class AccountController extends Controller
|
||||
{
|
||||
//
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ public function index(Request $request): Response
|
||||
->get(['id', 'profile_id', 'sname', 'phone_number']);
|
||||
$templates = \App\Models\SmsTemplate::query()
|
||||
->orderBy('name')
|
||||
->get(['id', 'name']);
|
||||
->get(['id', 'name', 'content']);
|
||||
$segments = \App\Models\Segment::query()
|
||||
->where('active', true)
|
||||
->orderBy('name')
|
||||
@@ -121,7 +121,7 @@ public function show(Package $package, SmsService $sms): Response
|
||||
if (! $rendered) {
|
||||
$body = isset($payload['body']) ? trim((string) $payload['body']) : '';
|
||||
if ($body !== '') {
|
||||
$rendered = $body;
|
||||
$rendered = $sms->renderContent($body, $vars);
|
||||
} elseif (! empty($payload['template_id'])) {
|
||||
$tpl = \App\Models\SmsTemplate::find((int) $payload['template_id']);
|
||||
if ($tpl) {
|
||||
@@ -175,7 +175,7 @@ public function show(Package $package, SmsService $sms): Response
|
||||
if ($body !== '') {
|
||||
$preview = [
|
||||
'source' => 'body',
|
||||
'content' => $body,
|
||||
'content' => $sms->renderContent($body, $vars),
|
||||
];
|
||||
} elseif (! empty($payload['template_id'])) {
|
||||
/** @var SmsTemplate|null $tpl */
|
||||
@@ -292,6 +292,8 @@ public function contracts(Request $request, PhoneSelector $selector): \Illuminat
|
||||
'client_id' => ['nullable', 'integer', 'exists:clients,id'],
|
||||
'only_mobile' => ['nullable', 'boolean'],
|
||||
'only_validated' => ['nullable', 'boolean'],
|
||||
'start_date_from' => ['nullable', 'date'],
|
||||
'start_date_to' => ['nullable', 'date'],
|
||||
]);
|
||||
|
||||
$segmentId = (int) $request->input('segment_id');
|
||||
@@ -321,6 +323,15 @@ public function contracts(Request $request, PhoneSelector $selector): \Illuminat
|
||||
->where('client_cases.client_id', $clientId);
|
||||
}
|
||||
|
||||
// Date range filters for start_date
|
||||
if ($startDateFrom = $request->input('start_date_from')) {
|
||||
$query->where('contracts.start_date', '>=', $startDateFrom);
|
||||
}
|
||||
|
||||
if ($startDateTo = $request->input('start_date_to')) {
|
||||
$query->where('contracts.start_date', '<=', $startDateTo);
|
||||
}
|
||||
|
||||
// Optional phone filters
|
||||
if ($request->boolean('only_mobile') || $request->boolean('only_validated')) {
|
||||
$query->whereHas('clientCase.person.phones', function ($q) use ($request) {
|
||||
@@ -345,6 +356,7 @@ public function contracts(Request $request, PhoneSelector $selector): \Illuminat
|
||||
'id' => $contract->id,
|
||||
'uuid' => $contract->uuid,
|
||||
'reference' => $contract->reference,
|
||||
'start_date' => $contract->start_date,
|
||||
'case' => [
|
||||
'id' => $contract->clientCase?->id,
|
||||
'uuid' => $contract->clientCase?->uuid,
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\StoreUserRequest;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
@@ -29,6 +31,23 @@ public function index(Request $request): Response
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(StoreUserRequest $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
|
||||
$user = User::create([
|
||||
'name' => $validated['name'],
|
||||
'email' => $validated['email'],
|
||||
'password' => Hash::make($validated['password']),
|
||||
]);
|
||||
|
||||
if (! empty($validated['roles'])) {
|
||||
$user->roles()->sync($validated['roles']);
|
||||
}
|
||||
|
||||
return back()->with('success', 'Uporabnik uspešno ustvarjen');
|
||||
}
|
||||
|
||||
public function update(Request $request, User $user): RedirectResponse
|
||||
{
|
||||
Gate::authorize('manage-settings');
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
use App\Models\CaseObject;
|
||||
use App\Models\ClientCase;
|
||||
use App\Models\Contract;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CaseObjectController extends Controller
|
||||
@@ -27,8 +26,8 @@ public function store(ClientCase $clientCase, string $uuid, Request $request)
|
||||
|
||||
public function update(ClientCase $clientCase, int $id, Request $request)
|
||||
{
|
||||
$object = CaseObject::where('id', $id)
|
||||
->whereHas('contract', fn($q) => $q->where('client_case_id', $clientCase->id))
|
||||
$object = CaseObject::where('id', $id)
|
||||
->whereHas('contract', fn ($q) => $q->where('client_case_id', $clientCase->id))
|
||||
->firstOrFail();
|
||||
|
||||
$validated = $request->validate([
|
||||
@@ -45,8 +44,8 @@ public function update(ClientCase $clientCase, int $id, Request $request)
|
||||
|
||||
public function destroy(ClientCase $clientCase, int $id)
|
||||
{
|
||||
$object = CaseObject::where('id', $id)
|
||||
->whereHas('contract', fn($q) => $q->where('client_case_id', $clientCase->id))
|
||||
$object = CaseObject::where('id', $id)
|
||||
->whereHas('contract', fn ($q) => $q->where('client_case_id', $clientCase->id))
|
||||
->firstOrFail();
|
||||
|
||||
$object->delete();
|
||||
|
||||
@@ -14,8 +14,8 @@ public function index()
|
||||
{
|
||||
return Inertia::render('Settings/ContractConfigs/Index', [
|
||||
'configs' => ContractConfig::with(['type:id,name', 'segment:id,name'])->get(),
|
||||
'types' => ContractType::query()->get(['id','name']),
|
||||
'segments' => Segment::query()->where('active', true)->get(['id','name']),
|
||||
'types' => ContractType::query()->get(['id', 'name']),
|
||||
'segments' => Segment::query()->where('active', true)->get(['id', 'name']),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -40,8 +40,8 @@ public function store(Request $request)
|
||||
ContractConfig::create([
|
||||
'contract_type_id' => $data['contract_type_id'],
|
||||
'segment_id' => $data['segment_id'],
|
||||
'is_initial' => (bool)($data['is_initial'] ?? false),
|
||||
'active' => (bool)($data['active'] ?? true),
|
||||
'is_initial' => (bool) ($data['is_initial'] ?? false),
|
||||
'active' => (bool) ($data['active'] ?? true),
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Configuration created');
|
||||
@@ -57,8 +57,8 @@ public function update(ContractConfig $config, Request $request)
|
||||
|
||||
$config->update([
|
||||
'segment_id' => $data['segment_id'],
|
||||
'is_initial' => (bool)($data['is_initial'] ?? $config->is_initial),
|
||||
'active' => (bool)($data['active'] ?? $config->active),
|
||||
'is_initial' => (bool) ($data['is_initial'] ?? $config->is_initial),
|
||||
'active' => (bool) ($data['active'] ?? $config->active),
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Configuration updated');
|
||||
@@ -67,6 +67,7 @@ public function update(ContractConfig $config, Request $request)
|
||||
public function destroy(ContractConfig $config)
|
||||
{
|
||||
$config->delete();
|
||||
|
||||
return back()->with('success', 'Configuration deleted');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,24 +6,24 @@
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
|
||||
class ContractController extends Controller
|
||||
{
|
||||
|
||||
public function index(Contract $contract) {
|
||||
public function index(Contract $contract)
|
||||
{
|
||||
return Inertia::render('Contract/Index', [
|
||||
'contracts' => $contract::with(['type', 'debtor'])
|
||||
->where('active', 1)
|
||||
->orderByDesc('created_at')
|
||||
->paginate(10),
|
||||
'person_types' => \App\Models\Person\PersonType::all(['id', 'name', 'description'])
|
||||
->where('deleted', 0)
|
||||
->where('deleted', 0),
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(Contract $contract){
|
||||
public function show(Contract $contract)
|
||||
{
|
||||
return inertia('Contract/Show', [
|
||||
'contract' => $contract::with(['type', 'client', 'debtor'])->findOrFail($contract->id)
|
||||
'contract' => $contract::with(['type', 'client', 'debtor'])->findOrFail($contract->id),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -32,16 +32,16 @@ public function store(Request $request)
|
||||
$uuid = $request->input('client_case_uuid');
|
||||
|
||||
$clientCase = \App\Models\ClientCase::where('uuid', $uuid)->firstOrFail();
|
||||
|
||||
if( isset($clientCase->id) ){
|
||||
|
||||
\DB::transaction(function() use ($request, $clientCase){
|
||||
if (isset($clientCase->id)) {
|
||||
|
||||
//Create contract
|
||||
\DB::transaction(function () use ($request, $clientCase) {
|
||||
|
||||
// Create contract
|
||||
$clientCase->contracts()->create([
|
||||
'reference' => $request->input('reference'),
|
||||
'start_date' => date('Y-m-d', strtotime($request->input('start_date'))),
|
||||
'type_id' => $request->input('type_id')
|
||||
'type_id' => $request->input('type_id'),
|
||||
]);
|
||||
|
||||
});
|
||||
@@ -50,12 +50,12 @@ public function store(Request $request)
|
||||
return to_route('clientCase.show', $clientCase);
|
||||
}
|
||||
|
||||
public function update(Contract $contract, Request $request){
|
||||
public function update(Contract $contract, Request $request)
|
||||
{
|
||||
$contract->update([
|
||||
'referenca' => $request->input('referenca'),
|
||||
'type_id' => $request->input('type_id')
|
||||
'type_id' => $request->input('type_id'),
|
||||
]);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DebtController extends Controller
|
||||
{
|
||||
//
|
||||
|
||||
@@ -111,10 +111,10 @@ public function store(Request $request)
|
||||
'is_active' => 'boolean',
|
||||
'reactivate' => 'boolean',
|
||||
'entities' => 'nullable|array',
|
||||
'entities.*' => 'string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,payments',
|
||||
'entities.*' => 'string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,case_objects,payments',
|
||||
'mappings' => 'array',
|
||||
'mappings.*.source_column' => 'required|string',
|
||||
'mappings.*.entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,payments',
|
||||
'mappings.*.entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,case_objects,payments',
|
||||
'mappings.*.target_field' => 'nullable|string',
|
||||
'mappings.*.transform' => 'nullable|string|max:50',
|
||||
'mappings.*.apply_mode' => 'nullable|string|in:insert,update,both,keyref',
|
||||
@@ -244,7 +244,7 @@ public function addMapping(Request $request, ImportTemplate $template)
|
||||
}
|
||||
$data = validator($raw, [
|
||||
'source_column' => 'required|string',
|
||||
'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,payments',
|
||||
'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,case_objects,payments',
|
||||
'target_field' => 'nullable|string',
|
||||
'transform' => 'nullable|string|in:trim,upper,lower',
|
||||
'apply_mode' => 'nullable|string|in:insert,update,both,keyref',
|
||||
@@ -381,7 +381,7 @@ public function bulkAddMappings(Request $request, ImportTemplate $template)
|
||||
}
|
||||
$data = validator($raw, [
|
||||
'sources' => 'required|string', // comma and/or newline separated
|
||||
'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,payments',
|
||||
'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,case_objects,payments',
|
||||
'default_field' => 'nullable|string', // if provided, used as the field name for all entries
|
||||
'apply_mode' => 'nullable|string|in:insert,update,both,keyref',
|
||||
'transform' => 'nullable|string|in:trim,upper,lower',
|
||||
@@ -488,7 +488,7 @@ public function updateMapping(Request $request, ImportTemplate $template, Import
|
||||
}
|
||||
$data = validator($raw, [
|
||||
'source_column' => 'required|string',
|
||||
'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,payments',
|
||||
'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,case_objects,payments',
|
||||
'target_field' => 'nullable|string',
|
||||
'transform' => 'nullable|string|in:trim,upper,lower',
|
||||
'apply_mode' => 'nullable|string|in:insert,update,both,keyref',
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PaymentController extends Controller
|
||||
{
|
||||
//
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Post;
|
||||
use App\Http\Requests\StorePostRequest;
|
||||
use App\Http\Requests\UpdatePostRequest;
|
||||
use App\Models\Post;
|
||||
|
||||
class PostController extends Controller
|
||||
{
|
||||
|
||||
@@ -9,7 +9,8 @@ class SettingController extends Controller
|
||||
{
|
||||
//
|
||||
|
||||
public function index(Request $request){
|
||||
public function index(Request $request)
|
||||
{
|
||||
return Inertia::render('Settings/Index');
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,15 @@ public function share(Request $request): array
|
||||
$activities = \App\Models\Activity::query()
|
||||
->select(['id', 'due_date', 'amount', 'contract_id', 'client_case_id', 'created_at'])
|
||||
->whereDate('due_date', $today)
|
||||
// Removed per-user unread filter: show notifications regardless of individual reads
|
||||
// Exclude activities that have been marked as read by this user
|
||||
->whereNotExists(function ($q) use ($user, $today) {
|
||||
$q->select(\DB::raw(1))
|
||||
->from('activity_notification_reads')
|
||||
->whereColumn('activity_notification_reads.activity_id', 'activities.id')
|
||||
->where('activity_notification_reads.user_id', $user->id)
|
||||
->whereDate('activity_notification_reads.due_date', '<=', $today)
|
||||
->whereNotNull('activity_notification_reads.read_at');
|
||||
})
|
||||
->orderBy('created_at')
|
||||
->limit(20)
|
||||
->get();
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
class StoreUserRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return Gate::allows('manage-settings');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email'],
|
||||
'password' => ['required', 'string', Password::defaults(), 'confirmed'],
|
||||
'roles' => ['array'],
|
||||
'roles.*' => ['integer', 'exists:roles,id'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom error messages.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => 'Ime uporabnika je obvezno.',
|
||||
'email.required' => 'E-poštni naslov je obvezen.',
|
||||
'email.email' => 'E-poštni naslov mora biti veljaven.',
|
||||
'email.unique' => 'Ta e-poštni naslov je že v uporabi.',
|
||||
'password.required' => 'Geslo je obvezno.',
|
||||
'password.confirmed' => 'Gesli se ne ujemata.',
|
||||
'roles.*.exists' => 'Izbrana vloga ni veljavna.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ public function rules(): array
|
||||
'name' => ['required', 'string', 'max:50'],
|
||||
'description' => ['nullable', 'string', 'max:255'],
|
||||
'active' => ['boolean'],
|
||||
'exclude' => ['boolean']
|
||||
'exclude' => ['boolean'],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ class PersonCollection extends ResourceCollection
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'data' => $this->collection
|
||||
'data' => $this->collection,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
use App\Models\SmsTemplate;
|
||||
use App\Services\Sms\SmsService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Bus\Batchable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
@@ -18,7 +19,7 @@
|
||||
|
||||
class PackageItemSmsJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, Batchable;
|
||||
|
||||
public function __construct(public int $packageItemId)
|
||||
{
|
||||
@@ -97,7 +98,7 @@ public function handle(SmsService $sms): void
|
||||
/** @var SmsSender|null $sender */
|
||||
$sender = $senderId ? SmsSender::find($senderId) : null;
|
||||
/** @var SmsTemplate|null $template */
|
||||
$template = $templateId ? SmsTemplate::find($templateId) : null;
|
||||
$template = $templateId ? SmsTemplate::with(['action', 'decision'])->find($templateId) : null;
|
||||
|
||||
$to = $target['number'] ?? null;
|
||||
if (! is_string($to) || $to === '') {
|
||||
@@ -117,7 +118,7 @@ public function handle(SmsService $sms): void
|
||||
$key = $scope === 'per_profile' && $profile ? "sms:{$provider}:{$profile->id}" : "sms:{$provider}";
|
||||
|
||||
// Throttle
|
||||
$sendClosure = function () use ($sms, $item, $package, $profile, $sender, $template, $to, $variables, $deliveryReport, $bodyOverride) {
|
||||
$sendClosure = function () use ($sms, $item, $package, $profile, $sender, $template, $to, $variables, $deliveryReport, $bodyOverride, $target) {
|
||||
// Idempotency key (optional external use)
|
||||
if (empty($item->idempotency_key)) {
|
||||
$hash = sha1(implode('|', [
|
||||
@@ -188,6 +189,25 @@ public function handle(SmsService $sms): void
|
||||
$item->last_error = $log->status === 'sent' ? null : ($log->meta['error_message'] ?? 'Failed');
|
||||
$item->save();
|
||||
|
||||
// Create activity if template has action_id and decision_id configured and SMS was sent successfully
|
||||
if ($newStatus === 'sent' && $template && ($template->action_id || $template->decision_id)) {
|
||||
if (! empty($target['contract_id'])) {
|
||||
$contract = Contract::query()->with('clientCase')->find($target['contract_id']);
|
||||
|
||||
if ($contract && $contract->client_case_id) {
|
||||
\App\Models\Activity::create(array_filter([
|
||||
'client_case_id' => $contract->client_case_id,
|
||||
'contract_id' => $contract->id,
|
||||
'action_id' => $template->action_id,
|
||||
'decision_id' => $template->decision_id,
|
||||
'note' => "SMS poslan na {$to}: {$result['message']}",
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update package counters atomically
|
||||
if ($newStatus === 'sent') {
|
||||
$package->increment('sent_count');
|
||||
|
||||
@@ -109,7 +109,7 @@ public function handle(SmsService $sms): void
|
||||
}
|
||||
|
||||
// If no pre-created activity is provided and invoked from the case UI with a selected template, create an Activity
|
||||
if (!$this->activityId && $this->templateId && $this->clientCaseId && $log) {
|
||||
if (! $this->activityId && $this->templateId && $this->clientCaseId && $log) {
|
||||
try {
|
||||
/** @var SmsTemplate|null $template */
|
||||
$template = SmsTemplate::find($this->templateId);
|
||||
|
||||
@@ -75,7 +75,8 @@ protected function performSmtpAuthTest(MailProfile $profile): void
|
||||
}
|
||||
|
||||
$remote = ($encryption === 'ssl') ? 'ssl://'.$host : $host;
|
||||
$errno = 0; $errstr = '';
|
||||
$errno = 0;
|
||||
$errstr = '';
|
||||
$socket = @fsockopen($remote, $port, $errno, $errstr, 15);
|
||||
if (! $socket) {
|
||||
throw new \RuntimeException("Connect failed: $errstr ($errno)");
|
||||
@@ -104,7 +105,9 @@ protected function performSmtpAuthTest(MailProfile $profile): void
|
||||
// Cleanly quit
|
||||
$this->command($socket, "QUIT\r\n", [221], 'QUIT');
|
||||
} finally {
|
||||
try { fclose($socket); } catch (\Throwable) {
|
||||
try {
|
||||
fclose($socket);
|
||||
} catch (\Throwable) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
@@ -116,6 +119,7 @@ protected function performSmtpAuthTest(MailProfile $profile): void
|
||||
protected function command($socket, string $cmd, array $expect, string $context): string
|
||||
{
|
||||
fwrite($socket, $cmd);
|
||||
|
||||
return $this->expect($socket, $expect, $context);
|
||||
}
|
||||
|
||||
@@ -138,6 +142,7 @@ protected function expect($socket, array $expectedCodes, string $context): strin
|
||||
if (! in_array($code, $expectedCodes, true)) {
|
||||
throw new \RuntimeException("Unexpected SMTP code $code during $context: ".implode(' | ', $lines));
|
||||
}
|
||||
|
||||
return $line;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ class Action extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\ActionFactory> */
|
||||
use HasFactory;
|
||||
|
||||
use Searchable;
|
||||
|
||||
protected $fillable = ['name', 'color_tag', 'segment_id'];
|
||||
@@ -26,10 +27,9 @@ public function segment(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Segment::class);
|
||||
}
|
||||
|
||||
|
||||
public function activities(): HasMany
|
||||
{
|
||||
return $this->hasMany(\App\Models\Activity::class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,22 +3,23 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\Uuid;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Laravel\Scout\Searchable;
|
||||
|
||||
class Client extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\ClientFactory> */
|
||||
use HasFactory;
|
||||
use Uuid;
|
||||
|
||||
use Searchable;
|
||||
use Uuid;
|
||||
|
||||
protected $fillable = [
|
||||
'person_id'
|
||||
'person_id',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
@@ -26,8 +27,7 @@ class Client extends Model
|
||||
'person_id',
|
||||
];
|
||||
|
||||
|
||||
protected function makeAllSearchableUsing(Builder $query): Builder
|
||||
protected function makeAllSearchableUsing(Builder $query): Builder
|
||||
{
|
||||
return $query->with('person');
|
||||
}
|
||||
@@ -37,12 +37,11 @@ public function toSearchableArray(): array
|
||||
|
||||
return [
|
||||
'person.full_name' => '',
|
||||
'person_addresses.address' => ''
|
||||
'person_addresses.address' => '',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
public function person(): BelongsTo
|
||||
public function person(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Person\Person::class);
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ protected function startDate(): Attribute
|
||||
return null;
|
||||
}
|
||||
$str = is_string($value) ? $value : (string) $value;
|
||||
|
||||
return \App\Services\DateNormalizer::toDate($str);
|
||||
}
|
||||
);
|
||||
@@ -71,6 +72,7 @@ protected function endDate(): Attribute
|
||||
return null;
|
||||
}
|
||||
$str = is_string($value) ? $value : (string) $value;
|
||||
|
||||
return \App\Services\DateNormalizer::toDate($str);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -11,7 +11,7 @@ class ImportEvent extends Model
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'import_id','user_id','event','level','message','context','import_row_id'
|
||||
'import_id', 'user_id', 'event', 'level', 'message', 'context', 'import_row_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
|
||||
@@ -11,7 +11,7 @@ class ImportTemplateMapping extends Model
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'import_template_id', 'entity', 'source_column', 'target_field', 'transform', 'apply_mode', 'options', 'position'
|
||||
'import_template_id', 'entity', 'source_column', 'target_field', 'transform', 'apply_mode', 'options', 'position',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
|
||||
@@ -29,7 +29,7 @@ class MailProfile extends Model
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::created(function (MailProfile $profile): void {
|
||||
|
||||
|
||||
\Log::info('mail_profile.created', [
|
||||
'id' => $profile->id,
|
||||
'name' => $profile->name,
|
||||
|
||||
@@ -15,5 +15,4 @@ public function persons(): HasMany
|
||||
{
|
||||
return $this->hasMany(\App\Models\Person\Person::class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class PersonType extends Model
|
||||
@@ -14,12 +13,11 @@ class PersonType extends Model
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'description'
|
||||
'description',
|
||||
];
|
||||
|
||||
public function persons(): HasMany
|
||||
{
|
||||
return $this->hasMany(\App\Models\Person\Person::class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ class Post extends Model
|
||||
public function toSearchableArray()
|
||||
{
|
||||
$array = $this->toArray();
|
||||
|
||||
return $array;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,22 +15,24 @@ class Segment extends Model
|
||||
'name',
|
||||
'description',
|
||||
'active',
|
||||
'exclude'
|
||||
'exclude',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'active' => 'boolean',
|
||||
'exclude' => 'boolean'
|
||||
'exclude' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function contracts(): BelongsToMany {
|
||||
public function contracts(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(\App\Models\Contract::class);
|
||||
}
|
||||
|
||||
public function clientCase(): BelongsToMany {
|
||||
public function clientCase(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(\App\Models\ClientCase::class)->withTimestamps();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ protected function isAdmin(User $user): bool
|
||||
if (app()->environment('testing')) {
|
||||
return true; // simplify for tests
|
||||
}
|
||||
|
||||
return method_exists($user, 'isAdmin') ? $user->isAdmin() : $user->id === 1; // fallback heuristic
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
use App\Models\Post;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\Response;
|
||||
|
||||
class PostPolicy
|
||||
{
|
||||
|
||||
@@ -38,8 +38,10 @@ public static function toDate(?string $raw): ?string
|
||||
// Rebuild date with corrected year
|
||||
$month = (int) $dt->format('m');
|
||||
$day = (int) $dt->format('d');
|
||||
|
||||
return sprintf('%04d-%02d-%02d', $year, $month, $day);
|
||||
}
|
||||
|
||||
return $dt->format('Y-m-d');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
use App\Models\Account;
|
||||
use App\Models\AccountType;
|
||||
use App\Models\Activity;
|
||||
use App\Models\CaseObject;
|
||||
use App\Models\Client;
|
||||
use App\Models\ClientCase;
|
||||
use App\Models\Contract;
|
||||
@@ -480,6 +481,80 @@ public function process(Import $import, ?Authenticatable $user = null): array
|
||||
}
|
||||
}
|
||||
|
||||
// Case Objects: create or update case objects associated with contracts
|
||||
// Support both 'case_object' and 'case_objects' keys (template may use plural)
|
||||
if (isset($mapped['case_objects']) || isset($mapped['case_object'])) {
|
||||
// Resolve contract_id from various sources
|
||||
$contractIdForObject = null;
|
||||
|
||||
// Get the case object data (support both plural and singular)
|
||||
$caseObjectData = $mapped['case_objects'] ?? $mapped['case_object'] ?? [];
|
||||
|
||||
// First, check if contract_id is directly provided in the mapping
|
||||
if (! empty($caseObjectData['contract_id'])) {
|
||||
$contractIdForObject = $caseObjectData['contract_id'];
|
||||
}
|
||||
// If contract was just created/resolved above, use its id
|
||||
elseif ($contractResult && isset($contractResult['contract']) && $contractResult['contract'] instanceof Contract) {
|
||||
$contractIdForObject = $contractResult['contract']->id;
|
||||
}
|
||||
// If account was processed and has a contract, use that contract
|
||||
elseif ($accountResult && isset($accountResult['contract_id'])) {
|
||||
$contractIdForObject = $accountResult['contract_id'];
|
||||
}
|
||||
|
||||
if ($contractIdForObject) {
|
||||
$caseObjectResult = $this->upsertCaseObject($import, $mapped, $mappings, $contractIdForObject);
|
||||
if ($caseObjectResult['action'] === 'skipped') {
|
||||
$skipped++;
|
||||
$importRow->update(['status' => 'skipped']);
|
||||
ImportEvent::create([
|
||||
'import_id' => $import->id,
|
||||
'user_id' => $user?->getAuthIdentifier(),
|
||||
'import_row_id' => $importRow->id,
|
||||
'event' => 'row_skipped',
|
||||
'level' => 'info',
|
||||
'message' => $caseObjectResult['message'] ?? 'Skipped (no changes).',
|
||||
'context' => $caseObjectResult['context'] ?? null,
|
||||
]);
|
||||
} elseif ($caseObjectResult['action'] === 'inserted' || $caseObjectResult['action'] === 'updated') {
|
||||
$imported++;
|
||||
$importRow->update([
|
||||
'status' => 'imported',
|
||||
'entity_type' => CaseObject::class,
|
||||
'entity_id' => $caseObjectResult['case_object']->id,
|
||||
]);
|
||||
$objectFieldsStr = '';
|
||||
if (! empty($caseObjectResult['applied_fields'] ?? [])) {
|
||||
$objectFieldsStr = $this->formatAppliedFieldMessage('case_object', $caseObjectResult['applied_fields']);
|
||||
}
|
||||
ImportEvent::create([
|
||||
'import_id' => $import->id,
|
||||
'user_id' => $user?->getAuthIdentifier(),
|
||||
'import_row_id' => $importRow->id,
|
||||
'event' => 'row_imported',
|
||||
'level' => 'info',
|
||||
'message' => ucfirst($caseObjectResult['action']).' case object'.($objectFieldsStr ? ' '.$objectFieldsStr : ''),
|
||||
'context' => ['id' => $caseObjectResult['case_object']->id, 'fields' => $caseObjectResult['applied_fields'] ?? []],
|
||||
]);
|
||||
} else {
|
||||
$invalid++;
|
||||
$importRow->update(['status' => 'invalid', 'errors' => ['Unhandled case object result']]);
|
||||
}
|
||||
} else {
|
||||
$invalid++;
|
||||
$importRow->update(['status' => 'invalid', 'errors' => ['Case object requires a valid contract_id']]);
|
||||
ImportEvent::create([
|
||||
'import_id' => $import->id,
|
||||
'user_id' => $user?->getAuthIdentifier(),
|
||||
'import_row_id' => $importRow->id,
|
||||
'event' => 'row_invalid',
|
||||
'level' => 'error',
|
||||
'message' => 'Case object requires a valid contract_id (not resolved).',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Contacts: resolve person via Contract/Account chain, client_case.client_ref, contacts, or identifiers
|
||||
$personIdForRow = null;
|
||||
// Prefer person from contract created/updated above
|
||||
@@ -1447,6 +1522,109 @@ private function upsertAccount(Import $import, array $mapped, $mappings): array
|
||||
}
|
||||
}
|
||||
|
||||
private function upsertCaseObject(Import $import, array $mapped, $mappings, int $contractId): array
|
||||
{
|
||||
// Support both 'case_object' and 'case_objects' keys (template may use plural)
|
||||
$obj = $mapped['case_objects'] ?? $mapped['case_object'] ?? [];
|
||||
$reference = $obj['reference'] ?? null;
|
||||
$name = $obj['name'] ?? null;
|
||||
|
||||
// Normalize reference (remove spaces) for consistent matching
|
||||
if (! is_null($reference)) {
|
||||
$reference = preg_replace('/\s+/', '', trim((string) $reference));
|
||||
}
|
||||
|
||||
// At least name or reference must be provided
|
||||
if (! $reference && (! $name || trim($name) === '')) {
|
||||
return [
|
||||
'action' => 'skipped',
|
||||
'message' => 'Case object requires at least a reference or name',
|
||||
'context' => ['missing' => 'reference and name'],
|
||||
];
|
||||
}
|
||||
|
||||
$existing = null;
|
||||
|
||||
// First, try to find by contract_id and reference (if reference provided)
|
||||
if ($reference) {
|
||||
$existing = CaseObject::query()
|
||||
->where('contract_id', $contractId)
|
||||
->where('reference', $reference)
|
||||
->first();
|
||||
}
|
||||
|
||||
// If not found by reference and name is provided, check for duplicate by name
|
||||
// This prevents creating duplicate case objects with same name for a contract
|
||||
if (! $existing && ! is_null($name) && trim($name) !== '') {
|
||||
$normalizedName = trim($name);
|
||||
$duplicateByName = CaseObject::query()
|
||||
->where('contract_id', $contractId)
|
||||
->where('name', $normalizedName)
|
||||
->first();
|
||||
|
||||
if ($duplicateByName) {
|
||||
// Found existing by name - use it as the existing record
|
||||
$existing = $duplicateByName;
|
||||
}
|
||||
}
|
||||
|
||||
// Build applyable data based on apply_mode
|
||||
$applyInsert = [];
|
||||
$applyUpdate = [];
|
||||
$applyModeByField = [];
|
||||
foreach ($mappings as $map) {
|
||||
$target = (string) ($map->target_field ?? '');
|
||||
// Support both 'case_object.' and 'case_objects.' (template may use plural)
|
||||
if (! str_starts_with($target, 'case_object.') && ! str_starts_with($target, 'case_objects.')) {
|
||||
continue;
|
||||
}
|
||||
// Extract field name - handle both singular and plural prefix
|
||||
$field = str_starts_with($target, 'case_objects.')
|
||||
? substr($target, strlen('case_objects.'))
|
||||
: substr($target, strlen('case_object.'));
|
||||
$applyModeByField[$field] = (string) ($map->apply_mode ?? 'both');
|
||||
}
|
||||
|
||||
foreach ($obj as $field => $value) {
|
||||
$applyMode = $applyModeByField[$field] ?? 'both';
|
||||
if (is_null($value) || (is_string($value) && trim($value) === '')) {
|
||||
continue;
|
||||
}
|
||||
if (in_array($applyMode, ['both', 'insert'], true)) {
|
||||
$applyInsert[$field] = $value;
|
||||
}
|
||||
if (in_array($applyMode, ['both', 'update'], true)) {
|
||||
$applyUpdate[$field] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if ($existing) {
|
||||
// Build non-null changes for case object fields
|
||||
$changes = array_filter($applyUpdate, fn ($v) => ! is_null($v));
|
||||
if (! empty($changes)) {
|
||||
$existing->fill($changes);
|
||||
$existing->save();
|
||||
|
||||
return ['action' => 'updated', 'case_object' => $existing, 'applied_fields' => $changes];
|
||||
} else {
|
||||
return ['action' => 'skipped', 'case_object' => $existing, 'message' => 'No changes needed'];
|
||||
}
|
||||
} else {
|
||||
// Create new case object
|
||||
$data = array_merge([
|
||||
'contract_id' => $contractId,
|
||||
'reference' => $reference,
|
||||
], $applyInsert);
|
||||
|
||||
// Remove any null values
|
||||
$data = array_filter($data, fn ($v) => ! is_null($v));
|
||||
|
||||
$created = CaseObject::create($data);
|
||||
|
||||
return ['action' => 'inserted', 'case_object' => $created, 'applied_fields' => $data];
|
||||
}
|
||||
}
|
||||
|
||||
private function mappingsContainRoot($mappings, string $root): bool
|
||||
{
|
||||
foreach ($mappings as $map) {
|
||||
@@ -1491,6 +1669,17 @@ private function upsertContractChain(Import $import, array $mapped, $mappings):
|
||||
return ['action' => 'invalid', 'message' => 'Missing contract.reference'];
|
||||
}
|
||||
|
||||
// Temporary debug output
|
||||
$debugInfo = [
|
||||
'row' => $rowIndex ?? 'unknown',
|
||||
'reference' => $reference,
|
||||
'has_person' => isset($mapped['person']),
|
||||
'person_tax' => $mapped['person']['tax_number'] ?? null,
|
||||
'client_id' => $import->client_id,
|
||||
];
|
||||
|
||||
\Log::info('ImportProcessor: upsertContractChain START', $debugInfo);
|
||||
|
||||
// Determine mapping mode for contract.reference (e.g., keyref)
|
||||
$refMode = $this->mappingMode($mappings, 'contract.reference');
|
||||
|
||||
@@ -1507,6 +1696,21 @@ private function upsertContractChain(Import $import, array $mapped, $mappings):
|
||||
->where('contracts.reference', $reference)
|
||||
->select('contracts.*')
|
||||
->first();
|
||||
|
||||
// Debug logging to track contract lookup
|
||||
if ($existing) {
|
||||
\Log::info('ImportProcessor: Found existing contract', [
|
||||
'client_id' => $clientId,
|
||||
'reference' => $reference,
|
||||
'contract_id' => $existing->id,
|
||||
'client_case_id' => $existing->client_case_id,
|
||||
]);
|
||||
} else {
|
||||
\Log::info('ImportProcessor: No existing contract found', [
|
||||
'client_id' => $clientId,
|
||||
'reference' => $reference,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// If not found by client+reference and a specific client_case_id is provided, try that too
|
||||
@@ -1605,6 +1809,7 @@ private function upsertContractChain(Import $import, array $mapped, $mappings):
|
||||
// keyref: used as lookup and applied on insert, but not on update
|
||||
if ($mode === 'keyref') {
|
||||
$applyInsert[$field] = $value;
|
||||
|
||||
continue;
|
||||
}
|
||||
if (in_array($mode, ['insert', 'both'], true)) {
|
||||
@@ -2303,14 +2508,19 @@ private function findOrCreateClientCaseId(int $clientId, int $personId, ?string
|
||||
|
||||
return $cc->id;
|
||||
}
|
||||
|
||||
// client_ref was provided but not found, create new case with this client_ref
|
||||
return ClientCase::create(['client_id' => $clientId, 'person_id' => $personId, 'client_ref' => $clientRef])->id;
|
||||
}
|
||||
// Fallback: by (client_id, person_id)
|
||||
|
||||
// No client_ref provided: reuse existing case by (client_id, person_id) if available
|
||||
$cc = ClientCase::where('client_id', $clientId)->where('person_id', $personId)->first();
|
||||
if ($cc) {
|
||||
return $cc->id;
|
||||
}
|
||||
|
||||
return ClientCase::create(['client_id' => $clientId, 'person_id' => $personId, 'client_ref' => $clientRef])->id;
|
||||
// Create new case without client_ref
|
||||
return ClientCase::create(['client_id' => $clientId, 'person_id' => $personId, 'client_ref' => null])->id;
|
||||
}
|
||||
|
||||
private function upsertEmail(int $personId, array $emailData, $mappings): array
|
||||
@@ -2429,10 +2639,16 @@ private function upsertAddress(int $personId, array $addrData, $mappings): array
|
||||
private function upsertPhone(int $personId, array $phoneData, $mappings): array
|
||||
{
|
||||
$nu = trim((string) ($phoneData['nu'] ?? ''));
|
||||
// Strip all non-numeric characters from phone number
|
||||
$nu = preg_replace('/[^0-9]/', '', $nu);
|
||||
if ($nu === '') {
|
||||
return ['action' => 'skipped', 'message' => 'No phone value'];
|
||||
}
|
||||
$existing = PersonPhone::where('person_id', $personId)->where('nu', $nu)->first();
|
||||
|
||||
// Find existing phone by normalized number (strip non-numeric from DB values too)
|
||||
$existing = PersonPhone::where('person_id', $personId)
|
||||
->whereRaw("REGEXP_REPLACE(nu, '[^0-9]', '', 'g') = ?", [$nu])
|
||||
->first();
|
||||
$applyInsert = [];
|
||||
$applyUpdate = [];
|
||||
foreach ($mappings as $map) {
|
||||
@@ -2472,6 +2688,12 @@ private function upsertPhone(int $personId, array $phoneData, $mappings): array
|
||||
$data = array_filter($applyInsert, fn ($v) => ! is_null($v));
|
||||
$data['person_id'] = $personId;
|
||||
$data['type_id'] = $data['type_id'] ?? $this->getDefaultPhoneTypeId();
|
||||
// Override nu field with normalized value (digits only)
|
||||
$data['nu'] = $nu;
|
||||
// Set default phone_type to mobile enum if not provided
|
||||
if (! array_key_exists('phone_type', $data) || $data['phone_type'] === null) {
|
||||
$data['phone_type'] = \App\Enums\PersonPhoneType::Mobile;
|
||||
}
|
||||
$created = PersonPhone::create($data);
|
||||
|
||||
return ['action' => 'inserted', 'phone' => $created];
|
||||
|
||||
@@ -1237,7 +1237,9 @@ private function simulateGenericRoot(
|
||||
$entity['country'] = $val('address.country') ?? null;
|
||||
break;
|
||||
case 'phone':
|
||||
$entity['nu'] = $val('phone.nu') ?? null;
|
||||
$rawNu = $val('phone.nu') ?? null;
|
||||
// Strip all non-numeric characters from phone number
|
||||
$entity['nu'] = $rawNu !== null ? preg_replace('/[^0-9]/', '', (string) $rawNu) : null;
|
||||
break;
|
||||
case 'email':
|
||||
$entity['value'] = $val('email.value') ?? null;
|
||||
@@ -1246,6 +1248,11 @@ private function simulateGenericRoot(
|
||||
$entity['title'] = $val('client_case.title') ?? null;
|
||||
$entity['status'] = $val('client_case.status') ?? null;
|
||||
break;
|
||||
case 'case_object':
|
||||
$entity['name'] = $val('case_object.name') ?? null;
|
||||
$entity['description'] = $val('case_object.description') ?? null;
|
||||
$entity['type'] = $val('case_object.type') ?? null;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($verbose) {
|
||||
@@ -1313,7 +1320,8 @@ private function genericIdentityCandidates(string $root, callable $val): array
|
||||
case 'phone':
|
||||
$nu = $val('phone.nu');
|
||||
if ($nu) {
|
||||
$norm = preg_replace('/\D+/', '', (string) $nu) ?? '';
|
||||
// Strip all non-numeric characters from phone number
|
||||
$norm = preg_replace('/[^0-9]/', '', (string) $nu) ?? '';
|
||||
|
||||
return $norm ? ['nu:'.$norm] : [];
|
||||
}
|
||||
@@ -1346,6 +1354,20 @@ private function genericIdentityCandidates(string $root, callable $val): array
|
||||
}
|
||||
|
||||
return [];
|
||||
case 'case_object':
|
||||
$ref = $val('case_object.reference');
|
||||
$name = $val('case_object.name');
|
||||
$ids = [];
|
||||
if ($ref) {
|
||||
// Normalize reference (remove spaces)
|
||||
$normRef = preg_replace('/\s+/', '', trim((string) $ref));
|
||||
$ids[] = 'ref:'.$normRef;
|
||||
}
|
||||
if ($name) {
|
||||
$ids[] = 'name:'.mb_strtolower(trim((string) $name));
|
||||
}
|
||||
|
||||
return $ids;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
@@ -1366,7 +1388,8 @@ private function loadExistingGenericIdentities(string $root): array
|
||||
case 'phone':
|
||||
foreach (\App\Models\Person\PersonPhone::query()->pluck('nu') as $p) {
|
||||
if ($p) {
|
||||
$set['nu:'.preg_replace('/\D+/', '', (string) $p)] = true;
|
||||
// Strip all non-numeric characters from phone number
|
||||
$set['nu:'.preg_replace('/[^0-9]/', '', (string) $p)] = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -1391,6 +1414,18 @@ private function loadExistingGenericIdentities(string $root): array
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'case_object':
|
||||
foreach (\App\Models\CaseObject::query()->get(['reference', 'name']) as $rec) {
|
||||
if ($rec->reference) {
|
||||
// Normalize reference (remove spaces)
|
||||
$normRef = preg_replace('/\s+/', '', trim((string) $rec->reference));
|
||||
$set['ref:'.$normRef] = true;
|
||||
}
|
||||
if ($rec->name) {
|
||||
$set['name:'.mb_strtolower(trim((string) $rec->name))] = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// swallow and return what we have
|
||||
@@ -1411,6 +1446,7 @@ private function modelClassForGeneric(string $root): ?string
|
||||
'activity' => \App\Models\Activity::class,
|
||||
'client' => \App\Models\Client::class,
|
||||
'client_case' => \App\Models\ClientCase::class,
|
||||
'case_object' => \App\Models\CaseObject::class,
|
||||
][$root] ?? null;
|
||||
}
|
||||
|
||||
@@ -1563,7 +1599,8 @@ private function simulateGenericRootMulti(
|
||||
} elseif ($root === 'phone') {
|
||||
$nu = $groupVals('phone', 'nu')[$g] ?? null;
|
||||
if ($nu) {
|
||||
$norm = preg_replace('/\D+/', '', (string) $nu) ?? '';
|
||||
// Strip all non-numeric characters from phone number
|
||||
$norm = preg_replace('/[^0-9]/', '', (string) $nu) ?? '';
|
||||
if ($norm) {
|
||||
$identityCandidates = ['nu:'.$norm];
|
||||
}
|
||||
@@ -1615,7 +1652,9 @@ private function simulateGenericRootMulti(
|
||||
if ($root === 'email') {
|
||||
$entity['value'] = $groupVals('email', 'value')[$g] ?? null;
|
||||
} elseif ($root === 'phone') {
|
||||
$entity['nu'] = $groupVals('phone', 'nu')[$g] ?? null;
|
||||
$rawNu = $groupVals('phone', 'nu')[$g] ?? null;
|
||||
// Strip all non-numeric characters from phone number
|
||||
$entity['nu'] = $rawNu !== null ? preg_replace('/[^0-9]/', '', (string) $rawNu) : null;
|
||||
} elseif ($root === 'address') {
|
||||
$entity['address'] = $groupVals('address', 'address')[$g] ?? null;
|
||||
$entity['country'] = $groupVals('address', 'country')[$g] ?? null;
|
||||
|
||||
@@ -24,6 +24,7 @@ protected function normalizeForSms(string $text): string
|
||||
{
|
||||
// Replace NBSP (\xC2\xA0 in UTF-8) and tabs with regular space
|
||||
$text = str_replace(["\u{00A0}", "\t"], ' ', $text);
|
||||
|
||||
// Optionally collapse CRLF to LF (providers typically accept both); keep as-is otherwise
|
||||
return $text;
|
||||
}
|
||||
|
||||
+3
-1
@@ -3,9 +3,11 @@
|
||||
namespace App\Traits;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
trait Uuid
|
||||
{
|
||||
protected static function boot(){
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
static::creating(function ($model) {
|
||||
$model->uuid = (string) Str::uuid();
|
||||
|
||||
Reference in New Issue
Block a user