From 04f31e62aa76f8151643581a1389234ea1205291 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Pocrnji=C4=8D?= Date: Thu, 16 Oct 2025 21:28:40 +0200 Subject: [PATCH] field job added option to add multiple contracts to user at once --- app/Http/Controllers/FieldJobController.php | 71 +++++++++++++++++++ database/factories/ActionFactory.php | 2 + database/factories/FieldJobSettingFactory.php | 37 ++++++++++ resources/js/Pages/Dashboard.vue | 56 ++++++++++----- resources/js/Pages/FieldJob/Index.vue | 41 +++++++++++ resources/js/Pages/Imports/Import.vue | 2 +- routes/web.php | 1 + tests/Feature/FieldJobBulkAssignTest.php | 55 ++++++++++++++ 8 files changed, 247 insertions(+), 18 deletions(-) create mode 100644 database/factories/FieldJobSettingFactory.php create mode 100644 tests/Feature/FieldJobBulkAssignTest.php diff --git a/app/Http/Controllers/FieldJobController.php b/app/Http/Controllers/FieldJobController.php index 17d3aeb..89683bf 100644 --- a/app/Http/Controllers/FieldJobController.php +++ b/app/Http/Controllers/FieldJobController.php @@ -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([ diff --git a/database/factories/ActionFactory.php b/database/factories/ActionFactory.php index 2af92b8..c80c2f8 100644 --- a/database/factories/ActionFactory.php +++ b/database/factories/ActionFactory.php @@ -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(), ]; } } diff --git a/database/factories/FieldJobSettingFactory.php b/database/factories/FieldJobSettingFactory.php new file mode 100644 index 0000000..d3e6d2c --- /dev/null +++ b/database/factories/FieldJobSettingFactory.php @@ -0,0 +1,37 @@ + + */ +class FieldJobSettingFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + 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, + ]; + } +} diff --git a/resources/js/Pages/Dashboard.vue b/resources/js/Pages/Dashboard.vue index e415455..d69c042 100644 --- a/resources/js/Pages/Dashboard.vue +++ b/resources/js/Pages/Dashboard.vue @@ -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}`; + } +}