Action::query()->with(['decisions', 'segment'])->withCount('activities')->orderBy('id')->get(), 'decisions' => Decision::query()->with(['actions', 'events'])->withCount('activities')->orderBy('id')->get(), 'segments' => Segment::query()->get(), 'email_templates' => EmailTemplate::query()->where('active', true)->get(['id', 'name', 'entity_types']), 'events' => \App\Models\Event::query()->orderBy('name')->get(['id', 'name', 'key', 'description', 'active']), 'archive_settings' => ArchiveSetting::query()->where('enabled', true)->orderBy('id')->get(['id', 'name']), ]); } public function storeAction(Request $request) { $attributes = $request->validate([ 'name' => 'required|string|max:50', 'color_tag' => 'nullable|string|max:25', 'segment_id' => 'nullable|integer|exists:segments,id', 'decisions' => 'nullable|array', 'decisions.*.id' => 'required_with:decisions.*|integer|exists:decisions,id', 'decisions.*.name' => 'required_with:decisions.*|string|max:50', ]); $decisionIds = collect($attributes['decisions'] ?? [])->pluck('id')->toArray(); \DB::transaction(function () use ($attributes, $decisionIds) { /** @var \App\Models\Action $row */ $row = Action::create([ 'name' => $attributes['name'], 'color_tag' => $attributes['color_tag'] ?? null, 'segment_id' => $attributes['segment_id'] ?? null, ]); if (! empty($decisionIds)) { $row->decisions()->sync($decisionIds); } }); return to_route('settings.workflow')->with('success', 'Action created successfully!'); } public function updateAction(int $id, Request $request) { $row = Action::findOrFail($id); $attributes = $request->validate([ 'name' => 'required|string|max:50', 'color_tag' => 'nullable|string|max:25', 'segment_id' => 'nullable|integer|exists:segments,id', 'decisions' => 'nullable|array', 'decisions.*.id' => 'required_with:decisions.*|integer|exists:decisions,id', 'decisions.*.name' => 'required_with:decisions.*|string|max:50', ]); $decisionIds = collect($attributes['decisions'] ?? [])->pluck('id')->toArray(); \DB::transaction(function () use ($attributes, $decisionIds, $row) { $row->update([ 'name' => $attributes['name'], 'color_tag' => $attributes['color_tag'], 'segment_id' => $attributes['segment_id'] ?? null, ]); $row->decisions()->sync($decisionIds); }); return to_route('settings.workflow')->with('success', 'Update successful!'); } public function storeDecision(Request $request) { $attributes = $request->validate([ 'name' => 'required|string|max:50', 'color_tag' => 'nullable|string|max:25', 'auto_mail' => 'sometimes|boolean', 'email_template_id' => 'nullable|integer|exists:email_templates,id', 'actions' => 'nullable|array', 'actions.*.id' => 'required_with:actions.*|integer|exists:actions,id', 'actions.*.name' => 'required_with:actions.*|string|max:50', 'events' => 'nullable|array', 'events.*.id' => 'required_with:events.*|integer|exists:events,id', 'events.*.active' => 'sometimes|boolean', 'events.*.run_order' => 'nullable|integer', 'events.*.config' => 'nullable|array', ]); $actionIds = collect($attributes['actions'] ?? [])->pluck('id')->toArray(); $eventsPayload = collect($attributes['events'] ?? []); // Extra server-side validation for event-specific config keys $validationErrors = []; foreach ($eventsPayload as $i => $ev) { $idEv = isset($ev['id']) ? (int) $ev['id'] : null; if (! $idEv) { continue; } $eventModel = \App\Models\Event::find($idEv); $key = $eventModel?->key ?? ($ev['key'] ?? null); if ($key === 'add_segment') { $seg = $ev['config']['segment_id'] ?? null; if (empty($seg) || ! Segment::where('id', $seg)->exists()) { $validationErrors["events.$i.config.segment_id"] = 'Please select a valid segment for the add_segment event.'; } } elseif ($key === 'archive_contract') { $as = $ev['config']['archive_setting_id'] ?? null; if (empty($as) || ! ArchiveSetting::where('id', $as)->exists()) { $validationErrors["events.$i.config.archive_setting_id"] = 'Please select a valid archive setting for the archive_contract event.'; } } } if (! empty($validationErrors)) { throw ValidationException::withMessages($validationErrors); } \DB::transaction(function () use ($attributes, $actionIds, $eventsPayload) { /** @var \App\Models\Decision $row */ $row = Decision::create([ 'name' => $attributes['name'], 'color_tag' => $attributes['color_tag'] ?? null, 'auto_mail' => (bool) ($attributes['auto_mail'] ?? false), 'email_template_id' => $attributes['email_template_id'] ?? null, ]); if (! empty($actionIds)) { $row->actions()->sync($actionIds); } // Attach decision events with pivot attributes if ($eventsPayload->isNotEmpty()) { $sync = []; foreach ($eventsPayload as $ev) { $id = (int) ($ev['id'] ?? 0); if ($id <= 0) { continue; } $cfg = $ev['config'] ?? null; if (is_array($cfg)) { $cfg = json_encode($cfg); } elseif (is_string($cfg)) { $trim = trim($cfg); $cfg = $trim === '' ? null : $cfg; } else { $cfg = null; } $sync[$id] = [ 'active' => (bool) ($ev['active'] ?? true), 'run_order' => isset($ev['run_order']) ? (int) $ev['run_order'] : null, 'config' => $cfg, ]; } $row->events()->sync($sync); } }); return to_route('settings.workflow')->with('success', 'Decision created successfully!'); } public function updateDecision(int $id, Request $request) { $row = Decision::findOrFail($id); $attributes = $request->validate([ 'name' => 'required|string|max:50', 'color_tag' => 'nullable|string|max:25', 'auto_mail' => 'sometimes|boolean', 'email_template_id' => 'nullable|integer|exists:email_templates,id', 'actions' => 'nullable|array', 'actions.*.id' => 'required_with:actions.*|integer|exists:actions,id', 'actions.*.name' => 'required_with:actions.*|string|max:50', 'events' => 'nullable|array', 'events.*.id' => 'required_with:events.*|integer|exists:events,id', 'events.*.active' => 'sometimes|boolean', 'events.*.run_order' => 'nullable|integer', 'events.*.config' => 'nullable|array', ]); $actionIds = collect($attributes['actions'] ?? [])->pluck('id')->toArray(); $eventsPayload = collect($attributes['events'] ?? []); // Extra server-side validation for event-specific config keys $validationErrors = []; foreach ($eventsPayload as $i => $ev) { $idEv = isset($ev['id']) ? (int) $ev['id'] : null; if (! $idEv) { continue; } $eventModel = \App\Models\Event::find($idEv); $key = $eventModel?->key ?? ($ev['key'] ?? null); if ($key === 'add_segment') { $seg = $ev['config']['segment_id'] ?? null; if (empty($seg) || ! Segment::where('id', $seg)->exists()) { $validationErrors["events.$i.config.segment_id"] = 'Please select a valid segment for the add_segment event.'; } } elseif ($key === 'archive_contract') { $as = $ev['config']['archive_setting_id'] ?? null; if (empty($as) || ! ArchiveSetting::where('id', $as)->exists()) { $validationErrors["events.$i.config.archive_setting_id"] = 'Please select a valid archive setting for the archive_contract event.'; } } } if (! empty($validationErrors)) { throw ValidationException::withMessages($validationErrors); } \DB::transaction(function () use ($attributes, $actionIds, $eventsPayload, $row) { $row->update([ 'name' => $attributes['name'], 'color_tag' => $attributes['color_tag'] ?? null, 'auto_mail' => (bool) ($attributes['auto_mail'] ?? false), 'email_template_id' => $attributes['email_template_id'] ?? null, ]); $row->actions()->sync($actionIds); // Sync decision events with pivot attributes if ($eventsPayload->isNotEmpty()) { $sync = []; foreach ($eventsPayload as $ev) { $id = (int) ($ev['id'] ?? 0); if ($id <= 0) { continue; } $cfg = $ev['config'] ?? null; // ensure string JSON stored; accept already-JSON strings if (is_array($cfg)) { $cfg = json_encode($cfg); } elseif (is_string($cfg)) { $trim = trim($cfg); // If not valid JSON, keep raw string (handler side can parse/ignore) $cfg = $trim === '' ? null : $cfg; } else { $cfg = null; } $sync[$id] = [ 'active' => (bool) ($ev['active'] ?? true), 'run_order' => isset($ev['run_order']) ? (int) $ev['run_order'] : null, 'config' => $cfg, ]; } $row->events()->sync($sync); } else { // If empty provided explicitly, detach all to reflect UI intent if (array_key_exists('events', $attributes)) { $row->events()->detach(); } } }); return to_route('settings.workflow')->with('success', 'Decision updated successfully!'); } public function destroyAction(int $id) { $row = Action::findOrFail($id); if ($row->activities()->exists()) { return back()->with('error', 'Cannot delete action because dependent activities exist.'); } \DB::transaction(function () use ($row) { $row->decisions()->detach(); $row->delete(); }); return back()->with('success', 'Action deleted successfully!'); } public function destroyDecision(int $id) { $row = Decision::findOrFail($id); if ($row->activities()->exists()) { return back()->with('error', 'Cannot delete decision because dependent activities exist.'); } \DB::transaction(function () use ($row) { $row->actions()->detach(); $row->delete(); }); return back()->with('success', 'Decision deleted successfully!'); } }