field job added option to add multiple contracts to user at once
This commit is contained in:
parent
e782bcca7c
commit
04f31e62aa
|
|
@ -132,6 +132,77 @@ public function assign(Request $request)
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk assign multiple contracts to a single user.
|
||||
*/
|
||||
public function assignBulk(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'contract_uuids' => 'required|array|min:1',
|
||||
'contract_uuids.*' => 'required|string|distinct|exists:contracts,uuid',
|
||||
'assigned_user_id' => 'required|integer|exists:users,id',
|
||||
]);
|
||||
|
||||
try {
|
||||
DB::transaction(function () use ($data) {
|
||||
$setting = FieldJobSetting::query()->latest('id')->first();
|
||||
|
||||
if (! $setting) {
|
||||
throw new Exception('No Field Job Setting found. Create one in Settings → Field Job Settings.');
|
||||
}
|
||||
|
||||
if (! ($setting->action_id && $setting->assign_decision_id)) {
|
||||
throw new Exception('The current Field Job Setting is missing an action or assign decision. Please update it in Settings → Field Job Settings.');
|
||||
}
|
||||
|
||||
$assigneeName = User::query()->where('id', $data['assigned_user_id'])->value('name');
|
||||
$noteBase = 'Terensko opravilo dodeljeno'.($assigneeName ? ' uporabniku '.$assigneeName : '');
|
||||
|
||||
// Load all contracts in one query
|
||||
$contracts = Contract::query()->whereIn('uuid', $data['contract_uuids'])->get();
|
||||
|
||||
foreach ($contracts as $contract) {
|
||||
// Skip if already has an active job
|
||||
$hasActive = FieldJob::query()
|
||||
->where('contract_id', $contract->id)
|
||||
->whereNull('completed_at')
|
||||
->whereNull('cancelled_at')
|
||||
->exists();
|
||||
|
||||
if ($hasActive) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$job = FieldJob::create([
|
||||
'field_job_setting_id' => $setting->id,
|
||||
'assigned_user_id' => $data['assigned_user_id'],
|
||||
'contract_id' => $contract->id,
|
||||
'assigned_at' => now(),
|
||||
]);
|
||||
|
||||
Activity::create([
|
||||
'due_date' => null,
|
||||
'amount' => null,
|
||||
'note' => $noteBase,
|
||||
'action_id' => $setting->action_id,
|
||||
'decision_id' => $setting->assign_decision_id,
|
||||
'client_case_id' => $contract->client_case_id,
|
||||
'contract_id' => $contract->id,
|
||||
]);
|
||||
|
||||
// Move contract to the configured segment for field jobs
|
||||
$job->moveContractToSegment($setting->segment_id);
|
||||
}
|
||||
});
|
||||
|
||||
return back()->with('success', 'Field jobs assigned.');
|
||||
} catch (QueryException $e) {
|
||||
return back()->withErrors(['database' => 'Database error: '.$e->getMessage()]);
|
||||
} catch (Exception $e) {
|
||||
return back()->withErrors(['error' => 'Error: '.$e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function cancel(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use App\Models\Segment;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Action>
|
||||
|
|
@ -19,6 +20,7 @@ public function definition(): array
|
|||
return [
|
||||
'name' => $this->faker->unique()->words(2, true),
|
||||
'color_tag' => $this->faker->optional()->safeColorName(),
|
||||
'segment_id' => Segment::factory(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
37
database/factories/FieldJobSettingFactory.php
Normal file
37
database/factories/FieldJobSettingFactory.php
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Action;
|
||||
use App\Models\Decision;
|
||||
use App\Models\Segment;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\FieldJobSetting>
|
||||
*/
|
||||
class FieldJobSettingFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
$action = Action::factory()->create();
|
||||
$decision = Decision::factory()->create();
|
||||
$segment = Segment::factory()->create();
|
||||
|
||||
return [
|
||||
'segment_id' => $segment->id,
|
||||
'initial_decision_id' => $decision->id,
|
||||
'assign_decision_id' => $decision->id,
|
||||
'complete_decision_id' => $decision->id,
|
||||
'cancel_decision_id' => $decision->id,
|
||||
'return_segment_id' => $segment->id,
|
||||
'queue_segment_id' => $segment->id,
|
||||
'action_id' => $action->id,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -78,21 +78,20 @@ function buildRelated(a) {
|
|||
// Only client case link (other routes not defined yet)
|
||||
if (a.client_case_uuid || a.client_case_id) {
|
||||
const caseParam = a.client_case_uuid || a.client_case_id;
|
||||
if (typeof route === "function" && route().hasOwnProperty) {
|
||||
try {
|
||||
links.push({
|
||||
type: "client_case",
|
||||
label: "Primer",
|
||||
href: route("clientCase.show", caseParam),
|
||||
});
|
||||
} catch (e) {
|
||||
/* silently ignore */
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
// Prefer Ziggy when available and force stringification here
|
||||
const href = String(route("clientCase.show", { client_case: caseParam }));
|
||||
links.push({
|
||||
type: "client_case",
|
||||
label: "Primer",
|
||||
href: route("clientCase.show", caseParam),
|
||||
href,
|
||||
});
|
||||
} catch (e) {
|
||||
// Safe fallback to a best-effort URL to avoid breaking render
|
||||
links.push({
|
||||
type: "client_case",
|
||||
label: "Primer",
|
||||
href: `/client-cases/${caseParam}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -132,6 +131,24 @@ function formatJobTime(ts) {
|
|||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// Safely build a client case href using Ziggy when available, with a plain fallback.
|
||||
function safeCaseHref(uuid, segment = null) {
|
||||
if (!uuid) {
|
||||
return "#";
|
||||
}
|
||||
try {
|
||||
const params = { client_case: uuid };
|
||||
if (segment != null) {
|
||||
params.segment = segment;
|
||||
}
|
||||
return String(route("clientCase.show", params));
|
||||
} catch (e) {
|
||||
return segment != null
|
||||
? `/client-cases/${uuid}?segment=${segment}`
|
||||
: `/client-cases/${uuid}`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -425,10 +442,14 @@ function formatJobTime(ts) {
|
|||
>
|
||||
<div class="min-w-0">
|
||||
<Link
|
||||
:href="route('clientCase.show', c.uuid)"
|
||||
v-if="c?.uuid"
|
||||
:href="safeCaseHref(c.uuid)"
|
||||
class="text-indigo-600 dark:text-indigo-400 hover:underline font-medium"
|
||||
>{{ c.client_ref || c.uuid.slice(0, 8) }}</Link
|
||||
>
|
||||
<span v-else class="text-gray-700 dark:text-gray-300 font-medium">{{
|
||||
c.client_ref || "Primer"
|
||||
}}</span>
|
||||
<p class="text-[11px] text-gray-400 dark:text-gray-500">
|
||||
Brez aktivnosti:
|
||||
{{ formatStaleDaysLabel(c.days_without_activity ?? c.days_stale) }}
|
||||
|
|
@ -479,16 +500,17 @@ function formatJobTime(ts) {
|
|||
<template v-if="f.contract">
|
||||
·
|
||||
<Link
|
||||
v-if="f.contract.client_case_uuid"
|
||||
:href="
|
||||
route('clientCase.show', {
|
||||
client_case: f.contract.client_case_uuid,
|
||||
segment: f.contract.segment_id,
|
||||
})
|
||||
safeCaseHref(f.contract.client_case_uuid, f.contract.segment_id)
|
||||
"
|
||||
class="text-indigo-600 dark:text-indigo-400 hover:underline"
|
||||
>
|
||||
{{ f.contract.reference || f.contract.uuid?.slice(0, 8) }}
|
||||
</Link>
|
||||
<span v-else class="text-gray-700 dark:text-gray-300">{{
|
||||
f.contract.reference || f.contract.uuid?.slice(0, 8)
|
||||
}}</span>
|
||||
<span
|
||||
v-if="f.contract.person_full_name"
|
||||
class="text-gray-500 dark:text-gray-400"
|
||||
|
|
|
|||
|
|
@ -18,6 +18,12 @@ const form = useForm({
|
|||
end_date: null,
|
||||
});
|
||||
|
||||
// Bulk selection form
|
||||
const bulkForm = useForm({
|
||||
contract_uuids: [],
|
||||
assigned_user_id: null,
|
||||
});
|
||||
|
||||
// Global search (applies to both tables)
|
||||
const search = ref("");
|
||||
|
||||
|
|
@ -68,6 +74,16 @@ function assign(contract) {
|
|||
form.post(route("fieldjobs.assign"));
|
||||
}
|
||||
|
||||
function assignSelected() {
|
||||
// Use the same selected user as in the single-assign dropdown
|
||||
bulkForm.assigned_user_id = form.assigned_user_id;
|
||||
bulkForm.post(route("fieldjobs.assign-bulk"), {
|
||||
onSuccess: () => {
|
||||
bulkForm.contract_uuids = [];
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function cancelAssignment(contract) {
|
||||
const payload = { contract_uuid: contract.uuid };
|
||||
form.transform(() => payload).post(route("fieldjobs.cancel"));
|
||||
|
|
@ -154,6 +170,7 @@ watch([search, assignedFilterUserId], () => {
|
|||
|
||||
// Column definitions for DataTableClient
|
||||
const unassignedColumns = [
|
||||
{ key: "_select", label: "", class: "w-8" },
|
||||
{ key: "reference", label: "Pogodba", sortable: true, class: "w-32" },
|
||||
{
|
||||
key: "case_person",
|
||||
|
|
@ -307,6 +324,22 @@ const assignedRows = computed(() =>
|
|||
<div v-if="form.errors.assigned_user_id" class="text-red-600 text-sm mt-1">
|
||||
{{ form.errors.assigned_user_id }}
|
||||
</div>
|
||||
<div class="mt-3 flex items-center gap-2">
|
||||
<button
|
||||
class="px-3 py-2 text-sm rounded bg-indigo-600 text-white disabled:opacity-50"
|
||||
:disabled="!bulkForm.contract_uuids.length || !form.assigned_user_id"
|
||||
@click="assignSelected"
|
||||
>
|
||||
Dodeli izbrane ({{ bulkForm.contract_uuids.length }})
|
||||
</button>
|
||||
<button
|
||||
class="px-3 py-2 text-sm rounded border border-gray-300 disabled:opacity-50"
|
||||
:disabled="!bulkForm.contract_uuids.length"
|
||||
@click="bulkForm.contract_uuids = []"
|
||||
>
|
||||
Počisti izbor
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<DataTableClient
|
||||
:columns="unassignedColumns"
|
||||
|
|
@ -317,6 +350,14 @@ const assignedRows = computed(() =>
|
|||
v-model:page="unassignedPage"
|
||||
v-model:pageSize="unassignedPageSize"
|
||||
>
|
||||
<template #cell-_select="{ row }">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="h-4 w-4"
|
||||
:value="row.uuid"
|
||||
v-model="bulkForm.contract_uuids"
|
||||
/>
|
||||
</template>
|
||||
<template #cell-case_person="{ row }">
|
||||
<Link
|
||||
v-if="row.client_case?.uuid"
|
||||
|
|
|
|||
|
|
@ -1321,7 +1321,7 @@ async function fetchSimulation() {
|
|||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<a
|
||||
:href="route('clientCase.show', row.case_uuid)"
|
||||
:href="route('clientCase.show', { client_case: row.case_uuid })"
|
||||
class="text-blue-600 hover:underline text-xs"
|
||||
>Odpri primer</a
|
||||
>
|
||||
|
|
|
|||
|
|
@ -291,6 +291,7 @@
|
|||
// field jobs assignment
|
||||
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-bulk', [FieldJobController::class, 'assignBulk'])->name('fieldjobs.assign-bulk');
|
||||
Route::post('field-jobs/cancel', [FieldJobController::class, 'cancel'])->name('fieldjobs.cancel');
|
||||
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');
|
||||
|
|
|
|||
55
tests/Feature/FieldJobBulkAssignTest.php
Normal file
55
tests/Feature/FieldJobBulkAssignTest.php
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Activity;
|
||||
use App\Models\ClientCase;
|
||||
use App\Models\Contract;
|
||||
use App\Models\FieldJob;
|
||||
use App\Models\FieldJobSetting;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
it('bulk assigns multiple contracts and skips already assigned', function () {
|
||||
$user = User::factory()->create();
|
||||
$assignee = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
// Minimal related data
|
||||
$case = ClientCase::factory()->create();
|
||||
$c1 = Contract::factory()->create(['client_case_id' => $case->id]);
|
||||
$c2 = Contract::factory()->create(['client_case_id' => $case->id]);
|
||||
$c3 = Contract::factory()->create(['client_case_id' => $case->id]);
|
||||
|
||||
// Existing active assignment for c3
|
||||
$setting = FieldJobSetting::factory()->create([
|
||||
'segment_id' => 1,
|
||||
'action_id' => 1,
|
||||
'assign_decision_id' => 1,
|
||||
'complete_decision_id' => 1,
|
||||
'return_segment_id' => 1,
|
||||
'queue_segment_id' => 1,
|
||||
]);
|
||||
FieldJob::create([
|
||||
'field_job_setting_id' => $setting->id,
|
||||
'assigned_user_id' => $assignee->id,
|
||||
'contract_id' => $c3->id,
|
||||
'assigned_at' => now(),
|
||||
]);
|
||||
|
||||
// Post bulk assign
|
||||
$resp = $this->post(route('fieldjobs.assign-bulk'), [
|
||||
'contract_uuids' => [$c1->uuid, $c2->uuid, $c3->uuid],
|
||||
'assigned_user_id' => $assignee->id,
|
||||
]);
|
||||
|
||||
$resp->assertRedirect();
|
||||
|
||||
// c1 and c2 should have active jobs
|
||||
expect(FieldJob::where('contract_id', $c1->id)->whereNull('cancelled_at')->whereNull('completed_at')->exists())->toBeTrue();
|
||||
expect(FieldJob::where('contract_id', $c2->id)->whereNull('cancelled_at')->whereNull('completed_at')->exists())->toBeTrue();
|
||||
// c3 should still have only one active job
|
||||
expect(FieldJob::where('contract_id', $c3->id)->whereNull('cancelled_at')->whereNull('completed_at')->count())->toBe(1);
|
||||
|
||||
// Activities were created for c1 and c2 (two records)
|
||||
expect(Activity::where('contract_id', $c1->id)->exists())->toBeTrue();
|
||||
expect(Activity::where('contract_id', $c2->id)->exists())->toBeTrue();
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user