Fixed some field job problem where field operator could still see archived contracts

This commit is contained in:
Simon Pocrnjič 2025-12-21 21:00:49 +01:00
parent 11206fb4f7
commit adc2a64687
10 changed files with 919 additions and 319 deletions

View File

@ -252,11 +252,14 @@ public function storeActivity(ClientCase $clientCase, Request $request)
'action_id' => 'exists:\App\Models\Action,id', 'action_id' => 'exists:\App\Models\Action,id',
'decision_id' => 'exists:\App\Models\Decision,id', 'decision_id' => 'exists:\App\Models\Decision,id',
'contract_uuid' => 'nullable|uuid', 'contract_uuid' => 'nullable|uuid',
'phone_view' => 'nullable|boolean',
'send_auto_mail' => 'sometimes|boolean', 'send_auto_mail' => 'sometimes|boolean',
'attachment_document_ids' => 'sometimes|array', 'attachment_document_ids' => 'sometimes|array',
'attachment_document_ids.*' => 'integer', 'attachment_document_ids.*' => 'integer',
]); ]);
$isPhoneView = $attributes['phone_view'] ?? false;
// Map contract_uuid to contract_id within the same client case, if provided // Map contract_uuid to contract_id within the same client case, if provided
$contractId = null; $contractId = null;
if (! empty($attributes['contract_uuid'])) { if (! empty($attributes['contract_uuid'])) {
@ -279,10 +282,23 @@ public function storeActivity(ClientCase $clientCase, Request $request)
'decision_id' => $attributes['decision_id'], 'decision_id' => $attributes['decision_id'],
'contract_id' => $contractId, 'contract_id' => $contractId,
]); ]);
/*foreach ($activity->decision->events as $e) {
$class = '\\App\\Events\\' . $e->name; if ($isPhoneView && $contractId) {
event(new $class($clientCase)); $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); logger()->info('Activity successfully inserted', $attributes);
@ -297,8 +313,8 @@ public function storeActivity(ClientCase $clientCase, Request $request)
->values(); ->values();
$validAttachmentIds = collect(); $validAttachmentIds = collect();
if ($attachmentIds->isNotEmpty() && $contractId) { if ($attachmentIds->isNotEmpty() && $contractId) {
$validAttachmentIds = \App\Models\Document::query() $validAttachmentIds = Document::query()
->where('documentable_type', \App\Models\Contract::class) ->where('documentable_type', Contract::class)
->where('documentable_id', $contractId) ->where('documentable_id', $contractId)
->whereIn('id', $attachmentIds) ->whereIn('id', $attachmentIds)
->pluck('id'); ->pluck('id');
@ -1458,178 +1474,115 @@ public function archiveContract(ClientCase $clientCase, string $uuid, Request $r
{ {
$contract = Contract::query()->where('uuid', $uuid)->firstOrFail(); $contract = Contract::query()->where('uuid', $uuid)->firstOrFail();
if ($contract->client_case_id !== $clientCase->id) { if ($contract->client_case_id !== $clientCase->id) {
\Log::warning('Contract not found uuid: {uuid}', ['uuid' => $uuid]);
abort(404); abort(404);
} }
$reactivateRequested = (bool) $request->boolean('reactivate');
// Determine applicable settings based on intent (archive vs reactivate) $attr = $request->validate([
if ($reactivateRequested) { 'reactivate' => 'boolean',
$latestReactivate = \App\Models\ArchiveSetting::query() ]);
$reactivate = $attr['reactivate'] ?? false;
$setting = \App\Models\ArchiveSetting::query()
->where('enabled', true) ->where('enabled', true)
->where('reactivate', true)
->whereIn('strategy', ['immediate', 'manual']) ->whereIn('strategy', ['immediate', 'manual'])
->where('reactivate', $reactivate)
->orderByDesc('id') ->orderByDesc('id')
->first(); ->first();
if (! $latestReactivate) {
return back()->with('warning', __('contracts.reactivate_not_allowed')); if (! $setting->exists()) {
} \Log::warning('No archive settings found!');
$settings = collect([$latestReactivate]);
$hasReactivateRule = true; return back()->with('warning', 'No settings found');
} 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;
} }
// Service archive executor
$executor = app(\App\Services\Archiving\ArchiveExecutor::class); $executor = app(\App\Services\Archiving\ArchiveExecutor::class);
$result = null;
$context = [ $context = [
'contract_id' => $contract->id, 'contract_id' => $contract->id,
'client_case_id' => $clientCase->id, 'client_case_id' => $clientCase->id,
'account_id' => $contract->account->id ?? null,
]; ];
if ($contract->account) {
$context['account_id'] = $contract->account->id;
}
$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 { try {
if ($reactivateRequested) { $result = $executor->executeSetting($setting, $context, \Auth::id());
$note = 'Ponovna aktivacija pogodba '.$contract->reference; } catch (Exception $e) {
} else { \Log::error('There was an error executing ArchiveExecutor::executeSetting {msg}', ['msg' => $e->getMessage()]);
$noteKey = 'contracts.archived_activity_note';
$note = __($noteKey, ['reference' => $contract->reference]); return back()->with('warning', 'Something went wrong!');
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 = [ $activityData = [
'client_case_id' => $clientCase->id, 'client_case_id' => $clientCase->id,
'action_id' => $activitySetting->action_id, 'action_id' => $setting->action_id,
'decision_id' => $activitySetting->decision_id, 'decision_id' => $setting->decision_id,
'note' => $note, 'note' => ($reactivate)
'active' => 1, ? "Ponovno aktivirana pogodba $contract->reference"
'user_id' => optional($request->user())->id, : "Arhivirana pogodba $contract->reference",
]; ];
if ($reactivateRequested) {
// Attach the contract_id when reactivated as per requirement
$activityData['contract_id'] = $contract->id;
}
\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 { try {
$segmentId = $segmentSetting->segment_id; \App\Models\Activity::create($activityData);
\DB::transaction(function () use ($contract, $segmentId, $clientCase) { } catch (Exception $e) {
// Ensure the segment is attached to the client case (activate if previously inactive) \Log::warning('Activity could not be created!');
$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,
'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 // If any archive setting specifies a segment_id, move the contract to that segment (archive bucket)
$existing = \DB::table('contract_segment') if ($setting->segment_id) {
->where('contract_id', $contract->id) $segmentId = $setting->segment_id;
->where('segment_id', $segmentId)
->first(); $contract->segments()
if ($existing) { ->allRelatedIds()
\DB::table('contract_segment') ->map(fn (int $val, int|string $key) => $contract->segments()->updateExistingPivot($val, [
->where('id', $existing->id) 'active' => false,
->update(['active' => true, 'updated_at' => now()]); 'updated_at' => now(),
])
);
if ($contract->attachedSegments()->find($segmentId)->pluck('id')->isNotEmpty()) {
$contract->attachedSegments()->updateExistingPivot($segmentId, [
'active' => true,
'updated_at' => now(),
]);
} else { } else {
\DB::table('contract_segment')->insert([ $contract->segments()->attach(
'contract_id' => $contract->id, $segmentId,
'segment_id' => $segmentId, [
'active' => true, 'active' => true,
'created_at' => now(), 'created_at' => now(),
'updated_at' => now(), 'updated_at' => now(),
]); ]
);
} }
}
$contract->fieldJobs()
->whereNull('completed_at')
->whereNull('cancelled_at')
->update([
'cancelled_at' => date('Y-m-d'),
'updated_at' => now(),
]);
}); });
} catch (\Throwable $e) { } catch (Exception $e) {
logger()->warning('Failed to move contract to archive segment', [ \Log::warning('Something went wrong with inserting / updating archive setting partials!');
'error' => $e->getMessage(),
'contract_id' => $contract->id, return back()->with('warning', 'Something went wrong!');
'segment_id' => $segmentSetting->segment_id,
'setting_id' => $segmentSetting->id,
]);
}
} }
$message = $reactivateRequested ? __('contracts.reactivated') : __('contracts.archived'); return back()->with('success', $reactivate
? __('contracts.reactivated')
return back()->with('success', $message); : __('contracts.archived')
);
} }
/** /**

View File

@ -76,169 +76,81 @@ public function completedToday(Request $request)
public function showCase(\App\Models\ClientCase $clientCase, Request $request) public function showCase(\App\Models\ClientCase $clientCase, Request $request)
{ {
$userId = $request->user()->id; $userId = $request->user()->id;
$completedMode = (bool) $request->boolean('completed'); $completedMode = $request->boolean('completed');
// Eager load client case with person details // Eager load case with person details
$case = \App\Models\ClientCase::query() $case = $clientCase->load('person.addresses', 'person.phones', 'person.emails', 'person.bankAccounts');
->with(['person' => fn ($q) => $q->with(['addresses', 'phones', 'emails', 'bankAccounts'])])
->findOrFail($clientCase->id);
// Determine contracts of this case relevant to the current user // Query contracts based on field jobs
// - Normal mode: contracts assigned to me and still active (not completed/cancelled) $contractsQuery = FieldJob::query()
// - 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) ->where('assigned_user_id', $userId)
->whereNull('cancelled_at')
->whereBetween('completed_at', [$start, $end])
->whereHas('contract', fn ($q) => $q->where('client_case_id', $case->id)) ->whereHas('contract', fn ($q) => $q->where('client_case_id', $case->id))
->pluck('contract_id') ->when($completedMode,
->unique() fn ($q) => $q->whereNull('cancelled_at')->whereBetween('completed_at', [now()->startOfDay(), now()->endOfDay()]),
->values(); fn ($q) => $q->whereNull('completed_at')->whereNull('cancelled_at')
} 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();
}
// Get contracts with relationships
$contracts = \App\Models\Contract::query() $contracts = \App\Models\Contract::query()
->where('client_case_id', $case->id) ->where('client_case_id', $case->id)
->whereIn('id', $contractIds) ->whereIn('id', $contractsQuery->pluck('contract_id')->unique())
->with(['type:id,name', 'account']) ->with(['type:id,name', 'account', 'latestObject'])
->orderByDesc('created_at') ->orderByDesc('created_at')
->get(); ->get();
// Attach latest object (if any) to each contract as last_object for display // Build merged documents
if ($contracts->isNotEmpty()) { $documents = $case->documents()
$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') ->orderByDesc('created_at')
->get() ->get()
->groupBy('contract_id') ->map(fn ($d) => array_merge($d->toArray(), [
->map(function ($group) { 'documentable_type' => \App\Models\ClientCase::class,
return $group->first(); 'client_case_uuid' => $case->uuid,
}); ]))
->concat(
foreach ($latestObjects as $cid => $obj) { \App\Models\Document::query()
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) ->where('documentable_type', \App\Models\Contract::class)
->whereIn('documentable_id', $contractIds) ->whereIn('documentable_id', $contracts->pluck('id'))
->with('documentable:id,uuid,reference')
->orderByDesc('created_at') ->orderByDesc('created_at')
->get() ->get()
->map(function ($d) use ($contractRefMap) { ->map(fn ($d) => array_merge($d->toArray(), [
$arr = $d->toArray(); 'contract_reference' => $d->documentable?->reference,
$arr['contract_reference'] = $contractRefMap[$d->documentable_id] ?? null; 'contract_uuid' => $d->documentable?->uuid,
$arr['documentable_type'] = \App\Models\Contract::class; ]))
$arr['contract_uuid'] = optional(\App\Models\Contract::withTrashed()->find($d->documentable_id))->uuid; )
->sortByDesc('created_at')
->values();
return $arr; // 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();
$caseDocs = $case->documents()->orderByDesc('created_at')->get()->map(function ($d) use ($case) { return Inertia::render('Phone/Case/Index', [
$arr = $d->toArray(); 'client' => $case->client->load('person.addresses', 'person.phones', 'person.emails', 'person.bankAccounts'),
$arr['documentable_type'] = \App\Models\ClientCase::class; 'client_case' => $case,
$arr['client_case_uuid'] = $case->uuid; 'contracts' => $contracts,
'documents' => $documents,
return $arr; 'types' => [
});
$documents = $caseDocs->concat($contractDocs)->sortByDesc('created_at')->values();
// Provide minimal types for PersonInfoGrid
$types = [
'address_types' => \App\Models\Person\AddressType::all(), 'address_types' => \App\Models\Person\AddressType::all(),
'phone_types' => \App\Models\Person\PhoneType::all(), 'phone_types' => \App\Models\Person\PhoneType::all(),
]; ],
'account_types' => \App\Models\AccountType::all(),
// Case activities (compact for phone): latest 20 with relations 'actions' => \App\Models\Action::query()
$activities = $case->activities() ->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']) ->with(['action', 'decision', 'contract:id,uuid,reference', 'user:id,name'])
->orderByDesc('created_at') ->orderByDesc('created_at')
->limit(20) ->limit(20)
->get() ->get()
->map(function ($a) { ->map(fn ($a) => $a->setAttribute('user_name', $a->user?->name)),
$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');
}
)
->pluck('field_job_setting_id')
->filter()
->unique()
->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']);
return Inertia::render('Phone/Case/Index', [
'client' => $case->client()->with('person', fn ($q) => $q->with(['addresses', 'phones', 'emails', 'bankAccounts']))->firstOrFail(),
'client_case' => $case,
'contracts' => $contracts,
'documents' => $documents,
'types' => $types,
'account_types' => \App\Models\AccountType::all(),
// Provide decisions (filtered by segment) with linked email template metadata (entity_types, allow_attachments)
'actions' => $actions,
'activities' => $activities,
'completed_mode' => $completedMode, 'completed_mode' => $completedMode,
]); ]);
} }

View File

@ -96,6 +96,11 @@ public function segments(): BelongsToMany
->wherePivot('active', true); ->wherePivot('active', true);
} }
public function attachedSegments(): BelongsToMany
{
return $this->belongsToMany(\App\Models\Segment::class);
}
public function account(): HasOne public function account(): HasOne
{ {
// Use latestOfMany to always surface newest account snapshot if multiple exist. // Use latestOfMany to always surface newest account snapshot if multiple exist.
@ -114,6 +119,18 @@ public function documents(): MorphMany
return $this->morphMany(\App\Models\Document::class, 'documentable'); 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 protected static function booted(): void
{ {
static::created(function (Contract $contract): void { static::created(function (Contract $contract): void {

View File

@ -24,6 +24,8 @@ class FieldJob extends Model
'priority', 'priority',
'notes', 'notes',
'address_snapshot ', 'address_snapshot ',
'last_activity',
'added_activity'
]; ];
protected $casts = [ protected $casts = [
@ -31,6 +33,8 @@ class FieldJob extends Model
'completed_at' => 'datetime', 'completed_at' => 'datetime',
'cancelled_at' => 'datetime', 'cancelled_at' => 'datetime',
'priority' => 'boolean', 'priority' => 'boolean',
'last_activity' => 'datetime',
'added_activity' => 'boolean',
'address_snapshot ' => 'array', 'address_snapshot ' => 'array',
]; ];
@ -90,7 +94,8 @@ public function user(): BelongsTo
public function contract(): BelongsTo public function contract(): BelongsTo
{ {
return $this->belongsTo(Contract::class, 'contract_id'); return $this->belongsTo(Contract::class, 'contract_id')
->where('active', true);
} }
/** /**

View File

@ -69,6 +69,8 @@ public function executeSetting(ArchiveSetting $setting, ?array $context = null,
$entities = $flat; $entities = $flat;
} }
// dd($entities);
foreach ($entities as $entityDef) { foreach ($entities as $entityDef) {
$rawTable = $entityDef['table'] ?? null; $rawTable = $entityDef['table'] ?? null;
if (! $rawTable) { if (! $rawTable) {
@ -97,7 +99,7 @@ public function executeSetting(ArchiveSetting $setting, ?array $context = null,
// Process in batches to avoid locking large tables // Process in batches to avoid locking large tables
while (true) { while (true) {
$query = DB::table($table)->whereNull('deleted_at'); $query = DB::table($table)->whereNull('deleted_at');
if (Schema::hasColumn($table, 'active')) { if (Schema::hasColumn($table, 'active') && ! $reactivate) {
$query->where('active', 1); $query->where('active', 1);
} }
// Apply context filters or chain derived filters // Apply context filters or chain derived filters

View File

@ -5,7 +5,6 @@
"keywords": ["laravel", "framework"], "keywords": ["laravel", "framework"],
"license": "MIT", "license": "MIT",
"require": { "require": {
"tijsverkoyen/css-to-inline-styles": "^2.2",
"php": "^8.2", "php": "^8.2",
"arielmejiadev/larapex-charts": "^2.1", "arielmejiadev/larapex-charts": "^2.1",
"diglactic/laravel-breadcrumbs": "^10.0", "diglactic/laravel-breadcrumbs": "^10.0",
@ -19,7 +18,8 @@
"maatwebsite/excel": "^3.1", "maatwebsite/excel": "^3.1",
"meilisearch/meilisearch-php": "^1.11", "meilisearch/meilisearch-php": "^1.11",
"robertboes/inertia-breadcrumbs": "dev-laravel-12", "robertboes/inertia-breadcrumbs": "dev-laravel-12",
"tightenco/ziggy": "^2.0" "tightenco/ziggy": "^2.0",
"tijsverkoyen/css-to-inline-styles": "^2.2"
}, },
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.23", "fakerphp/faker": "^1.23",

593
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "51fd57123c1b9f51c24f28e04a692ec4", "content-hash": "d29c47a4d6813ee8e80a7c8112c2f17e",
"packages": [ "packages": [
{ {
"name": "arielmejiadev/larapex-charts", "name": "arielmejiadev/larapex-charts",
@ -242,6 +242,162 @@
], ],
"time": "2024-02-09T16:56:22+00:00" "time": "2024-02-09T16:56:22+00:00"
}, },
{
"name": "composer/pcre",
"version": "3.3.2",
"source": {
"type": "git",
"url": "https://github.com/composer/pcre.git",
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0"
},
"conflict": {
"phpstan/phpstan": "<1.11.10"
},
"require-dev": {
"phpstan/phpstan": "^1.12 || ^2",
"phpstan/phpstan-strict-rules": "^1 || ^2",
"phpunit/phpunit": "^8 || ^9"
},
"type": "library",
"extra": {
"phpstan": {
"includes": [
"extension.neon"
]
},
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Composer\\Pcre\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
}
],
"description": "PCRE wrapping library that offers type-safe preg_* replacements.",
"keywords": [
"PCRE",
"preg",
"regex",
"regular expression"
],
"support": {
"issues": "https://github.com/composer/pcre/issues",
"source": "https://github.com/composer/pcre/tree/3.3.2"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2024-11-12T16:29:46+00:00"
},
{
"name": "composer/semver",
"version": "3.4.4",
"source": {
"type": "git",
"url": "https://github.com/composer/semver.git",
"reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95",
"reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95",
"shasum": ""
},
"require": {
"php": "^5.3.2 || ^7.0 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^1.11",
"symfony/phpunit-bridge": "^3 || ^7"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Composer\\Semver\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nils Adermann",
"email": "naderman@naderman.de",
"homepage": "http://www.naderman.de"
},
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
},
{
"name": "Rob Bast",
"email": "rob.bast@gmail.com",
"homepage": "http://robbast.nl"
}
],
"description": "Semver library that offers utilities, version constraint parsing and validation.",
"keywords": [
"semantic",
"semver",
"validation",
"versioning"
],
"support": {
"irc": "ircs://irc.libera.chat:6697/composer",
"issues": "https://github.com/composer/semver/issues",
"source": "https://github.com/composer/semver/tree/3.4.4"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
}
],
"time": "2025-08-20T19:15:30+00:00"
},
{ {
"name": "dasprid/enum", "name": "dasprid/enum",
"version": "1.0.6", "version": "1.0.6",
@ -737,6 +893,67 @@
], ],
"time": "2025-03-06T22:45:56+00:00" "time": "2025-03-06T22:45:56+00:00"
}, },
{
"name": "ezyang/htmlpurifier",
"version": "v4.19.0",
"source": {
"type": "git",
"url": "https://github.com/ezyang/htmlpurifier.git",
"reference": "b287d2a16aceffbf6e0295559b39662612b77fcf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/b287d2a16aceffbf6e0295559b39662612b77fcf",
"reference": "b287d2a16aceffbf6e0295559b39662612b77fcf",
"shasum": ""
},
"require": {
"php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0"
},
"require-dev": {
"cerdic/css-tidy": "^1.7 || ^2.0",
"simpletest/simpletest": "dev-master"
},
"suggest": {
"cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.",
"ext-bcmath": "Used for unit conversion and imagecrash protection",
"ext-iconv": "Converts text to and from non-UTF-8 encodings",
"ext-tidy": "Used for pretty-printing HTML"
},
"type": "library",
"autoload": {
"files": [
"library/HTMLPurifier.composer.php"
],
"psr-0": {
"HTMLPurifier": "library/"
},
"exclude-from-classmap": [
"/library/HTMLPurifier/Language/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1-or-later"
],
"authors": [
{
"name": "Edward Z. Yang",
"email": "admin@htmlpurifier.org",
"homepage": "http://ezyang.com"
}
],
"description": "Standards compliant HTML filter written in PHP",
"homepage": "http://htmlpurifier.org/",
"keywords": [
"html"
],
"support": {
"issues": "https://github.com/ezyang/htmlpurifier/issues",
"source": "https://github.com/ezyang/htmlpurifier/tree/v4.19.0"
},
"time": "2025-10-17T16:34:55+00:00"
},
{ {
"name": "facade/ignition-contracts", "name": "facade/ignition-contracts",
"version": "1.0.2", "version": "1.0.2",
@ -2695,6 +2912,272 @@
], ],
"time": "2024-12-08T08:18:47+00:00" "time": "2024-12-08T08:18:47+00:00"
}, },
{
"name": "maatwebsite/excel",
"version": "3.1.67",
"source": {
"type": "git",
"url": "https://github.com/SpartnerNL/Laravel-Excel.git",
"reference": "e508e34a502a3acc3329b464dad257378a7edb4d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SpartnerNL/Laravel-Excel/zipball/e508e34a502a3acc3329b464dad257378a7edb4d",
"reference": "e508e34a502a3acc3329b464dad257378a7edb4d",
"shasum": ""
},
"require": {
"composer/semver": "^3.3",
"ext-json": "*",
"illuminate/support": "5.8.*||^6.0||^7.0||^8.0||^9.0||^10.0||^11.0||^12.0",
"php": "^7.0||^8.0",
"phpoffice/phpspreadsheet": "^1.30.0",
"psr/simple-cache": "^1.0||^2.0||^3.0"
},
"require-dev": {
"laravel/scout": "^7.0||^8.0||^9.0||^10.0",
"orchestra/testbench": "^6.0||^7.0||^8.0||^9.0||^10.0",
"predis/predis": "^1.1"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"Excel": "Maatwebsite\\Excel\\Facades\\Excel"
},
"providers": [
"Maatwebsite\\Excel\\ExcelServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Maatwebsite\\Excel\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Patrick Brouwers",
"email": "patrick@spartner.nl"
}
],
"description": "Supercharged Excel exports and imports in Laravel",
"keywords": [
"PHPExcel",
"batch",
"csv",
"excel",
"export",
"import",
"laravel",
"php",
"phpspreadsheet"
],
"support": {
"issues": "https://github.com/SpartnerNL/Laravel-Excel/issues",
"source": "https://github.com/SpartnerNL/Laravel-Excel/tree/3.1.67"
},
"funding": [
{
"url": "https://laravel-excel.com/commercial-support",
"type": "custom"
},
{
"url": "https://github.com/patrickbrouwers",
"type": "github"
}
],
"time": "2025-08-26T09:13:16+00:00"
},
{
"name": "maennchen/zipstream-php",
"version": "3.2.1",
"source": {
"type": "git",
"url": "https://github.com/maennchen/ZipStream-PHP.git",
"reference": "682f1098a8fddbaf43edac2306a691c7ad508ec5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/682f1098a8fddbaf43edac2306a691c7ad508ec5",
"reference": "682f1098a8fddbaf43edac2306a691c7ad508ec5",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"ext-zlib": "*",
"php-64bit": "^8.3"
},
"require-dev": {
"brianium/paratest": "^7.7",
"ext-zip": "*",
"friendsofphp/php-cs-fixer": "^3.86",
"guzzlehttp/guzzle": "^7.5",
"mikey179/vfsstream": "^1.6",
"php-coveralls/php-coveralls": "^2.5",
"phpunit/phpunit": "^12.0",
"vimeo/psalm": "^6.0"
},
"suggest": {
"guzzlehttp/psr7": "^2.4",
"psr/http-message": "^2.0"
},
"type": "library",
"autoload": {
"psr-4": {
"ZipStream\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paul Duncan",
"email": "pabs@pablotron.org"
},
{
"name": "Jonatan Männchen",
"email": "jonatan@maennchen.ch"
},
{
"name": "Jesse Donat",
"email": "donatj@gmail.com"
},
{
"name": "András Kolesár",
"email": "kolesar@kolesar.hu"
}
],
"description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.",
"keywords": [
"stream",
"zip"
],
"support": {
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
"source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.1"
},
"funding": [
{
"url": "https://github.com/maennchen",
"type": "github"
}
],
"time": "2025-12-10T09:58:31+00:00"
},
{
"name": "markbaker/complex",
"version": "3.0.2",
"source": {
"type": "git",
"url": "https://github.com/MarkBaker/PHPComplex.git",
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
"phpcompatibility/php-compatibility": "^9.3",
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
"squizlabs/php_codesniffer": "^3.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Complex\\": "classes/src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Baker",
"email": "mark@lange.demon.co.uk"
}
],
"description": "PHP Class for working with complex numbers",
"homepage": "https://github.com/MarkBaker/PHPComplex",
"keywords": [
"complex",
"mathematics"
],
"support": {
"issues": "https://github.com/MarkBaker/PHPComplex/issues",
"source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2"
},
"time": "2022-12-06T16:21:08+00:00"
},
{
"name": "markbaker/matrix",
"version": "3.0.1",
"source": {
"type": "git",
"url": "https://github.com/MarkBaker/PHPMatrix.git",
"reference": "728434227fe21be27ff6d86621a1b13107a2562c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c",
"reference": "728434227fe21be27ff6d86621a1b13107a2562c",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
"phpcompatibility/php-compatibility": "^9.3",
"phpdocumentor/phpdocumentor": "2.*",
"phploc/phploc": "^4.0",
"phpmd/phpmd": "2.*",
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
"sebastian/phpcpd": "^4.0",
"squizlabs/php_codesniffer": "^3.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Matrix\\": "classes/src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Baker",
"email": "mark@demon-angel.eu"
}
],
"description": "PHP Class for working with matrices",
"homepage": "https://github.com/MarkBaker/PHPMatrix",
"keywords": [
"mathematics",
"matrix",
"vector"
],
"support": {
"issues": "https://github.com/MarkBaker/PHPMatrix/issues",
"source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1"
},
"time": "2022-12-02T22:17:43+00:00"
},
{ {
"name": "meilisearch/meilisearch-php", "name": "meilisearch/meilisearch-php",
"version": "v1.13.0", "version": "v1.13.0",
@ -3475,6 +3958,112 @@
}, },
"time": "2024-10-02T11:20:13+00:00" "time": "2024-10-02T11:20:13+00:00"
}, },
{
"name": "phpoffice/phpspreadsheet",
"version": "1.30.1",
"source": {
"type": "git",
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
"reference": "fa8257a579ec623473eabfe49731de5967306c4c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/fa8257a579ec623473eabfe49731de5967306c4c",
"reference": "fa8257a579ec623473eabfe49731de5967306c4c",
"shasum": ""
},
"require": {
"composer/pcre": "^1||^2||^3",
"ext-ctype": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
"ext-gd": "*",
"ext-iconv": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-simplexml": "*",
"ext-xml": "*",
"ext-xmlreader": "*",
"ext-xmlwriter": "*",
"ext-zip": "*",
"ext-zlib": "*",
"ezyang/htmlpurifier": "^4.15",
"maennchen/zipstream-php": "^2.1 || ^3.0",
"markbaker/complex": "^3.0",
"markbaker/matrix": "^3.0",
"php": ">=7.4.0 <8.5.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0",
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
"dompdf/dompdf": "^1.0 || ^2.0 || ^3.0",
"friendsofphp/php-cs-fixer": "^3.2",
"mitoteam/jpgraph": "^10.3",
"mpdf/mpdf": "^8.1.1",
"phpcompatibility/php-compatibility": "^9.3",
"phpstan/phpstan": "^1.1",
"phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^8.5 || ^9.0",
"squizlabs/php_codesniffer": "^3.7",
"tecnickcom/tcpdf": "^6.5"
},
"suggest": {
"dompdf/dompdf": "Option for rendering PDF with PDF Writer",
"ext-intl": "PHP Internationalization Functions",
"mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
"mpdf/mpdf": "Option for rendering PDF with PDF Writer",
"tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
},
"type": "library",
"autoload": {
"psr-4": {
"PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Maarten Balliauw",
"homepage": "https://blog.maartenballiauw.be"
},
{
"name": "Mark Baker",
"homepage": "https://markbakeruk.net"
},
{
"name": "Franck Lefevre",
"homepage": "https://rootslabs.net"
},
{
"name": "Erik Tilt"
},
{
"name": "Adrien Crivelli"
}
],
"description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
"homepage": "https://github.com/PHPOffice/PhpSpreadsheet",
"keywords": [
"OpenXML",
"excel",
"gnumeric",
"ods",
"php",
"spreadsheet",
"xls",
"xlsx"
],
"support": {
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.1"
},
"time": "2025-10-26T16:01:04+00:00"
},
{ {
"name": "phpoption/phpoption", "name": "phpoption/phpoption",
"version": "1.9.3", "version": "1.9.3",
@ -10335,6 +10924,6 @@
"platform": { "platform": {
"php": "^8.2" "php": "^8.2"
}, },
"platform-dev": {}, "platform-dev": [],
"plugin-api-version": "2.6.0" "plugin-api-version": "2.6.0"
} }

View File

@ -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');
});
}
};

View File

@ -113,6 +113,7 @@ const store = async () => {
form form
.transform((data) => ({ .transform((data) => ({
...data, ...data,
phone_view: props.phoneMode,
due_date: formatDateForSubmit(data.due_date), due_date: formatDateForSubmit(data.due_date),
attachment_document_ids: attachment_document_ids:
templateAllowsAttachments.value && data.attach_documents templateAllowsAttachments.value && data.attach_documents

View File

@ -1,5 +1,6 @@
<script setup> <script setup>
import AppPhoneLayout from "@/Layouts/AppPhoneLayout.vue"; import AppPhoneLayout from "@/Layouts/AppPhoneLayout.vue";
import { Separator } from "reka-ui";
import { computed, ref } from "vue"; import { computed, ref } from "vue";
const props = defineProps({ const props = defineProps({
@ -11,6 +12,9 @@ const items = computed(() => props.jobs || []);
// Client filter options derived from jobs // Client filter options derived from jobs
const clientFilter = ref(""); const clientFilter = ref("");
const listNonActivity = ref([]);
const listActivity = ref([]);
const clientOptions = computed(() => { const clientOptions = computed(() => {
const map = new Map(); const map = new Map();
for (const job of items.value) { for (const job of items.value) {
@ -28,7 +32,7 @@ const clientOptions = computed(() => {
const search = ref(""); const search = ref("");
const filteredJobs = computed(() => { const filteredJobs = computed(() => {
const term = search.value.trim().toLowerCase(); const term = search.value.trim().toLowerCase();
return items.value.filter((job) => { const filterList = items.value.filter((job) => {
// Filter by selected client (if any) // Filter by selected client (if any)
if (clientFilter.value) { if (clientFilter.value) {
const juuid = job?.contract?.client_case?.client?.uuid; const juuid = job?.contract?.client_case?.client?.uuid;
@ -50,6 +54,9 @@ const filteredJobs = computed(() => {
refStr.includes(term) || nameStr.includes(term) || clientNameStr.includes(term) 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) { function formatDateDMY(d) {
@ -125,10 +132,12 @@ function getCaseUuid(job) {
Počisti Počisti
</button> </button>
</div> </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 <div
v-for="job in filteredJobs" v-for="job in listNonActivity"
:key="job.id" :key="job.id"
class="bg-white rounded-lg shadow border p-3 sm:p-4" 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"> <p class="text-sm text-gray-600 truncate">
Kontrakt: {{ job.contract?.reference || job.contract?.uuid }} Kontrakt: {{ job.contract?.reference || job.contract?.uuid }}
</p> </p>
<p class="text-sm text-gray-600">
Tip: {{ job.contract?.type?.name || "—" }}
</p>
<p <p
class="text-sm text-gray-600" class="text-sm text-gray-600"
v-if=" v-if="
@ -205,6 +211,92 @@ function getCaseUuid(job) {
</p> </p>
</div> </div>
</div> </div>
</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> </template>
<div <div
v-else v-else
@ -215,6 +307,5 @@ function getCaseUuid(job) {
</div> </div>
</div> </div>
</div> </div>
</div>
</AppPhoneLayout> </AppPhoneLayout>
</template> </template>