Field job changed permissions and other things

This commit is contained in:
Simon Pocrnjič 2025-10-31 13:21:54 +01:00
parent 0d9c8c8b30
commit 5f879c9436
6 changed files with 160 additions and 46 deletions

View File

@ -76,14 +76,29 @@ 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');
// Eager load client case with person details // Eager load client case with person details
$case = \App\Models\ClientCase::query() $case = \App\Models\ClientCase::query()
->with(['person' => fn ($q) => $q->with(['addresses', 'phones', 'emails', 'bankAccounts'])]) ->with(['person' => fn ($q) => $q->with(['addresses', 'phones', 'emails', 'bankAccounts'])])
->findOrFail($clientCase->id); ->findOrFail($clientCase->id);
// Determine contracts of this case assigned to the current user via FieldJobs and still active // Determine contracts of this case relevant to the current user
$assignedContractIds = FieldJob::query() // - 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) ->where('assigned_user_id', $userId)
->whereNull('completed_at') ->whereNull('completed_at')
->whereNull('cancelled_at') ->whereNull('cancelled_at')
@ -91,10 +106,11 @@ public function showCase(\App\Models\ClientCase $clientCase, Request $request)
->pluck('contract_id') ->pluck('contract_id')
->unique() ->unique()
->values(); ->values();
}
$contracts = \App\Models\Contract::query() $contracts = \App\Models\Contract::query()
->where('client_case_id', $case->id) ->where('client_case_id', $case->id)
->whereIn('id', $assignedContractIds) ->whereIn('id', $contractIds)
->with(['type:id,name', 'account']) ->with(['type:id,name', 'account'])
->orderByDesc('created_at') ->orderByDesc('created_at')
->get(); ->get();
@ -128,7 +144,7 @@ public function showCase(\App\Models\ClientCase $clientCase, Request $request)
$contractDocs = \App\Models\Document::query() $contractDocs = \App\Models\Document::query()
->where('documentable_type', \App\Models\Contract::class) ->where('documentable_type', \App\Models\Contract::class)
->whereIn('documentable_id', $assignedContractIds) ->whereIn('documentable_id', $contractIds)
->orderByDesc('created_at') ->orderByDesc('created_at')
->get() ->get()
->map(function ($d) use ($contractRefMap) { ->map(function ($d) use ($contractRefMap) {
@ -168,15 +184,41 @@ public function showCase(\App\Models\ClientCase $clientCase, Request $request)
return $a; return $a;
}); });
return Inertia::render('Phone/Case/Index', [ // Determine segment filters from FieldJobSettings for this case/user context
'client' => $case->client()->with('person', fn ($q) => $q->with(['addresses', 'phones', 'emails', 'bankAccounts']))->firstOrFail(), $settingIds = FieldJob::query()
'client_case' => $case, ->where('assigned_user_id', $userId)
'contracts' => $contracts, ->whereHas('contract', fn ($q) => $q->where('client_case_id', $case->id))
'documents' => $documents, ->when(
'types' => $types, $completedMode,
'account_types' => \App\Models\AccountType::all(), function ($q) {
// Provide decisions with linked email template metadata (entity_types, allow_attachments) $q->whereNull('cancelled_at')
'actions' => \App\Models\Action::query() ->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([ ->with([
'decisions' => function ($q) { 'decisions' => function ($q) {
$q->select('decisions.id', 'decisions.name', 'decisions.color_tag', 'decisions.auto_mail', 'decisions.email_template_id'); $q->select('decisions.id', 'decisions.name', 'decisions.color_tag', 'decisions.auto_mail', 'decisions.email_template_id');
@ -185,9 +227,19 @@ public function showCase(\App\Models\ClientCase $clientCase, Request $request)
$q->select('id', 'name', 'entity_types', 'allow_attachments'); $q->select('id', 'name', 'entity_types', 'allow_attachments');
}, },
]) ])
->get(['id', 'name', 'color_tag', 'segment_id']), ->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, 'activities' => $activities,
'completed_mode' => (bool) $request->boolean('completed'), 'completed_mode' => $completedMode,
]); ]);
} }
} }

View File

@ -186,6 +186,7 @@ const rawMenuGroups = [
}, },
{ {
label: "Terensko delo", label: "Terensko delo",
requires: { permission: "field-job" },
items: [ items: [
{ {
key: "fieldjobs", key: "fieldjobs",

View File

@ -130,9 +130,9 @@ const redirectIfNoContracts = () => {
} }
}; };
onMounted(() => { /*onMounted(() => {
redirectIfNoContracts(); redirectIfNoContracts();
}); });*/
watch( watch(
() => (Array.isArray(props.contracts) ? props.contracts.length : null), () => (Array.isArray(props.contracts) ? props.contracts.length : null),

View File

@ -38,6 +38,7 @@ const columns = [
{ key: "id", label: "#", sortable: true, class: "w-16" }, { key: "id", label: "#", sortable: true, class: "w-16" },
{ key: "name", label: "Ime", sortable: true }, { key: "name", label: "Ime", sortable: true },
{ key: "color_tag", label: "Barva", sortable: false }, { key: "color_tag", label: "Barva", sortable: false },
{ key: "segment", label: "Segment", sortable: false },
{ key: "decisions", label: "Odločitve", sortable: false, class: "w-32" }, { key: "decisions", label: "Odločitve", sortable: false, class: "w-32" },
]; ];
@ -195,6 +196,11 @@ const destroyAction = () => {
<template #cell-decisions="{ row }"> <template #cell-decisions="{ row }">
{{ row.decisions?.length ?? 0 }} {{ row.decisions?.length ?? 0 }}
</template> </template>
<template #cell-segment="{ row }">
<span>
{{ row.segment?.name || "" }}
</span>
</template>
<template #actions="{ row }"> <template #actions="{ row }">
<button class="px-2" @click="openEditDrawer(row)"> <button class="px-2" @click="openEditDrawer(row)">
<EditIcon size="md" css="text-gray-500" /> <EditIcon size="md" css="text-gray-500" />

View File

@ -166,7 +166,7 @@
}); });
// Contract document generation (JSON) - protected by auth+verified; permission enforced inside controller service // Contract document generation (JSON) - protected by auth+verified; permission enforced inside controller service
Route::post('contracts/{contract:uuid}/generate-document', \App\Http\Controllers\ContractDocumentGenerationController::class)->name('contracts.generate-document')->middleware("permission:create-docs"); Route::post('contracts/{contract:uuid}/generate-document', \App\Http\Controllers\ContractDocumentGenerationController::class)->name('contracts.generate-document')->middleware('permission:create-docs');
// Phone page // Phone page
Route::get('phone', [PhoneViewController::class, 'index'])->name('phone.index'); Route::get('phone', [PhoneViewController::class, 'index'])->name('phone.index');
@ -303,7 +303,7 @@
Route::get('clients/{client:uuid}', [ClientController::class, 'show'])->name('client.show'); Route::get('clients/{client:uuid}', [ClientController::class, 'show'])->name('client.show');
Route::get('clients/{client:uuid}/contracts', [ClientController::class, 'contracts'])->name('client.contracts'); Route::get('clients/{client:uuid}/contracts', [ClientController::class, 'contracts'])->name('client.contracts');
Route::middleware('permission:client-edit')->group( function() { Route::middleware('permission:client-edit')->group(function () {
Route::post('clients', [ClientController::class, 'store'])->name('client.store'); Route::post('clients', [ClientController::class, 'store'])->name('client.store');
Route::put('clients/{client:uuid}', [ClientController::class, 'update'])->name('client.update'); Route::put('clients/{client:uuid}', [ClientController::class, 'update'])->name('client.update');
Route::post('clients/{client:uuid}/emergency-person', [ClientController::class, 'emergencyCreatePerson'])->name('client.emergencyPerson'); Route::post('clients/{client:uuid}/emergency-person', [ClientController::class, 'emergencyCreatePerson'])->name('client.emergencyPerson');
@ -320,7 +320,7 @@
// client-case / contract // client-case / contract
Route::get('client-cases/{client_case:uuid}/contract/{uuid}/debug-accounts', [ClientCaseContoller::class, 'debugContractAccounts'])->name('clientCase.contract.debugAccounts'); Route::get('client-cases/{client_case:uuid}/contract/{uuid}/debug-accounts', [ClientCaseContoller::class, 'debugContractAccounts'])->name('clientCase.contract.debugAccounts');
Route::middleware('permission:contract-edit')->group( function () { Route::middleware('permission:contract-edit')->group(function () {
Route::post('client-cases/{client_case:uuid}/contract', [ClientCaseContoller::class, 'storeContract'])->name('clientCase.contract.store'); Route::post('client-cases/{client_case:uuid}/contract', [ClientCaseContoller::class, 'storeContract'])->name('clientCase.contract.store');
Route::put('client-cases/{client_case:uuid}/contract/{uuid}', [ClientCaseContoller::class, 'updateContract'])->name('clientCase.contract.update'); Route::put('client-cases/{client_case:uuid}/contract/{uuid}', [ClientCaseContoller::class, 'updateContract'])->name('clientCase.contract.update');
Route::delete('client-cases/{client_case:uuid}/contract/{uuid}', [ClientCaseContoller::class, 'deleteContract'])->name('clientCase.contract.delete'); Route::delete('client-cases/{client_case:uuid}/contract/{uuid}', [ClientCaseContoller::class, 'deleteContract'])->name('clientCase.contract.delete');
@ -332,7 +332,7 @@
Route::delete('client-cases/{client_case:uuid}/objects/{id}', [CaseObjectController::class, 'destroy'])->name('clientCase.object.delete'); Route::delete('client-cases/{client_case:uuid}/objects/{id}', [CaseObjectController::class, 'destroy'])->name('clientCase.object.delete');
// client-case / activity // client-case / activity
Route::post('client-cases/{client_case:uuid}/activity', [ClientCaseContoller::class, 'storeActivity'])->name('clientCase.activity.store'); Route::post('client-cases/{client_case:uuid}/activity', [ClientCaseContoller::class, 'storeActivity'])->name('clientCase.activity.store');
Route::delete('client-cases/{client_case:uuid}/activity/{activity}', [ClientCaseContoller::class, 'deleteActivity'])->name('clientCase.activity.delete')->middleware("permission:activity-edit"); Route::delete('client-cases/{client_case:uuid}/activity/{activity}', [ClientCaseContoller::class, 'deleteActivity'])->name('clientCase.activity.delete')->middleware('permission:activity-edit');
// client-case / segments // client-case / segments
Route::post('client-cases/{client_case:uuid}/segments', [ClientCaseContoller::class, 'attachSegment'])->name('clientCase.segments.attach'); Route::post('client-cases/{client_case:uuid}/segments', [ClientCaseContoller::class, 'attachSegment'])->name('clientCase.segments.attach');
// client-case / documents // client-case / documents
@ -340,7 +340,7 @@
Route::get('client-cases/{client_case:uuid}/documents/{document:uuid}/view', [ClientCaseContoller::class, 'viewDocument'])->name('clientCase.document.view'); Route::get('client-cases/{client_case:uuid}/documents/{document:uuid}/view', [ClientCaseContoller::class, 'viewDocument'])->name('clientCase.document.view');
Route::get('client-cases/{client_case:uuid}/documents/{document:uuid}/download', [ClientCaseContoller::class, 'downloadDocument'])->name('clientCase.document.download'); Route::get('client-cases/{client_case:uuid}/documents/{document:uuid}/download', [ClientCaseContoller::class, 'downloadDocument'])->name('clientCase.document.download');
Route::middleware("permission:doc-edit")->group( function() { Route::middleware('permission:doc-edit')->group(function () {
Route::patch('client-cases/{client_case:uuid}/documents/{document:uuid}', [ClientCaseContoller::class, 'updateDocument']) Route::patch('client-cases/{client_case:uuid}/documents/{document:uuid}', [ClientCaseContoller::class, 'updateDocument'])
->withoutScopedBindings() ->withoutScopedBindings()
->name('clientCase.document.update'); ->name('clientCase.document.update');
@ -375,6 +375,7 @@
Route::get('settings/field-job', [FieldJobSettingController::class, 'index'])->name('settings.fieldjob.index'); Route::get('settings/field-job', [FieldJobSettingController::class, 'index'])->name('settings.fieldjob.index');
// field jobs assignment // field jobs assignment
Route::middleware('permission:field-job')->group(function () {
Route::get('field-jobs', [FieldJobController::class, 'index'])->name('fieldjobs.index'); Route::get('field-jobs', [FieldJobController::class, 'index'])->name('fieldjobs.index');
Route::post('field-jobs/assign', [FieldJobController::class, 'assign'])->name('fieldjobs.assign'); Route::post('field-jobs/assign', [FieldJobController::class, 'assign'])->name('fieldjobs.assign');
Route::post('field-jobs/assign-bulk', [FieldJobController::class, 'assignBulk'])->name('fieldjobs.assign-bulk'); Route::post('field-jobs/assign-bulk', [FieldJobController::class, 'assignBulk'])->name('fieldjobs.assign-bulk');
@ -382,6 +383,8 @@
Route::post('settings/field-job', [FieldJobSettingController::class, 'store'])->name('settings.fieldjob.store'); Route::post('settings/field-job', [FieldJobSettingController::class, 'store'])->name('settings.fieldjob.store');
Route::put('settings/field-job/{setting}', [FieldJobSettingController::class, 'update'])->name('settings.fieldjob.update'); Route::put('settings/field-job/{setting}', [FieldJobSettingController::class, 'update'])->name('settings.fieldjob.update');
// settings / contract-configs // settings / contract-configs
});
Route::get('settings/contract-configs', [ContractConfigController::class, 'index'])->name('settings.contractConfigs.index'); Route::get('settings/contract-configs', [ContractConfigController::class, 'index'])->name('settings.contractConfigs.index');
Route::post('settings/contract-configs', [ContractConfigController::class, 'store'])->name('settings.contractConfigs.store'); Route::post('settings/contract-configs', [ContractConfigController::class, 'store'])->name('settings.contractConfigs.store');
Route::put('settings/contract-configs/{config}', [ContractConfigController::class, 'update'])->name('settings.contractConfigs.update'); Route::put('settings/contract-configs/{config}', [ContractConfigController::class, 'update'])->name('settings.contractConfigs.update');
@ -397,7 +400,7 @@
Route::get('segments', [SegmentController::class, 'index'])->name('segments.index'); Route::get('segments', [SegmentController::class, 'index'])->name('segments.index');
Route::get('segments/{segment}', [SegmentController::class, 'show'])->name('segments.show'); Route::get('segments/{segment}', [SegmentController::class, 'show'])->name('segments.show');
Route::middleware("permission:manage-imports")->group( function () { Route::middleware('permission:manage-imports')->group(function () {
// imports // imports
Route::get('imports/create', [ImportController::class, 'create'])->name('imports.create'); Route::get('imports/create', [ImportController::class, 'create'])->name('imports.create');
Route::get('imports', [ImportController::class, 'index'])->name('imports.index'); Route::get('imports', [ImportController::class, 'index'])->name('imports.index');

View File

@ -0,0 +1,52 @@
<?php
use App\Models\ClientCase;
use App\Models\Contract;
use App\Models\FieldJob;
use App\Models\FieldJobSetting;
use App\Models\User;
use Inertia\Testing\AssertableInertia as Assert;
it('shows contracts completed today for the user when completed mode is on', function () {
$user = User::factory()->create();
// Create a client case and a contract under it
$case = ClientCase::factory()->create();
$contract = Contract::factory()->create([
'client_case_id' => $case->id,
]);
// Create field job setting (required FK)
$setting = FieldJobSetting::factory()->create();
// Create a completed field job for this user, for this contract, completed today
FieldJob::query()->create([
'field_job_setting_id' => $setting->id,
'assigned_user_id' => $user->id,
'user_id' => $user->id,
'contract_id' => $contract->id,
'assigned_at' => now()->subHour(),
'completed_at' => now()->subMinutes(5),
'cancelled_at' => null,
'priority' => false,
]);
// In normal mode (no completed flag), the contract should not be listed (job is completed)
$this->actingAs($user)
->get(route('phone.case', ['client_case' => $case->uuid]))
->assertInertia(fn (Assert $page) => $page
->component('Phone/Case/Index')
->where('completed_mode', false)
->has('contracts', 0)
);
// With completed=1, it should be listed
$this->actingAs($user)
->get(route('phone.case', ['client_case' => $case->uuid, 'completed' => 1]))
->assertInertia(fn (Assert $page) => $page
->component('Phone/Case/Index')
->where('completed_mode', true)
->has('contracts', 1)
->where('contracts.0.uuid', $contract->uuid)
);
});