Added condition options for events to trigger decisions
This commit is contained in:
parent
342d9d0700
commit
a5257df2b7
|
|
@ -7,6 +7,7 @@
|
||||||
use App\Models\Decision;
|
use App\Models\Decision;
|
||||||
use App\Models\EmailTemplate;
|
use App\Models\EmailTemplate;
|
||||||
use App\Models\Segment;
|
use App\Models\Segment;
|
||||||
|
use App\Services\DecisionEvents\ConditionEvaluator;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
|
|
@ -22,6 +23,8 @@ public function index(Request $request)
|
||||||
'email_templates' => EmailTemplate::query()->where('active', true)->get(['id', 'name', 'entity_types']),
|
'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']),
|
'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']),
|
'archive_settings' => ArchiveSetting::query()->where('enabled', true)->orderBy('id')->get(['id', 'name']),
|
||||||
|
'condition_fields' => ConditionEvaluator::availableFields(),
|
||||||
|
'condition_operators' => ConditionEvaluator::availableOperators(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,6 +86,9 @@ public function updateAction(int $id, Request $request)
|
||||||
|
|
||||||
public function storeDecision(Request $request)
|
public function storeDecision(Request $request)
|
||||||
{
|
{
|
||||||
|
$allowedConditionFields = collect(ConditionEvaluator::availableFields())->pluck('key')->implode(',');
|
||||||
|
$allowedOperators = 'in:=,!=,>,>=,<,<=,contains';
|
||||||
|
|
||||||
$attributes = $request->validate([
|
$attributes = $request->validate([
|
||||||
'name' => 'required|string|max:50',
|
'name' => 'required|string|max:50',
|
||||||
'color_tag' => 'nullable|string|max:25',
|
'color_tag' => 'nullable|string|max:25',
|
||||||
|
|
@ -96,6 +102,14 @@ public function storeDecision(Request $request)
|
||||||
'events.*.active' => 'sometimes|boolean',
|
'events.*.active' => 'sometimes|boolean',
|
||||||
'events.*.run_order' => 'nullable|integer',
|
'events.*.run_order' => 'nullable|integer',
|
||||||
'events.*.config' => 'nullable|array',
|
'events.*.config' => 'nullable|array',
|
||||||
|
'events.*.config.segment_id' => 'nullable|integer|exists:segments,id',
|
||||||
|
'events.*.config.deactivate_previous' => 'sometimes|boolean',
|
||||||
|
'events.*.config.archive_setting_id' => 'nullable|integer|exists:archive_settings,id',
|
||||||
|
'events.*.config.reactivate' => 'sometimes|boolean',
|
||||||
|
'events.*.config.conditions' => 'nullable|array',
|
||||||
|
'events.*.config.conditions.*.field' => "required_with:events.*.config.conditions.*|string|in:{$allowedConditionFields}",
|
||||||
|
'events.*.config.conditions.*.operator' => "required_with:events.*.config.conditions.*|string|{$allowedOperators}",
|
||||||
|
'events.*.config.conditions.*.value' => 'required_with:events.*.config.conditions.*',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$actionIds = collect($attributes['actions'] ?? [])->pluck('id')->toArray();
|
$actionIds = collect($attributes['actions'] ?? [])->pluck('id')->toArray();
|
||||||
|
|
@ -112,12 +126,12 @@ public function storeDecision(Request $request)
|
||||||
$key = $eventModel?->key ?? ($ev['key'] ?? null);
|
$key = $eventModel?->key ?? ($ev['key'] ?? null);
|
||||||
if ($key === 'add_segment') {
|
if ($key === 'add_segment') {
|
||||||
$seg = $ev['config']['segment_id'] ?? null;
|
$seg = $ev['config']['segment_id'] ?? null;
|
||||||
if (empty($seg) || ! Segment::where('id', $seg)->exists()) {
|
if (empty($seg)) {
|
||||||
$validationErrors["events.$i.config.segment_id"] = 'Please select a valid segment for the add_segment event.';
|
$validationErrors["events.$i.config.segment_id"] = 'Please select a valid segment for the add_segment event.';
|
||||||
}
|
}
|
||||||
} elseif ($key === 'archive_contract') {
|
} elseif ($key === 'archive_contract') {
|
||||||
$as = $ev['config']['archive_setting_id'] ?? null;
|
$as = $ev['config']['archive_setting_id'] ?? null;
|
||||||
if (empty($as) || ! ArchiveSetting::where('id', $as)->exists()) {
|
if (empty($as)) {
|
||||||
$validationErrors["events.$i.config.archive_setting_id"] = 'Please select a valid archive setting for the archive_contract event.';
|
$validationErrors["events.$i.config.archive_setting_id"] = 'Please select a valid archive setting for the archive_contract event.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -174,6 +188,9 @@ public function updateDecision(int $id, Request $request)
|
||||||
{
|
{
|
||||||
$row = Decision::findOrFail($id);
|
$row = Decision::findOrFail($id);
|
||||||
|
|
||||||
|
$allowedConditionFields = collect(ConditionEvaluator::availableFields())->pluck('key')->implode(',');
|
||||||
|
$allowedOperators = 'in:=,!=,>,>=,<,<=,contains';
|
||||||
|
|
||||||
$attributes = $request->validate([
|
$attributes = $request->validate([
|
||||||
'name' => 'required|string|max:50',
|
'name' => 'required|string|max:50',
|
||||||
'color_tag' => 'nullable|string|max:25',
|
'color_tag' => 'nullable|string|max:25',
|
||||||
|
|
@ -187,6 +204,14 @@ public function updateDecision(int $id, Request $request)
|
||||||
'events.*.active' => 'sometimes|boolean',
|
'events.*.active' => 'sometimes|boolean',
|
||||||
'events.*.run_order' => 'nullable|integer',
|
'events.*.run_order' => 'nullable|integer',
|
||||||
'events.*.config' => 'nullable|array',
|
'events.*.config' => 'nullable|array',
|
||||||
|
'events.*.config.segment_id' => 'nullable|integer|exists:segments,id',
|
||||||
|
'events.*.config.deactivate_previous' => 'sometimes|boolean',
|
||||||
|
'events.*.config.archive_setting_id' => 'nullable|integer|exists:archive_settings,id',
|
||||||
|
'events.*.config.reactivate' => 'sometimes|boolean',
|
||||||
|
'events.*.config.conditions' => 'nullable|array',
|
||||||
|
'events.*.config.conditions.*.field' => "required_with:events.*.config.conditions.*|string|in:{$allowedConditionFields}",
|
||||||
|
'events.*.config.conditions.*.operator' => "required_with:events.*.config.conditions.*|string|{$allowedOperators}",
|
||||||
|
'events.*.config.conditions.*.value' => 'required_with:events.*.config.conditions.*',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$actionIds = collect($attributes['actions'] ?? [])->pluck('id')->toArray();
|
$actionIds = collect($attributes['actions'] ?? [])->pluck('id')->toArray();
|
||||||
|
|
@ -203,12 +228,12 @@ public function updateDecision(int $id, Request $request)
|
||||||
$key = $eventModel?->key ?? ($ev['key'] ?? null);
|
$key = $eventModel?->key ?? ($ev['key'] ?? null);
|
||||||
if ($key === 'add_segment') {
|
if ($key === 'add_segment') {
|
||||||
$seg = $ev['config']['segment_id'] ?? null;
|
$seg = $ev['config']['segment_id'] ?? null;
|
||||||
if (empty($seg) || ! Segment::where('id', $seg)->exists()) {
|
if (empty($seg)) {
|
||||||
$validationErrors["events.$i.config.segment_id"] = 'Please select a valid segment for the add_segment event.';
|
$validationErrors["events.$i.config.segment_id"] = 'Please select a valid segment for the add_segment event.';
|
||||||
}
|
}
|
||||||
} elseif ($key === 'archive_contract') {
|
} elseif ($key === 'archive_contract') {
|
||||||
$as = $ev['config']['archive_setting_id'] ?? null;
|
$as = $ev['config']['archive_setting_id'] ?? null;
|
||||||
if (empty($as) || ! ArchiveSetting::where('id', $as)->exists()) {
|
if (empty($as)) {
|
||||||
$validationErrors["events.$i.config.archive_setting_id"] = 'Please select a valid archive setting for the archive_contract event.';
|
$validationErrors["events.$i.config.archive_setting_id"] = 'Please select a valid archive setting for the archive_contract event.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
use App\Models\Activity;
|
use App\Models\Activity;
|
||||||
use App\Models\Event as DecisionEventModel;
|
use App\Models\Event as DecisionEventModel;
|
||||||
|
use App\Services\DecisionEvents\ConditionEvaluator;
|
||||||
use App\Services\DecisionEvents\DecisionEventContext;
|
use App\Services\DecisionEvents\DecisionEventContext;
|
||||||
use App\Services\DecisionEvents\Registry;
|
use App\Services\DecisionEvents\Registry;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
|
|
@ -68,6 +69,23 @@ public function handle(): void
|
||||||
user: $activity->user,
|
user: $activity->user,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// [2] Condition check — skip the event if any condition is not met
|
||||||
|
$conditions = $this->config['conditions'] ?? [];
|
||||||
|
if (! empty($conditions)) {
|
||||||
|
$conditionsMet = app(ConditionEvaluator::class)->evaluate($conditions, $context);
|
||||||
|
if (! $conditionsMet) {
|
||||||
|
DB::table('decision_event_logs')->where('idempotency_key', $idempotencyKey)->update([
|
||||||
|
'status' => 'skipped',
|
||||||
|
'message' => 'Condition not met',
|
||||||
|
'finished_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [3] Resolve handler → handle()
|
||||||
$handler->handle($context, $this->config);
|
$handler->handle($context, $this->config);
|
||||||
|
|
||||||
DB::table('decision_event_logs')->where('idempotency_key', $idempotencyKey)->update([
|
DB::table('decision_event_logs')->where('idempotency_key', $idempotencyKey)->update([
|
||||||
|
|
|
||||||
123
app/Services/DecisionEvents/ConditionEvaluator.php
Normal file
123
app/Services/DecisionEvents/ConditionEvaluator.php
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\DecisionEvents;
|
||||||
|
|
||||||
|
class ConditionEvaluator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Returns true when ALL conditions pass (AND logic).
|
||||||
|
*
|
||||||
|
* Each condition: { field: string, operator: string, value: mixed }
|
||||||
|
*
|
||||||
|
* @param array<int, array{field: string, operator: string, value: mixed}> $conditions
|
||||||
|
*/
|
||||||
|
public function evaluate(array $conditions, DecisionEventContext $context): bool
|
||||||
|
{
|
||||||
|
foreach ($conditions as $condition) {
|
||||||
|
if (! $this->evaluateOne($condition, $context)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function evaluateOne(array $condition, DecisionEventContext $context): bool
|
||||||
|
{
|
||||||
|
$field = $condition['field'] ?? '';
|
||||||
|
$operator = $condition['operator'] ?? '=';
|
||||||
|
$expected = $condition['value'] ?? null;
|
||||||
|
|
||||||
|
$actual = $this->resolveField($field, $context);
|
||||||
|
|
||||||
|
return $this->compare($actual, $operator, $expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function resolveField(string $field, DecisionEventContext $context): mixed
|
||||||
|
{
|
||||||
|
return match ($field) {
|
||||||
|
'activity.amount' => $context->activity?->amount,
|
||||||
|
'activity.note' => $context->activity?->note,
|
||||||
|
'contract.active' => $context->contract !== null ? (bool) $context->contract->active : null,
|
||||||
|
'contract.account.balance_amount' => $this->resolveAccountBalance($context),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveAccountBalance(DecisionEventContext $context): mixed
|
||||||
|
{
|
||||||
|
if (! $context->contract) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$context->contract->loadMissing('account');
|
||||||
|
|
||||||
|
return $context->contract->account?->balance_amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function compare(mixed $actual, string $operator, mixed $expected): bool
|
||||||
|
{
|
||||||
|
if ($actual === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($operator, ['>', '>=', '<', '<='], true)) {
|
||||||
|
$actual = (float) $actual;
|
||||||
|
$expected = (float) $expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
return match ($operator) {
|
||||||
|
'=' => $actual == $expected,
|
||||||
|
'!=' => $actual != $expected,
|
||||||
|
'>' => $actual > $expected,
|
||||||
|
'>=' => $actual >= $expected,
|
||||||
|
'<' => $actual < $expected,
|
||||||
|
'<=' => $actual <= $expected,
|
||||||
|
'contains' => str_contains((string) $actual, (string) $expected),
|
||||||
|
default => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns available condition field definitions for the frontend.
|
||||||
|
*
|
||||||
|
* @return array<int, array{key: string, label: string, type: string}>
|
||||||
|
*/
|
||||||
|
public static function availableFields(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
['key' => 'activity.amount', 'label' => 'Aktivnost – znesek', 'type' => 'numeric'],
|
||||||
|
['key' => 'activity.note', 'label' => 'Aktivnost – opomba', 'type' => 'string'],
|
||||||
|
['key' => 'contract.active', 'label' => 'Pogodba – aktivna', 'type' => 'boolean'],
|
||||||
|
['key' => 'contract.account.balance_amount', 'label' => 'Račun – stanje', 'type' => 'numeric'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns available operators grouped by field type.
|
||||||
|
*
|
||||||
|
* @return array<string, array<int, array{key: string, label: string}>>
|
||||||
|
*/
|
||||||
|
public static function availableOperators(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'numeric' => [
|
||||||
|
['key' => '=', 'label' => 'je enako'],
|
||||||
|
['key' => '!=', 'label' => 'ni enako'],
|
||||||
|
['key' => '>', 'label' => 'je večje od'],
|
||||||
|
['key' => '>=', 'label' => 'je večje ali enako'],
|
||||||
|
['key' => '<', 'label' => 'je manjše od'],
|
||||||
|
['key' => '<=', 'label' => 'je manjše ali enako'],
|
||||||
|
],
|
||||||
|
'string' => [
|
||||||
|
['key' => '=', 'label' => 'je enako'],
|
||||||
|
['key' => '!=', 'label' => 'ni enako'],
|
||||||
|
['key' => 'contains', 'label' => 'vsebuje'],
|
||||||
|
],
|
||||||
|
'boolean' => [
|
||||||
|
['key' => '=', 'label' => 'je'],
|
||||||
|
['key' => '!=', 'label' => 'ni'],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,8 @@ const props = defineProps({
|
||||||
email_templates: { type: Array, default: () => [] },
|
email_templates: { type: Array, default: () => [] },
|
||||||
events: { type: Array, default: () => [] },
|
events: { type: Array, default: () => [] },
|
||||||
archive_settings: { type: Array, default: () => [] },
|
archive_settings: { type: Array, default: () => [] },
|
||||||
|
condition_fields: { type: Array, default: () => [] },
|
||||||
|
condition_operators: { type: Object, default: () => ({}) },
|
||||||
});
|
});
|
||||||
|
|
||||||
const activeTab = ref("actions");
|
const activeTab = ref("actions");
|
||||||
|
|
@ -57,6 +59,8 @@ const activeTab = ref("actions");
|
||||||
:available-events="events"
|
:available-events="events"
|
||||||
:segments="segments"
|
:segments="segments"
|
||||||
:archive-settings="archive_settings"
|
:archive-settings="archive_settings"
|
||||||
|
:condition-fields="condition_fields"
|
||||||
|
:condition-operators="condition_operators"
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,16 @@ import DataTableNew2 from "@/Components/DataTable/DataTableNew2.vue";
|
||||||
import InlineColorPicker from "@/Components/InlineColorPicker.vue";
|
import InlineColorPicker from "@/Components/InlineColorPicker.vue";
|
||||||
import Dropdown from "@/Components/Dropdown.vue";
|
import Dropdown from "@/Components/Dropdown.vue";
|
||||||
import AppPopover from "@/Components/app/ui/AppPopover.vue";
|
import AppPopover from "@/Components/app/ui/AppPopover.vue";
|
||||||
import { FilterIcon, MoreHorizontal, Pencil, Trash } from "lucide-vue-next";
|
import {
|
||||||
|
FilterIcon,
|
||||||
|
MoreHorizontal,
|
||||||
|
Pencil,
|
||||||
|
Plus,
|
||||||
|
Trash,
|
||||||
|
Trash2,
|
||||||
|
X,
|
||||||
|
} from "lucide-vue-next";
|
||||||
|
import { Switch } from "@/Components/ui/switch";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
|
@ -48,6 +57,8 @@ const props = defineProps({
|
||||||
availableEvents: { type: Array, default: () => [] },
|
availableEvents: { type: Array, default: () => [] },
|
||||||
segments: { type: Array, default: () => [] },
|
segments: { type: Array, default: () => [] },
|
||||||
archiveSettings: { type: Array, default: () => [] },
|
archiveSettings: { type: Array, default: () => [] },
|
||||||
|
conditionFields: { type: Array, default: () => [] },
|
||||||
|
conditionOperators: { type: Object, default: () => ({}) },
|
||||||
});
|
});
|
||||||
|
|
||||||
const drawerEdit = ref(false);
|
const drawerEdit = ref(false);
|
||||||
|
|
@ -223,6 +234,39 @@ function defaultEventPayload() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function operatorsForField(fieldKey) {
|
||||||
|
const field = (props.conditionFields || []).find((f) => f.key === fieldKey);
|
||||||
|
if (!field) {
|
||||||
|
return props.conditionOperators?.numeric ?? [];
|
||||||
|
}
|
||||||
|
return props.conditionOperators?.[field.type] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCondition(ev) {
|
||||||
|
if (!Array.isArray(ev.config.conditions)) {
|
||||||
|
ev.config.conditions = [];
|
||||||
|
}
|
||||||
|
const firstField = (props.conditionFields || [])[0];
|
||||||
|
const firstOperator = firstField
|
||||||
|
? operatorsForField(firstField.key)[0]?.key ?? "="
|
||||||
|
: "=";
|
||||||
|
ev.config.conditions.push({
|
||||||
|
field: firstField?.key ?? "",
|
||||||
|
operator: firstOperator,
|
||||||
|
value: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCondition(ev, idx) {
|
||||||
|
ev.config.conditions.splice(idx, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onConditionFieldChange(condition) {
|
||||||
|
const ops = operatorsForField(condition.field);
|
||||||
|
condition.operator = ops[0]?.key ?? "=";
|
||||||
|
condition.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
function tryAdoptRaw(ev) {
|
function tryAdoptRaw(ev) {
|
||||||
try {
|
try {
|
||||||
const obj = JSON.parse(ev.__rawJson || "{}");
|
const obj = JSON.parse(ev.__rawJson || "{}");
|
||||||
|
|
@ -532,7 +576,7 @@ const destroyDecision = () => {
|
||||||
<template #cell-auto_mail="{ row }">
|
<template #cell-auto_mail="{ row }">
|
||||||
<div class="flex flex-col text-sm">
|
<div class="flex flex-col text-sm">
|
||||||
<span :class="row.auto_mail ? 'text-green-700' : 'text-gray-500'">{{
|
<span :class="row.auto_mail ? 'text-green-700' : 'text-gray-500'">{{
|
||||||
row.auto_mail ? "Enabled" : "Disabled"
|
row.auto_mail ? "Vključeno" : "Izključeno"
|
||||||
}}</span>
|
}}</span>
|
||||||
<span v-if="row.auto_mail && row.email_template_id" class="text-gray-600">
|
<span v-if="row.auto_mail && row.email_template_id" class="text-gray-600">
|
||||||
Template:
|
Template:
|
||||||
|
|
@ -568,7 +612,7 @@ const destroyDecision = () => {
|
||||||
<Dialog v-model:open="drawerEdit">
|
<Dialog v-model:open="drawerEdit">
|
||||||
<DialogContent class="max-w-2xl max-h-[90vh] overflow-y-auto">
|
<DialogContent class="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Spremeni odločitev</DialogTitle>
|
<DialogTitle>Uredi odločitev</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -659,9 +703,16 @@ const destroyDecision = () => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 self-end">
|
<div class="flex items-center gap-2 self-end">
|
||||||
<label class="flex items-center gap-2 text-sm">
|
<label
|
||||||
<Checkbox v-model="ev.active" />
|
class="flex items-center gap-2 text-sm cursor-pointer select-none"
|
||||||
Aktivno
|
>
|
||||||
|
<Switch v-model="ev.active" />
|
||||||
|
<span
|
||||||
|
:class="
|
||||||
|
ev.active ? 'text-green-700 font-medium' : 'text-muted-foreground'
|
||||||
|
"
|
||||||
|
>{{ ev.active ? "Aktivno" : "Neaktivno" }}</span
|
||||||
|
>
|
||||||
</label>
|
</label>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
@ -707,7 +758,7 @@ const destroyDecision = () => {
|
||||||
<template v-else-if="eventKey(ev) === 'archive_contract'">
|
<template v-else-if="eventKey(ev) === 'archive_contract'">
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<InputLabel :for="`as-${idx}`" value="Archive setting" />
|
<InputLabel :for="`as-${idx}`" value="Nastavitev arhiva" />
|
||||||
<Select v-model="ev.config.archive_setting_id">
|
<Select v-model="ev.config.archive_setting_id">
|
||||||
<SelectTrigger :id="`as-${idx}`" class="w-full">
|
<SelectTrigger :id="`as-${idx}`" class="w-full">
|
||||||
<SelectValue placeholder="— Izberi nastavitev —" />
|
<SelectValue placeholder="— Izberi nastavitev —" />
|
||||||
|
|
@ -731,9 +782,14 @@ const destroyDecision = () => {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-end">
|
<div class="flex items-end">
|
||||||
<label class="flex items-center gap-2 text-sm mt-6">
|
<label
|
||||||
<Checkbox v-model:checked="ev.config.reactivate" />
|
class="flex items-center gap-2 text-sm mt-6 cursor-pointer select-none"
|
||||||
Reactivate namesto arhiva
|
>
|
||||||
|
<Switch
|
||||||
|
:model-value="ev.config.reactivate"
|
||||||
|
v-model:checked="ev.config.reactivate"
|
||||||
|
/>
|
||||||
|
Reaktiviraj namesto arhiviranja
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -749,7 +805,7 @@ const destroyDecision = () => {
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<!-- Fallback advanced editor for unknown event keys -->
|
<!-- Rezervni urejevalnik za neznane ključe dogodkov -->
|
||||||
<InputLabel :for="`cfg-${idx}`" value="Napredna nastavitev (JSON)" />
|
<InputLabel :for="`cfg-${idx}`" value="Napredna nastavitev (JSON)" />
|
||||||
<textarea
|
<textarea
|
||||||
:id="`cfg-${idx}`"
|
:id="`cfg-${idx}`"
|
||||||
|
|
@ -765,6 +821,104 @@ const destroyDecision = () => {
|
||||||
></textarea>
|
></textarea>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Conditions -->
|
||||||
|
<div v-if="conditionFields.length" class="mt-3 border-t pt-3">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<span
|
||||||
|
class="text-xs font-semibold text-muted-foreground uppercase tracking-wide"
|
||||||
|
>Pogoji za izvajanje</span
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="h-7 gap-1 text-xs"
|
||||||
|
@click="addCondition(ev)"
|
||||||
|
>
|
||||||
|
<Plus class="w-3 h-3" />
|
||||||
|
Dodaj pogoj
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
v-if="!ev.config.conditions?.length"
|
||||||
|
class="text-xs text-muted-foreground italic"
|
||||||
|
>
|
||||||
|
Brez pogojev — dogodek se vedno izvede.
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
v-for="(cond, cidx) in ev.config.conditions"
|
||||||
|
:key="cidx"
|
||||||
|
class="flex items-center gap-2 mt-1"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
v-model="cond.field"
|
||||||
|
@update:model-value="onConditionFieldChange(cond)"
|
||||||
|
class="w-48"
|
||||||
|
>
|
||||||
|
<SelectTrigger class="h-8 text-xs">
|
||||||
|
<SelectValue placeholder="Polje" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem
|
||||||
|
v-for="f in conditionFields"
|
||||||
|
:key="f.key"
|
||||||
|
:value="f.key"
|
||||||
|
class="text-xs"
|
||||||
|
>
|
||||||
|
{{ f.label }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Select v-model="cond.operator" class="w-36">
|
||||||
|
<SelectTrigger class="h-8 text-xs">
|
||||||
|
<SelectValue placeholder="Operator" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem
|
||||||
|
v-for="op in operatorsForField(cond.field)"
|
||||||
|
:key="op.key"
|
||||||
|
:value="op.key"
|
||||||
|
class="text-xs"
|
||||||
|
>
|
||||||
|
{{ op.label }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<template
|
||||||
|
v-if="
|
||||||
|
conditionFields.find((f) => f.key === cond.field)?.type ===
|
||||||
|
'boolean'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<Select v-model="cond.value" class="w-24">
|
||||||
|
<SelectTrigger class="h-8 text-xs">
|
||||||
|
<SelectValue placeholder="Vrednost" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="1" class="text-xs">Da</SelectItem>
|
||||||
|
<SelectItem value="0" class="text-xs">Ne</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<Input
|
||||||
|
v-model="cond.value"
|
||||||
|
class="h-8 text-xs w-28"
|
||||||
|
placeholder="Vrednost"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-7 w-7 text-red-500 hover:text-red-700"
|
||||||
|
@click="removeCondition(ev, cidx)"
|
||||||
|
>
|
||||||
|
<X class="w-3 h-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -782,7 +936,7 @@ const destroyDecision = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" @click="closeEditDrawer">Cancel</Button>
|
<Button variant="outline" @click="closeEditDrawer">Prekliči</Button>
|
||||||
<Button @click="update" :disabled="form.processing || !eventsValidEdit"
|
<Button @click="update" :disabled="form.processing || !eventsValidEdit"
|
||||||
>Shrani</Button
|
>Shrani</Button
|
||||||
>
|
>
|
||||||
|
|
@ -891,9 +1045,16 @@ const destroyDecision = () => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 self-end">
|
<div class="flex items-center gap-2 self-end">
|
||||||
<label class="flex items-center gap-2 text-sm">
|
<label
|
||||||
<Checkbox v-model:checked="ev.active" />
|
class="flex items-center gap-2 text-sm cursor-pointer select-none"
|
||||||
Aktivno
|
>
|
||||||
|
<Switch v-model="ev.active" />
|
||||||
|
<span
|
||||||
|
:class="
|
||||||
|
ev.active ? 'text-green-700 font-medium' : 'text-muted-foreground'
|
||||||
|
"
|
||||||
|
>{{ ev.active ? "Aktivno" : "Neaktivno" }}</span
|
||||||
|
>
|
||||||
</label>
|
</label>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
@ -939,7 +1100,7 @@ const destroyDecision = () => {
|
||||||
<template v-else-if="eventKey(ev) === 'archive_contract'">
|
<template v-else-if="eventKey(ev) === 'archive_contract'">
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<InputLabel :for="`cas-${idx}`" value="Archive setting" />
|
<InputLabel :for="`cas-${idx}`" value="Nastavitev arhiva" />
|
||||||
<Select v-model="ev.config.archive_setting_id">
|
<Select v-model="ev.config.archive_setting_id">
|
||||||
<SelectTrigger :id="`cas-${idx}`" class="w-full">
|
<SelectTrigger :id="`cas-${idx}`" class="w-full">
|
||||||
<SelectValue placeholder="— Izberi nastavitev —" />
|
<SelectValue placeholder="— Izberi nastavitev —" />
|
||||||
|
|
@ -965,9 +1126,14 @@ const destroyDecision = () => {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-end">
|
<div class="flex items-end">
|
||||||
<label class="flex items-center gap-2 text-sm mt-6">
|
<label
|
||||||
<Checkbox v-model:checked="ev.config.reactivate" />
|
class="flex items-center gap-2 text-sm mt-6 cursor-pointer select-none"
|
||||||
Reactivate namesto arhiva
|
>
|
||||||
|
<Switch
|
||||||
|
:model-value="ev.config.reactivate"
|
||||||
|
v-model:checked="ev.config.reactivate"
|
||||||
|
/>
|
||||||
|
Reaktiviraj namesto arhiviranja
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -998,6 +1164,104 @@ const destroyDecision = () => {
|
||||||
></textarea>
|
></textarea>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Conditions -->
|
||||||
|
<div v-if="conditionFields.length" class="mt-3 border-t pt-3">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<span
|
||||||
|
class="text-xs font-semibold text-muted-foreground uppercase tracking-wide"
|
||||||
|
>Pogoji za izvajanje</span
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="h-7 gap-1 text-xs"
|
||||||
|
@click="addCondition(ev)"
|
||||||
|
>
|
||||||
|
<Plus class="w-3 h-3" />
|
||||||
|
Dodaj pogoj
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
v-if="!ev.config.conditions?.length"
|
||||||
|
class="text-xs text-muted-foreground italic"
|
||||||
|
>
|
||||||
|
Brez pogojev — dogodek se vedno izvede.
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
v-for="(cond, cidx) in ev.config.conditions"
|
||||||
|
:key="cidx"
|
||||||
|
class="flex items-center gap-2 mt-1"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
v-model="cond.field"
|
||||||
|
@update:model-value="onConditionFieldChange(cond)"
|
||||||
|
class="w-48"
|
||||||
|
>
|
||||||
|
<SelectTrigger class="h-8 text-xs">
|
||||||
|
<SelectValue placeholder="Polje" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem
|
||||||
|
v-for="f in conditionFields"
|
||||||
|
:key="f.key"
|
||||||
|
:value="f.key"
|
||||||
|
class="text-xs"
|
||||||
|
>
|
||||||
|
{{ f.label }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Select v-model="cond.operator" class="w-36">
|
||||||
|
<SelectTrigger class="h-8 text-xs">
|
||||||
|
<SelectValue placeholder="Operator" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem
|
||||||
|
v-for="op in operatorsForField(cond.field)"
|
||||||
|
:key="op.key"
|
||||||
|
:value="op.key"
|
||||||
|
class="text-xs"
|
||||||
|
>
|
||||||
|
{{ op.label }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<template
|
||||||
|
v-if="
|
||||||
|
conditionFields.find((f) => f.key === cond.field)?.type ===
|
||||||
|
'boolean'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<Select v-model="cond.value" class="w-24">
|
||||||
|
<SelectTrigger class="h-8 text-xs">
|
||||||
|
<SelectValue placeholder="Vrednost" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="1" class="text-xs">Da</SelectItem>
|
||||||
|
<SelectItem value="0" class="text-xs">Ne</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<Input
|
||||||
|
v-model="cond.value"
|
||||||
|
class="h-8 text-xs w-28"
|
||||||
|
placeholder="Vrednost"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-7 w-7 text-red-500 hover:text-red-700"
|
||||||
|
@click="removeCondition(ev, cidx)"
|
||||||
|
>
|
||||||
|
<X class="w-3 h-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -1015,7 +1279,7 @@ const destroyDecision = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" @click="closeCreateDrawer">Cancel</Button>
|
<Button variant="outline" @click="closeCreateDrawer">Prekliči</Button>
|
||||||
<Button @click="store" :disabled="createForm.processing || !eventsValidCreate"
|
<Button @click="store" :disabled="createForm.processing || !eventsValidCreate"
|
||||||
>Dodaj</Button
|
>Dodaj</Button
|
||||||
>
|
>
|
||||||
|
|
@ -1026,15 +1290,15 @@ const destroyDecision = () => {
|
||||||
<AlertDialog v-model:open="showDelete">
|
<AlertDialog v-model:open="showDelete">
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Delete decision</AlertDialogTitle>
|
<AlertDialogTitle>Zbriši odločitev</AlertDialogTitle>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<div class="text-sm text-muted-foreground">
|
<div class="text-sm text-muted-foreground">
|
||||||
Are you sure you want to delete decision "{{ toDelete?.name }}"? This cannot be
|
Ali ste prepričani, da želite zbrisati odločitev "{{ toDelete?.name }}"? Tega
|
||||||
undone.
|
dejanja ni mogoče razveljaviti.
|
||||||
</div>
|
</div>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<Button variant="outline" @click="cancelDelete">Cancel</Button>
|
<Button variant="outline" @click="cancelDelete">Prekliči</Button>
|
||||||
<Button variant="destructive" @click="destroyDecision">Delete</Button>
|
<Button variant="destructive" @click="destroyDecision">Zbriši</Button>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user