production #1
|
|
@ -311,11 +311,14 @@ 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',
|
||||
'phone_view' => 'nullable|boolean',
|
||||
'send_auto_mail' => 'sometimes|boolean',
|
||||
'attachment_document_ids' => 'sometimes|array',
|
||||
'attachment_document_ids.*' => 'integer',
|
||||
]);
|
||||
|
||||
$isPhoneView = $attributes['phone_view'] ?? false;
|
||||
|
||||
// Map contract_uuid to contract_id within the same client case, if provided
|
||||
$contractId = null;
|
||||
if (! empty($attributes['contract_uuid'])) {
|
||||
|
|
@ -338,10 +341,23 @@ public function storeActivity(ClientCase $clientCase, Request $request)
|
|||
'decision_id' => $attributes['decision_id'],
|
||||
'contract_id' => $contractId,
|
||||
]);
|
||||
/*foreach ($activity->decision->events as $e) {
|
||||
$class = '\\App\\Events\\' . $e->name;
|
||||
event(new $class($clientCase));
|
||||
}*/
|
||||
|
||||
if ($isPhoneView && $contractId) {
|
||||
$fieldJob = $contract->fieldJobs()
|
||||
->whereNull('completed_at')
|
||||
->whereNull('cancelled_at')
|
||||
->where('assigned_user_id', \Auth::id())
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
|
||||
if ($fieldJob) {
|
||||
$fieldJob->update([
|
||||
'added_activity' => true,
|
||||
'last_activity' => $row->created_at,
|
||||
]);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
logger()->info('Activity successfully inserted', $attributes);
|
||||
|
||||
|
|
@ -356,8 +372,8 @@ public function storeActivity(ClientCase $clientCase, Request $request)
|
|||
->values();
|
||||
$validAttachmentIds = collect();
|
||||
if ($attachmentIds->isNotEmpty() && $contractId) {
|
||||
$validAttachmentIds = \App\Models\Document::query()
|
||||
->where('documentable_type', \App\Models\Contract::class)
|
||||
$validAttachmentIds = Document::query()
|
||||
->where('documentable_type', Contract::class)
|
||||
->where('documentable_id', $contractId)
|
||||
->whereIn('id', $attachmentIds)
|
||||
->pluck('id');
|
||||
|
|
@ -902,178 +918,115 @@ public function archiveContract(ClientCase $clientCase, string $uuid, Request $r
|
|||
{
|
||||
$contract = Contract::query()->where('uuid', $uuid)->firstOrFail();
|
||||
if ($contract->client_case_id !== $clientCase->id) {
|
||||
\Log::warning('Contract not found uuid: {uuid}', ['uuid' => $uuid]);
|
||||
abort(404);
|
||||
}
|
||||
$reactivateRequested = (bool) $request->boolean('reactivate');
|
||||
// Determine applicable settings based on intent (archive vs reactivate)
|
||||
if ($reactivateRequested) {
|
||||
$latestReactivate = \App\Models\ArchiveSetting::query()
|
||||
->where('enabled', true)
|
||||
->where('reactivate', true)
|
||||
->whereIn('strategy', ['immediate', 'manual'])
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
if (! $latestReactivate) {
|
||||
return back()->with('warning', __('contracts.reactivate_not_allowed'));
|
||||
}
|
||||
$settings = collect([$latestReactivate]);
|
||||
$hasReactivateRule = true;
|
||||
} else {
|
||||
$settings = \App\Models\ArchiveSetting::query()
|
||||
->where('enabled', true)
|
||||
->whereIn('strategy', ['immediate', 'manual'])
|
||||
->where(function ($q) { // exclude reactivate-only rules from archive run
|
||||
$q->whereNull('reactivate')->orWhere('reactivate', false);
|
||||
})
|
||||
->get();
|
||||
if ($settings->isEmpty()) {
|
||||
return back()->with('warning', __('contracts.no_archive_settings'));
|
||||
}
|
||||
$hasReactivateRule = false;
|
||||
|
||||
$attr = $request->validate([
|
||||
'reactivate' => 'boolean',
|
||||
]);
|
||||
|
||||
$reactivate = $attr['reactivate'] ?? false;
|
||||
|
||||
$setting = \App\Models\ArchiveSetting::query()
|
||||
->where('enabled', true)
|
||||
->whereIn('strategy', ['immediate', 'manual'])
|
||||
->where('reactivate', $reactivate)
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
|
||||
if (! $setting->exists()) {
|
||||
\Log::warning('No archive settings found!');
|
||||
|
||||
return back()->with('warning', 'No settings found');
|
||||
}
|
||||
|
||||
// Service archive executor
|
||||
$executor = app(\App\Services\Archiving\ArchiveExecutor::class);
|
||||
$result = null;
|
||||
|
||||
$context = [
|
||||
'contract_id' => $contract->id,
|
||||
'client_case_id' => $clientCase->id,
|
||||
'account_id' => $contract->account->id ?? null,
|
||||
];
|
||||
if ($contract->account) {
|
||||
$context['account_id'] = $contract->account->id;
|
||||
|
||||
try {
|
||||
$result = $executor->executeSetting($setting, $context, \Auth::id());
|
||||
} catch (Exception $e) {
|
||||
\Log::error('There was an error executing ArchiveExecutor::executeSetting {msg}', ['msg' => $e->getMessage()]);
|
||||
|
||||
return back()->with('warning', 'Something went wrong!');
|
||||
}
|
||||
|
||||
$overall = [];
|
||||
$hadAnyEffect = false;
|
||||
foreach ($settings as $setting) {
|
||||
|
||||
$res = $executor->executeSetting($setting, $context, optional($request->user())->id);
|
||||
foreach ($res as $table => $count) {
|
||||
$overall[$table] = ($overall[$table] ?? 0) + $count;
|
||||
if ($count > 0) {
|
||||
$hadAnyEffect = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($reactivateRequested && $hasReactivateRule) {
|
||||
// Reactivation path: ensure contract becomes active and soft-delete cleared.
|
||||
if ($contract->active == 0 || $contract->deleted_at) {
|
||||
$contract->forceFill(['active' => 1, 'deleted_at' => null])->save();
|
||||
$overall['contracts_reactivated'] = ($overall['contracts_reactivated'] ?? 0) + 1;
|
||||
$hadAnyEffect = true;
|
||||
}
|
||||
} else {
|
||||
// Ensure the contract itself is archived even if rule conditions would have excluded it
|
||||
if (! empty($contract->getAttributes()) && $contract->active) {
|
||||
if (! array_key_exists('contracts', $overall)) {
|
||||
$contract->update(['active' => 0]);
|
||||
$overall['contracts'] = ($overall['contracts'] ?? 0) + 1;
|
||||
} else {
|
||||
$contract->refresh();
|
||||
}
|
||||
$hadAnyEffect = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Create an Activity record logging this archive if an action or decision is tied to any setting
|
||||
if ($hadAnyEffect) {
|
||||
$activitySetting = $settings->first(fn ($s) => ! is_null($s->action_id) || ! is_null($s->decision_id));
|
||||
if ($activitySetting) {
|
||||
try {
|
||||
if ($reactivateRequested) {
|
||||
$note = 'Ponovna aktivacija pogodba '.$contract->reference;
|
||||
} else {
|
||||
$noteKey = 'contracts.archived_activity_note';
|
||||
$note = __($noteKey, ['reference' => $contract->reference]);
|
||||
if ($note === $noteKey) {
|
||||
$note = \Illuminate\Support\Facades\Lang::get($noteKey, ['reference' => $contract->reference], 'sl');
|
||||
}
|
||||
}
|
||||
try {
|
||||
\DB::transaction(function () use ($contract, $clientCase, $setting, $reactivate) {
|
||||
// Create an Activity record logging this archive if an action or decision is tied to any setting
|
||||
if ($setting->action_id && $setting->decision_id) {
|
||||
$activityData = [
|
||||
'client_case_id' => $clientCase->id,
|
||||
'action_id' => $activitySetting->action_id,
|
||||
'decision_id' => $activitySetting->decision_id,
|
||||
'note' => $note,
|
||||
'active' => 1,
|
||||
'user_id' => optional($request->user())->id,
|
||||
'action_id' => $setting->action_id,
|
||||
'decision_id' => $setting->decision_id,
|
||||
'note' => ($reactivate)
|
||||
? "Ponovno aktivirana pogodba $contract->reference"
|
||||
: "Arhivirana pogodba $contract->reference",
|
||||
];
|
||||
if ($reactivateRequested) {
|
||||
// Attach the contract_id when reactivated as per requirement
|
||||
$activityData['contract_id'] = $contract->id;
|
||||
|
||||
try {
|
||||
\App\Models\Activity::create($activityData);
|
||||
} catch (Exception $e) {
|
||||
\Log::warning('Activity could not be created!');
|
||||
}
|
||||
\App\Models\Activity::create($activityData);
|
||||
} catch (\Throwable $e) {
|
||||
logger()->warning('Failed to create archive/reactivate activity', [
|
||||
'error' => $e->getMessage(),
|
||||
'contract_id' => $contract->id,
|
||||
'setting_id' => optional($activitySetting)->id,
|
||||
'reactivate' => $reactivateRequested,
|
||||
]);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
// If any archive setting specifies a segment_id, move the contract to that segment (archive bucket)
|
||||
$segmentSetting = $settings->first(fn ($s) => ! is_null($s->segment_id)); // for reactivation this is the single reactivation setting if segment specified
|
||||
if ($segmentSetting && $segmentSetting->segment_id) {
|
||||
try {
|
||||
$segmentId = $segmentSetting->segment_id;
|
||||
\DB::transaction(function () use ($contract, $segmentId, $clientCase) {
|
||||
// Ensure the segment is attached to the client case (activate if previously inactive)
|
||||
$casePivot = \DB::table('client_case_segment')
|
||||
->where('client_case_id', $clientCase->id)
|
||||
->where('segment_id', $segmentId)
|
||||
->first();
|
||||
if (! $casePivot) {
|
||||
\DB::table('client_case_segment')->insert([
|
||||
'client_case_id' => $clientCase->id,
|
||||
'segment_id' => $segmentId,
|
||||
|
||||
// If any archive setting specifies a segment_id, move the contract to that segment (archive bucket)
|
||||
if ($setting->segment_id) {
|
||||
$segmentId = $setting->segment_id;
|
||||
|
||||
$contract->segments()
|
||||
->allRelatedIds()
|
||||
->map(fn (int $val, int|string $key) => $contract->segments()->updateExistingPivot($val, [
|
||||
'active' => false,
|
||||
'updated_at' => now(),
|
||||
])
|
||||
);
|
||||
|
||||
if ($contract->attachedSegments()->find($segmentId)->pluck('id')->isNotEmpty()) {
|
||||
$contract->attachedSegments()->updateExistingPivot($segmentId, [
|
||||
'active' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
} elseif (! $casePivot->active) {
|
||||
\DB::table('client_case_segment')
|
||||
->where('id', $casePivot->id)
|
||||
->update(['active' => true, 'updated_at' => now()]);
|
||||
}
|
||||
|
||||
// Deactivate all current active contract segments
|
||||
\DB::table('contract_segment')
|
||||
->where('contract_id', $contract->id)
|
||||
->where('active', true)
|
||||
->update(['active' => false, 'updated_at' => now()]);
|
||||
|
||||
// Attach or activate the archive segment for this contract
|
||||
$existing = \DB::table('contract_segment')
|
||||
->where('contract_id', $contract->id)
|
||||
->where('segment_id', $segmentId)
|
||||
->first();
|
||||
if ($existing) {
|
||||
\DB::table('contract_segment')
|
||||
->where('id', $existing->id)
|
||||
->update(['active' => true, 'updated_at' => now()]);
|
||||
} else {
|
||||
\DB::table('contract_segment')->insert([
|
||||
'contract_id' => $contract->id,
|
||||
'segment_id' => $segmentId,
|
||||
'active' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
$contract->segments()->attach(
|
||||
$segmentId,
|
||||
[
|
||||
'active' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
logger()->warning('Failed to move contract to archive segment', [
|
||||
'error' => $e->getMessage(),
|
||||
'contract_id' => $contract->id,
|
||||
'segment_id' => $segmentSetting->segment_id,
|
||||
'setting_id' => $segmentSetting->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$contract->fieldJobs()
|
||||
->whereNull('completed_at')
|
||||
->whereNull('cancelled_at')
|
||||
->update([
|
||||
'cancelled_at' => date('Y-m-d'),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
});
|
||||
} catch (Exception $e) {
|
||||
\Log::warning('Something went wrong with inserting / updating archive setting partials!');
|
||||
|
||||
return back()->with('warning', 'Something went wrong!');
|
||||
}
|
||||
|
||||
$message = $reactivateRequested ? __('contracts.reactivated') : __('contracts.archived');
|
||||
|
||||
return back()->with('success', $message)->with('flash_method', 'PATCH');
|
||||
return back()->with('success', $reactivate
|
||||
? __('contracts.reactivated')
|
||||
: __('contracts.archived')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -78,169 +78,81 @@ public function completedToday(Request $request)
|
|||
public function showCase(\App\Models\ClientCase $clientCase, Request $request)
|
||||
{
|
||||
$userId = $request->user()->id;
|
||||
$completedMode = (bool) $request->boolean('completed');
|
||||
$completedMode = $request->boolean('completed');
|
||||
|
||||
// Eager load client case with person details
|
||||
$case = \App\Models\ClientCase::query()
|
||||
->with(['person' => fn ($q) => $q->with(['addresses', 'phones', 'emails', 'bankAccounts'])])
|
||||
->findOrFail($clientCase->id);
|
||||
// Eager load case with person details
|
||||
$case = $clientCase->load('person.addresses', 'person.phones', 'person.emails', 'person.bankAccounts');
|
||||
|
||||
// Determine contracts of this case relevant to the current user
|
||||
// - Normal mode: contracts assigned to me and still active (not completed/cancelled)
|
||||
// - Completed mode (?completed=1): contracts where my field job was completed today
|
||||
if ($completedMode) {
|
||||
$start = now()->startOfDay();
|
||||
$end = now()->endOfDay();
|
||||
$contractIds = FieldJob::query()
|
||||
->where('assigned_user_id', $userId)
|
||||
->whereNull('cancelled_at')
|
||||
->whereBetween('completed_at', [$start, $end])
|
||||
->whereHas('contract', fn ($q) => $q->where('client_case_id', $case->id))
|
||||
->pluck('contract_id')
|
||||
->unique()
|
||||
->values();
|
||||
} else {
|
||||
$contractIds = FieldJob::query()
|
||||
->where('assigned_user_id', $userId)
|
||||
->whereNull('completed_at')
|
||||
->whereNull('cancelled_at')
|
||||
->whereHas('contract', fn ($q) => $q->where('client_case_id', $case->id))
|
||||
->pluck('contract_id')
|
||||
->unique()
|
||||
->values();
|
||||
}
|
||||
// Query contracts based on field jobs
|
||||
$contractsQuery = FieldJob::query()
|
||||
->where('assigned_user_id', $userId)
|
||||
->whereHas('contract', fn ($q) => $q->where('client_case_id', $case->id))
|
||||
->when($completedMode,
|
||||
fn ($q) => $q->whereNull('cancelled_at')->whereBetween('completed_at', [now()->startOfDay(), now()->endOfDay()]),
|
||||
fn ($q) => $q->whereNull('completed_at')->whereNull('cancelled_at')
|
||||
);
|
||||
|
||||
// Get contracts with relationships
|
||||
$contracts = \App\Models\Contract::query()
|
||||
->where('client_case_id', $case->id)
|
||||
->whereIn('id', $contractIds)
|
||||
->with(['type:id,name', 'account'])
|
||||
->whereIn('id', $contractsQuery->pluck('contract_id')->unique())
|
||||
->with(['type:id,name', 'account', 'latestObject'])
|
||||
->orderByDesc('created_at')
|
||||
->get();
|
||||
|
||||
// Attach latest object (if any) to each contract as last_object for display
|
||||
if ($contracts->isNotEmpty()) {
|
||||
$byId = $contracts->keyBy('id');
|
||||
$latestObjects = \App\Models\CaseObject::query()
|
||||
->whereIn('contract_id', $byId->keys())
|
||||
->whereNull('deleted_at')
|
||||
->select('id', 'reference', 'name', 'description', 'type', 'contract_id', 'created_at')
|
||||
->orderByDesc('created_at')
|
||||
->get()
|
||||
->groupBy('contract_id')
|
||||
->map(function ($group) {
|
||||
return $group->first();
|
||||
});
|
||||
|
||||
foreach ($latestObjects as $cid => $obj) {
|
||||
if (isset($byId[$cid])) {
|
||||
$byId[$cid]->setAttribute('last_object', $obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build merged documents: case documents + documents of assigned contracts
|
||||
$contractRefMap = [];
|
||||
foreach ($contracts as $c) {
|
||||
$contractRefMap[$c->id] = $c->reference;
|
||||
}
|
||||
|
||||
$contractDocs = \App\Models\Document::query()
|
||||
->where('documentable_type', \App\Models\Contract::class)
|
||||
->whereIn('documentable_id', $contractIds)
|
||||
// Build merged documents
|
||||
$documents = $case->documents()
|
||||
->orderByDesc('created_at')
|
||||
->get()
|
||||
->map(function ($d) use ($contractRefMap) {
|
||||
$arr = $d->toArray();
|
||||
$arr['contract_reference'] = $contractRefMap[$d->documentable_id] ?? null;
|
||||
$arr['documentable_type'] = \App\Models\Contract::class;
|
||||
$arr['contract_uuid'] = optional(\App\Models\Contract::withTrashed()->find($d->documentable_id))->uuid;
|
||||
|
||||
return $arr;
|
||||
});
|
||||
|
||||
$caseDocs = $case->documents()->orderByDesc('created_at')->get()->map(function ($d) use ($case) {
|
||||
$arr = $d->toArray();
|
||||
$arr['documentable_type'] = \App\Models\ClientCase::class;
|
||||
$arr['client_case_uuid'] = $case->uuid;
|
||||
|
||||
return $arr;
|
||||
});
|
||||
|
||||
$documents = $caseDocs->concat($contractDocs)->sortByDesc('created_at')->values();
|
||||
|
||||
// Provide minimal types for PersonInfoGrid
|
||||
$types = [
|
||||
'address_types' => $this->referenceCache->getAddressTypes(),
|
||||
'phone_types' => $this->referenceCache->getPhoneTypes(),
|
||||
];
|
||||
|
||||
// Case activities (compact for phone): latest 20 with relations
|
||||
$activities = $case->activities()
|
||||
->with(['action', 'decision', 'contract:id,uuid,reference', 'user:id,name'])
|
||||
->orderByDesc('created_at')
|
||||
->limit(20)
|
||||
->get()
|
||||
->map(function ($a) {
|
||||
$a->setAttribute('user_name', optional($a->user)->name);
|
||||
|
||||
return $a;
|
||||
});
|
||||
|
||||
// Determine segment filters from FieldJobSettings for this case/user context
|
||||
$settingIds = FieldJob::query()
|
||||
->where('assigned_user_id', $userId)
|
||||
->whereHas('contract', fn ($q) => $q->where('client_case_id', $case->id))
|
||||
->when(
|
||||
$completedMode,
|
||||
function ($q) {
|
||||
$q->whereNull('cancelled_at')
|
||||
->whereBetween('completed_at', [now()->startOfDay(), now()->endOfDay()]);
|
||||
},
|
||||
function ($q) {
|
||||
$q->whereNull('completed_at')->whereNull('cancelled_at');
|
||||
}
|
||||
->map(fn ($d) => array_merge($d->toArray(), [
|
||||
'documentable_type' => \App\Models\ClientCase::class,
|
||||
'client_case_uuid' => $case->uuid,
|
||||
]))
|
||||
->concat(
|
||||
\App\Models\Document::query()
|
||||
->where('documentable_type', \App\Models\Contract::class)
|
||||
->whereIn('documentable_id', $contracts->pluck('id'))
|
||||
->with('documentable:id,uuid,reference')
|
||||
->orderByDesc('created_at')
|
||||
->get()
|
||||
->map(fn ($d) => array_merge($d->toArray(), [
|
||||
'contract_reference' => $d->documentable?->reference,
|
||||
'contract_uuid' => $d->documentable?->uuid,
|
||||
]))
|
||||
)
|
||||
->pluck('field_job_setting_id')
|
||||
->filter()
|
||||
->unique()
|
||||
->sortByDesc('created_at')
|
||||
->values();
|
||||
|
||||
$segmentIds = collect();
|
||||
if ($settingIds->isNotEmpty()) {
|
||||
$segmentIds = \App\Models\FieldJobSetting::query()
|
||||
->whereIn('id', $settingIds)
|
||||
->pluck('segment_id')
|
||||
->filter()
|
||||
->unique()
|
||||
->values();
|
||||
}
|
||||
|
||||
// Filter actions and their decisions by the derived segment ids (decisions.segment_id)
|
||||
$actions = \App\Models\Action::query()
|
||||
->when($segmentIds->isNotEmpty(), function ($q) use ($segmentIds) {
|
||||
// Filter actions by their segment_id matching the FieldJobSetting segment(s)
|
||||
$q->whereIn('segment_id', $segmentIds);
|
||||
})
|
||||
->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', 'allow_attachments');
|
||||
},
|
||||
])
|
||||
->get(['id', 'name', 'color_tag', 'segment_id']);
|
||||
// Get segment IDs for filtering actions
|
||||
$segmentIds = \App\Models\FieldJobSetting::query()
|
||||
->whereIn('id', $contractsQuery->pluck('field_job_setting_id')->filter()->unique())
|
||||
->pluck('segment_id')
|
||||
->filter()
|
||||
->unique();
|
||||
|
||||
return Inertia::render('Phone/Case/Index', [
|
||||
'client' => $case->client()->with('person', fn ($q) => $q->with(['addresses', 'phones', 'emails', 'bankAccounts']))->firstOrFail(),
|
||||
'client' => $case->client->load('person.addresses', 'person.phones', 'person.emails', 'person.bankAccounts'),
|
||||
'client_case' => $case,
|
||||
'contracts' => $contracts,
|
||||
'documents' => $documents,
|
||||
'types' => $types,
|
||||
'account_types' => $this->referenceCache->getAccountTypes(),
|
||||
// Provide decisions (filtered by segment) with linked email template metadata (entity_types, allow_attachments)
|
||||
'actions' => $actions,
|
||||
'activities' => $activities,
|
||||
'types' => [
|
||||
'address_types' => \App\Models\Person\AddressType::all(),
|
||||
'phone_types' => \App\Models\Person\PhoneType::all(),
|
||||
],
|
||||
'account_types' => \App\Models\AccountType::all(),
|
||||
'actions' => \App\Models\Action::query()
|
||||
->when($segmentIds->isNotEmpty(), fn ($q) => $q->whereIn('segment_id', $segmentIds))
|
||||
->with([
|
||||
'decisions:id,name,color_tag,auto_mail,email_template_id',
|
||||
'decisions.emailTemplate:id,name,entity_types,allow_attachments',
|
||||
])
|
||||
->get(['id', 'name', 'color_tag', 'segment_id']),
|
||||
'activities' => $case->activities()
|
||||
->with(['action', 'decision', 'contract:id,uuid,reference', 'user:id,name'])
|
||||
->orderByDesc('created_at')
|
||||
->limit(20)
|
||||
->get()
|
||||
->map(fn ($a) => $a->setAttribute('user_name', $a->user?->name)),
|
||||
'completed_mode' => $completedMode,
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,6 +112,11 @@ public function segments(): BelongsToMany
|
|||
->wherePivot('active', true);
|
||||
}
|
||||
|
||||
public function attachedSegments(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(\App\Models\Segment::class);
|
||||
}
|
||||
|
||||
public function account(): HasOne
|
||||
{
|
||||
// Use latestOfMany to always surface newest account snapshot if multiple exist.
|
||||
|
|
@ -130,6 +135,18 @@ public function documents(): MorphMany
|
|||
return $this->morphMany(\App\Models\Document::class, 'documentable');
|
||||
}
|
||||
|
||||
public function fieldJobs(): HasMany
|
||||
{
|
||||
return $this->hasMany(\App\Models\FieldJob::class);
|
||||
}
|
||||
|
||||
public function latestObject(): HasOne
|
||||
{
|
||||
return $this->hasOne(\App\Models\CaseObject::class)
|
||||
->whereNull('deleted_at')
|
||||
->latest();
|
||||
}
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::created(function (Contract $contract): void {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ class FieldJob extends Model
|
|||
'priority',
|
||||
'notes',
|
||||
'address_snapshot ',
|
||||
'last_activity',
|
||||
'added_activity'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
|
|
@ -31,6 +33,8 @@ class FieldJob extends Model
|
|||
'completed_at' => 'datetime',
|
||||
'cancelled_at' => 'datetime',
|
||||
'priority' => 'boolean',
|
||||
'last_activity' => 'datetime',
|
||||
'added_activity' => 'boolean',
|
||||
'address_snapshot ' => 'array',
|
||||
];
|
||||
|
||||
|
|
@ -90,7 +94,8 @@ public function user(): BelongsTo
|
|||
|
||||
public function contract(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Contract::class, 'contract_id');
|
||||
return $this->belongsTo(Contract::class, 'contract_id')
|
||||
->where('active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -69,6 +69,8 @@ public function executeSetting(ArchiveSetting $setting, ?array $context = null,
|
|||
$entities = $flat;
|
||||
}
|
||||
|
||||
// dd($entities);
|
||||
|
||||
foreach ($entities as $entityDef) {
|
||||
$rawTable = $entityDef['table'] ?? null;
|
||||
if (! $rawTable) {
|
||||
|
|
@ -97,7 +99,7 @@ public function executeSetting(ArchiveSetting $setting, ?array $context = null,
|
|||
// Process in batches to avoid locking large tables
|
||||
while (true) {
|
||||
$query = DB::table($table)->whereNull('deleted_at');
|
||||
if (Schema::hasColumn($table, 'active')) {
|
||||
if (Schema::hasColumn($table, 'active') && ! $reactivate) {
|
||||
$query->where('active', 1);
|
||||
}
|
||||
// Apply context filters or chain derived filters
|
||||
|
|
|
|||
|
|
@ -25,14 +25,17 @@
|
|||
use App\Models\Person\PersonType;
|
||||
use App\Models\Person\PhoneType;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class ImportProcessor
|
||||
{
|
||||
/**
|
||||
* Track contracts that already existed and were matched during history imports.
|
||||
*
|
||||
* @var array<int,bool>
|
||||
*/
|
||||
private array $historyFoundContractIds = [];
|
||||
|
|
@ -180,10 +183,7 @@ public function process(Import $import, ?Authenticatable $user = null): array
|
|||
}
|
||||
// Preflight: warn if any mapped source columns are not present in the header (exact match)
|
||||
$headerSet = [];
|
||||
foreach ($header as $h) {
|
||||
$headerSet[$h] = true;
|
||||
}
|
||||
$missingSources = [];
|
||||
// Regex validation removed per request; rely on basic length/placeholder checks only
|
||||
foreach ($mappings as $map) {
|
||||
$src = (string) ($map->source_column ?? '');
|
||||
if ($src !== '' && ! array_key_exists($src, $headerSet)) {
|
||||
|
|
@ -216,7 +216,28 @@ public function process(Import $import, ?Authenticatable $user = null): array
|
|||
|
||||
if ($isPg) {
|
||||
// Establish a savepoint so a failing row does not poison the whole transaction
|
||||
DB::statement('SAVEPOINT import_row_'.$rowNum);
|
||||
try {
|
||||
DB::statement('SAVEPOINT import_row_'.$rowNum);
|
||||
} catch (\Throwable $se) {
|
||||
Log::error('Import savepoint_failed', [
|
||||
'import_id' => $import->id,
|
||||
'row_number' => $rowNum,
|
||||
'exception' => $this->exceptionContext($se),
|
||||
]);
|
||||
ImportEvent::create([
|
||||
'import_id' => $import->id,
|
||||
'user_id' => $user?->getAuthIdentifier(),
|
||||
'event' => 'savepoint_failed',
|
||||
'level' => 'error',
|
||||
'message' => 'Failed to create savepoint; transaction already aborted.',
|
||||
'context' => [
|
||||
'row_number' => $rowNum,
|
||||
'exception' => $this->exceptionContext($se),
|
||||
],
|
||||
]);
|
||||
|
||||
throw $se; // abort import so root cause surfaces
|
||||
}
|
||||
}
|
||||
|
||||
// Scope variables per row so they aren't reused after exception
|
||||
|
|
@ -1067,13 +1088,38 @@ public function process(Import $import, ?Authenticatable $user = null): array
|
|||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$rollbackFailed = false;
|
||||
$rollbackError = null;
|
||||
if ($isPg) {
|
||||
// Roll back only this row's work
|
||||
try {
|
||||
DB::statement('ROLLBACK TO SAVEPOINT import_row_'.$rowNum);
|
||||
} catch (\Throwable $ignored) { /* noop */
|
||||
} catch (\Throwable $ignored) {
|
||||
$rollbackFailed = true;
|
||||
$rollbackError = $ignored;
|
||||
}
|
||||
}
|
||||
if ($rollbackFailed) {
|
||||
Log::error('Import row_rollback_failed', [
|
||||
'import_id' => $import->id,
|
||||
'row_number' => $rowNum,
|
||||
'exception' => $this->exceptionContext($rollbackError ?? $e),
|
||||
]);
|
||||
// Abort the whole import if we cannot rollback to the row savepoint (transaction is poisoned)
|
||||
ImportEvent::create([
|
||||
'import_id' => $import->id,
|
||||
'user_id' => $user?->getAuthIdentifier(),
|
||||
'event' => 'row_rollback_failed',
|
||||
'level' => 'error',
|
||||
'message' => 'Rollback to savepoint failed; aborting import.',
|
||||
'context' => [
|
||||
'row_number' => $rowNum,
|
||||
'exception' => $this->exceptionContext($rollbackError ?? $e),
|
||||
],
|
||||
]);
|
||||
|
||||
throw $rollbackError ?? $e;
|
||||
}
|
||||
// Ensure importRow exists for logging if failure happened before its creation
|
||||
if (! $importRow) {
|
||||
try {
|
||||
|
|
@ -1100,6 +1146,12 @@ public function process(Import $import, ?Authenticatable $user = null): array
|
|||
}
|
||||
$failedRows[] = $rowNum;
|
||||
$invalid++;
|
||||
Log::error('Import row_exception', [
|
||||
'import_id' => $import->id,
|
||||
'row_number' => $rowNum,
|
||||
'exception' => $this->exceptionContext($e),
|
||||
'raw_preview' => isset($rawAssoc) ? $this->buildRawDataPreview($rawAssoc) : [],
|
||||
]);
|
||||
try {
|
||||
ImportEvent::create([
|
||||
'import_id' => $import->id,
|
||||
|
|
@ -1117,6 +1169,12 @@ public function process(Import $import, ?Authenticatable $user = null): array
|
|||
],
|
||||
]);
|
||||
} catch (\Throwable $evtErr) {
|
||||
Log::error('Import row_exception_event_failed', [
|
||||
'import_id' => $import->id,
|
||||
'row_number' => $rowNum,
|
||||
'exception' => $this->exceptionContext($evtErr),
|
||||
'original_exception' => $this->exceptionContext($e),
|
||||
]);
|
||||
// Swallow secondary failure to ensure loop continues
|
||||
}
|
||||
|
||||
|
|
@ -1189,12 +1247,17 @@ public function process(Import $import, ?Authenticatable $user = null): array
|
|||
// Mark failed and log after rollback (so no partial writes persist)
|
||||
$import->refresh();
|
||||
$import->update(['status' => 'failed', 'failed_at' => now()]);
|
||||
Log::error('Import processing_failed', [
|
||||
'import_id' => $import->id,
|
||||
'exception' => $this->exceptionContext($e),
|
||||
]);
|
||||
ImportEvent::create([
|
||||
'import_id' => $import->id,
|
||||
'user_id' => $user?->getAuthIdentifier(),
|
||||
'event' => 'processing_failed',
|
||||
'level' => 'error',
|
||||
'message' => $e->getMessage(),
|
||||
'message' => $this->safeErrorMessage($e->getMessage()),
|
||||
'context' => $this->exceptionContext($e),
|
||||
]);
|
||||
|
||||
return ['ok' => false, 'status' => 'failed', 'error' => $e->getMessage()];
|
||||
|
|
@ -1920,6 +1983,8 @@ private function upsertActivity(Import $import, array $mapped, $mappings, ?array
|
|||
} elseif (in_array($field, ['action_id', 'decision_id', 'user_id'], true)) {
|
||||
$normalized = is_null($value) ? null : (int) $value;
|
||||
} elseif (is_string($normalized)) {
|
||||
// Clean invalid UTF-8 sequences from string fields
|
||||
$normalized = mb_convert_encoding($normalized, 'UTF-8', 'UTF-8');
|
||||
$normalized = trim($normalized);
|
||||
}
|
||||
if (in_array($applyMode, ['both', 'insert'], true)) {
|
||||
|
|
@ -2009,7 +2074,7 @@ private function upsertActivity(Import $import, array $mapped, $mappings, ?array
|
|||
}
|
||||
|
||||
$data = array_filter($applyInsert, fn ($v) => ! is_null($v));
|
||||
$activityModel = new Activity();
|
||||
$activityModel = new Activity;
|
||||
$activityModel->forceFill($data);
|
||||
if (array_key_exists('created_at', $data)) {
|
||||
// Preserve provided timestamps by disabling automatic timestamps for this save
|
||||
|
|
@ -2233,6 +2298,7 @@ private function upsertContractChain(Import $import, array $mapped, $mappings, b
|
|||
if ($existing) {
|
||||
if ($historyImport) {
|
||||
$this->historyFoundContractIds[$existing->id] = true;
|
||||
|
||||
return ['action' => 'skipped_history', 'contract' => $existing, 'message' => 'Existing contract left unchanged (history import)'];
|
||||
}
|
||||
// 1) Prepare contract field changes (non-null)
|
||||
|
|
@ -2475,13 +2541,57 @@ private function safeErrorMessage(string $msg): string
|
|||
}
|
||||
// Fallback strip invalid bytes
|
||||
$msg = @iconv('UTF-8', 'UTF-8//IGNORE', $msg) ?: $msg;
|
||||
if (strlen($msg) > 500) {
|
||||
$msg = substr($msg, 0, 497).'...';
|
||||
// Use mb_strlen and mb_substr for UTF-8 safety
|
||||
if (mb_strlen($msg) > 500) {
|
||||
$msg = mb_substr($msg, 0, 497).'...';
|
||||
}
|
||||
|
||||
return $msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract structured exception details for logging.
|
||||
*/
|
||||
private function exceptionContext(\Throwable $e): array
|
||||
{
|
||||
$ctx = [
|
||||
'exception' => get_class($e),
|
||||
'message' => $this->safeErrorMessage($e->getMessage()),
|
||||
'code' => $e->getCode(),
|
||||
'file' => $e->getFile().':'.$e->getLine(),
|
||||
];
|
||||
|
||||
if (method_exists($e, 'getPrevious') && $e->getPrevious()) {
|
||||
$prev = $e->getPrevious();
|
||||
$ctx['previous'] = [
|
||||
'exception' => get_class($prev),
|
||||
'message' => $this->safeErrorMessage($prev->getMessage()),
|
||||
'code' => $prev->getCode(),
|
||||
'file' => $prev->getFile().':'.$prev->getLine(),
|
||||
];
|
||||
}
|
||||
|
||||
if ($e instanceof QueryException) {
|
||||
$ctx['sql'] = $e->getSql();
|
||||
$ctx['bindings'] = $e->getBindings();
|
||||
$info = $e->errorInfo ?? null;
|
||||
if (is_array($info)) {
|
||||
$ctx['sqlstate'] = $info[0] ?? null;
|
||||
$ctx['driver_error_code'] = $info[1] ?? null;
|
||||
$ctx['driver_error_message'] = $info[2] ?? null;
|
||||
}
|
||||
} elseif (property_exists($e, 'errorInfo')) {
|
||||
$info = $e->errorInfo;
|
||||
if (is_array($info)) {
|
||||
$ctx['sqlstate'] = $info[0] ?? null;
|
||||
$ctx['driver_error_code'] = $info[1] ?? null;
|
||||
$ctx['driver_error_message'] = $info[2] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
return $ctx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a trimmed raw data preview (first 8 columns, truncated values) for logging.
|
||||
*/
|
||||
|
|
@ -2522,9 +2632,9 @@ private function formatAppliedFieldMessage(string $root, array $fields): string
|
|||
} else {
|
||||
$disp = method_exists($v, '__toString') ? (string) $v : gettype($v);
|
||||
}
|
||||
// Truncate very long values for log safety
|
||||
if (strlen($disp) > 60) {
|
||||
$disp = substr($disp, 0, 57).'...';
|
||||
// Truncate very long values for log safety (use mb_substr for UTF-8 safety)
|
||||
if (mb_strlen($disp) > 60) {
|
||||
$disp = mb_substr($disp, 0, 57).'...';
|
||||
}
|
||||
$parts[] = $k.'='.$disp;
|
||||
}
|
||||
|
|
@ -3000,8 +3110,10 @@ private function upsertEmail(int $personId, array $emailData, $mappings): array
|
|||
private function upsertAddress(int $personId, array $addrData, $mappings): array
|
||||
{
|
||||
$addressLine = trim((string) ($addrData['address'] ?? ''));
|
||||
// Normalize whitespace
|
||||
// Normalize whitespace: collapse multiples and tighten around separators
|
||||
$addressLine = preg_replace('/\s+/', ' ', $addressLine);
|
||||
$addressLine = preg_replace('/\s*([,;\/])\s*/', '$1 ', $addressLine);
|
||||
$addressLine = trim($addressLine);
|
||||
// Skip common placeholders or missing values
|
||||
if ($addressLine === '' || $addressLine === '0' || strcasecmp($addressLine, '#N/A') === 0 || preg_match('/^(#?n\/?a|na|null|none)$/i', $addressLine)) {
|
||||
return ['action' => 'skipped', 'message' => 'No address value'];
|
||||
|
|
@ -3009,15 +3121,21 @@ private function upsertAddress(int $personId, array $addrData, $mappings): array
|
|||
if (mb_strlen($addressLine) < 3) {
|
||||
return ['action' => 'skipped', 'message' => 'Invalid address value'];
|
||||
}
|
||||
// Allow only basic address characters to avoid noisy special chars
|
||||
if (! preg_match('/^[A-Za-z0-9\\s\\.,\\-\\/\\#\\\'"\\(\\)&]+$/', $addressLine)) {
|
||||
return ['action' => 'skipped', 'message' => 'Invalid address value'];
|
||||
}
|
||||
// If identical address already exists anywhere, skip to avoid constraint violation
|
||||
/*$existingAny = PersonAddress::where('address', $addressLine)->first();
|
||||
if ($existingAny) {
|
||||
return ['action' => 'skipped', 'message' => 'Address already exists in database'];
|
||||
}*/
|
||||
// Default country SLO if not provided
|
||||
if (! isset($addrData['country']) || $addrData['country'] === null || $addrData['country'] === '') {
|
||||
$addrData['country'] = 'SLO';
|
||||
}
|
||||
$existing = PersonAddress::where('person_id', $personId)->where('address', $addressLine)->first();
|
||||
// Compare addresses with all spaces removed to handle whitespace variations
|
||||
$addressLineNoSpaces = preg_replace('/\s+/', '', $addressLine);
|
||||
$existing = PersonAddress::where('person_id', $personId)
|
||||
->whereRaw("REPLACE(address, ' ', '') = ?", [$addressLineNoSpaces])
|
||||
->first();
|
||||
|
||||
$applyInsert = [];
|
||||
$applyUpdate = [];
|
||||
foreach ($mappings as $map) {
|
||||
|
|
@ -3060,9 +3178,23 @@ private function upsertAddress(int $personId, array $addrData, $mappings): array
|
|||
$data['person_id'] = $personId;
|
||||
$data['country'] = $data['country'] ?? 'SLO';
|
||||
$data['type_id'] = $data['type_id'] ?? $this->getDefaultAddressTypeId();
|
||||
$created = PersonAddress::create($data);
|
||||
try {
|
||||
$created = PersonAddress::create($data);
|
||||
|
||||
return ['action' => 'inserted', 'address' => $created];
|
||||
return ['action' => 'inserted', 'address' => $created];
|
||||
} catch (QueryException $e) {
|
||||
// If unique constraint violation, skip instead of aborting
|
||||
Log::warning('Address constraint violation during import', [
|
||||
'person_id' => $personId,
|
||||
'address' => $addressLine,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
if ($e->getCode() === '23505' || str_contains($e->getMessage(), 'unique') || str_contains($e->getMessage(), 'duplicate')) {
|
||||
return ['action' => 'skipped', 'message' => 'Address already exists (constraint violation)'];
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
2
composer.lock
generated
2
composer.lock
generated
|
|
@ -11289,6 +11289,6 @@
|
|||
"platform": {
|
||||
"php": "^8.2"
|
||||
},
|
||||
"platform-dev": {},
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('field_jobs', function (Blueprint $table) {
|
||||
$table->boolean('added_activity')->default(false);
|
||||
$table->timestamp('last_activity')->nullable()->default(null);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('field_jobs', function (Blueprint $table) {
|
||||
$table->dropColumn('last_activity');
|
||||
$table->dropColumn('added_activity');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -122,6 +122,7 @@ const store = async () => {
|
|||
form
|
||||
.transform((data) => ({
|
||||
...data,
|
||||
phone_view: props.phoneMode,
|
||||
due_date: formatDateForSubmit(data.due_date),
|
||||
attachment_document_ids:
|
||||
templateAllowsAttachments.value && data.attach_documents
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script setup>
|
||||
import AppPhoneLayout from "@/Layouts/AppPhoneLayout.vue";
|
||||
import { Separator } from "reka-ui";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
|
|
@ -11,6 +12,9 @@ const items = computed(() => props.jobs || []);
|
|||
|
||||
// Client filter options derived from jobs
|
||||
const clientFilter = ref("");
|
||||
const listNonActivity = ref([]);
|
||||
const listActivity = ref([]);
|
||||
|
||||
const clientOptions = computed(() => {
|
||||
const map = new Map();
|
||||
for (const job of items.value) {
|
||||
|
|
@ -28,7 +32,7 @@ const clientOptions = computed(() => {
|
|||
const search = ref("");
|
||||
const filteredJobs = computed(() => {
|
||||
const term = search.value.trim().toLowerCase();
|
||||
return items.value.filter((job) => {
|
||||
const filterList = items.value.filter((job) => {
|
||||
// Filter by selected client (if any)
|
||||
if (clientFilter.value) {
|
||||
const juuid = job?.contract?.client_case?.client?.uuid;
|
||||
|
|
@ -50,6 +54,9 @@ const filteredJobs = computed(() => {
|
|||
refStr.includes(term) || nameStr.includes(term) || clientNameStr.includes(term)
|
||||
);
|
||||
});
|
||||
listNonActivity.value = filterList.filter((item) => !item.added_activity);
|
||||
listActivity.value = filterList.filter((item) => !!item.added_activity);
|
||||
return filterList;
|
||||
});
|
||||
|
||||
function formatDateDMY(d) {
|
||||
|
|
@ -125,10 +132,12 @@ function getCaseUuid(job) {
|
|||
Počisti
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-3 sm:gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<template v-if="filteredJobs.length">
|
||||
|
||||
<template v-if="filteredJobs.length">
|
||||
<h2 class="py-4">Nove / Ne obdelane</h2>
|
||||
<div class="grid grid-cols-1 gap-3 sm:gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div
|
||||
v-for="job in filteredJobs"
|
||||
v-for="job in listNonActivity"
|
||||
:key="job.id"
|
||||
class="bg-white rounded-lg shadow border p-3 sm:p-4"
|
||||
>
|
||||
|
|
@ -180,9 +189,6 @@ function getCaseUuid(job) {
|
|||
<p class="text-sm text-gray-600 truncate">
|
||||
Kontrakt: {{ job.contract?.reference || job.contract?.uuid }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-600">
|
||||
Tip: {{ job.contract?.type?.name || "—" }}
|
||||
</p>
|
||||
<p
|
||||
class="text-sm text-gray-600"
|
||||
v-if="
|
||||
|
|
@ -205,14 +211,99 @@ function getCaseUuid(job) {
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
v-else
|
||||
class="col-span-full bg-white rounded-lg shadow border p-6 text-center text-gray-600"
|
||||
>
|
||||
<span v-if="search">Ni zadetkov za podani filter.</span>
|
||||
<span v-else>Trenutno nimate dodeljenih terenskih opravil.</span>
|
||||
</div>
|
||||
<h2 class="py-4">Obdelane pogodbe</h2>
|
||||
<div class="grid grid-cols-1 gap-3 sm:gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div
|
||||
v-for="job in listActivity"
|
||||
:key="job.id"
|
||||
class="bg-white rounded-lg shadow border p-3 sm:p-4"
|
||||
>
|
||||
<div class="mb-4 flex gap-2">
|
||||
<a
|
||||
v-if="getCaseUuid(job)"
|
||||
:href="
|
||||
route('phone.case', {
|
||||
client_case: getCaseUuid(job),
|
||||
completed: props.view_mode === 'completed-today' ? 1 : undefined,
|
||||
})
|
||||
"
|
||||
class="inline-flex-1 flex-1 text-center px-3 py-2 rounded-md bg-blue-600 text-white text-sm hover:bg-blue-700"
|
||||
>
|
||||
Odpri primer
|
||||
</a>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
disabled
|
||||
class="inline-flex-1 flex-1 text-center px-3 py-2 rounded-md bg-gray-300 text-gray-600 text-sm cursor-not-allowed"
|
||||
>
|
||||
Manjka primer
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-sm text-gray-500">
|
||||
Dodeljeno:
|
||||
<span class="font-medium text-gray-700">{{
|
||||
formatDateDMY(job.assigned_at)
|
||||
}}</span>
|
||||
</p>
|
||||
<span
|
||||
v-if="job.priority"
|
||||
class="inline-block text-xs px-2 py-0.5 rounded bg-amber-100 text-amber-700"
|
||||
>Prioriteta</span
|
||||
>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<p class="text-base sm:text-lg font-semibold text-gray-800">
|
||||
{{ job.contract?.client_case?.person?.full_name || "—" }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-600">
|
||||
Naročnik:
|
||||
<span class="font-semibold text-gray-800">
|
||||
{{ job.contract?.client_case?.client?.person?.full_name || "—" }}
|
||||
</span>
|
||||
</p>
|
||||
<p class="text-sm text-gray-600 truncate">
|
||||
Kontrakt: {{ job.contract?.reference || job.contract?.uuid }}
|
||||
</p>
|
||||
|
||||
<p
|
||||
class="text-sm text-gray-600"
|
||||
v-if="
|
||||
job.contract?.account &&
|
||||
job.contract.account.balance_amount !== null &&
|
||||
job.contract.account.balance_amount !== undefined
|
||||
"
|
||||
>
|
||||
Odprto: {{ formatAmount(job.contract.account.balance_amount) }} €
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-3 text-sm text-gray-600">
|
||||
<p>
|
||||
<span class="font-medium">Naslov:</span>
|
||||
{{ job.contract?.client_case?.person?.addresses?.[0]?.address || "—" }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-medium">Telefon:</span>
|
||||
{{ job.contract?.client_case?.person?.phones?.[0]?.nu || "—" }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-3 text-sm text-gray-600">
|
||||
<p>
|
||||
<span class="font-medium">Zadnja aktivnost:</span>
|
||||
{{ formatDateDMY(job.last_activity) || "—" }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
v-else
|
||||
class="col-span-full bg-white rounded-lg shadow border p-6 text-center text-gray-600"
|
||||
>
|
||||
<span v-if="search">Ni zadetkov za podani filter.</span>
|
||||
<span v-else>Trenutno nimate dodeljenih terenskih opravil.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user