dbxapp 4.1.3
CMS, Shop, Workflows und modulare Geschäftsanwendungen
Loading...
Searching...
No Matches
dbxWorkflowAdmin.class.php
Go to the documentation of this file.
1<?php
2namespace dbx\dbxWorkflow_admin;
3
5
6 private $ddDefinition = 'dbxWorkflow|workflowDefinition';
7 private $ddInstance = 'dbxWorkflow|workflowInstance';
8 private $ddStep = 'dbxWorkflow|workflowStep';
9 private $ddBind = 'dbxWorkflow|workflowModuleBind';
10
11 private function tpl() {
12 return dbx()->get_system_obj('dbxTPL');
13 }
14
15 private function db() {
16 return dbx()->get_system_obj('dbxDB');
17 }
18
19 private function h($value) {
20 return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
21 }
22
23 private function alert($type, $msg) {
24 return $this->tpl()->get_tpl('dbx|alert-' . $type, array('msg' => $msg));
25 }
26
27 private function frame($content, $texts = null) {
28 $help = dbx()->get_include_obj('dbxAdminHelp', 'dbxAdmin');
29 $topic = $help->resolveTopic('dbxWorkflow_admin');
30 $title = $texts
31 ? $texts->get_fd_message('bar_title', 'Workflow Definitionen')
32 : 'Workflow Definitionen';
33 $subtitle = $texts
34 ? $texts->get_fd_message('bar_subtitle', 'Workflow-Definitionen')
35 : 'Workflow-Definitionen';
36 $moduleBindings = $texts
37 ? $texts->get_fd_message('module_bindings', 'Modul-Bindings')
38 : 'Modul-Bindings';
39 $newBinding = $texts
40 ? $texts->get_fd_message('new_binding', 'Neues Binding')
41 : 'Neues Binding';
42 $newWorkflow = $texts
43 ? $texts->get_fd_message('new_workflow', 'Neuer Workflow')
44 : 'Neuer Workflow';
45 $barActions = ''
46 . '<a class="btn btn-outline-secondary btn-sm" href="?dbx_modul=dbxWorkflow_admin&dbx_run1=binds"><i class="bi bi-plug"></i> ' . $this->h($moduleBindings) . '</a>'
47 . '<a class="btn btn-primary btn-sm" href="?dbx_modul=dbxWorkflow_admin&dbx_run1=edit_bind&rid=new"><i class="bi bi-plus-circle"></i> ' . $this->h($newBinding) . '</a>'
48 . '<a class="btn btn-primary btn-sm" href="?dbx_modul=dbxWorkflow_admin&dbx_run1=edit&rid=new"><i class="bi bi-plus-circle"></i> ' . $this->h($newWorkflow) . '</a>';
49
50 return $this->tpl()->get_tpl('dbxWorkflow_admin|workflow-admin-actions', array_merge(
51 $help->moduleBarTemplateData($topic, $barActions),
52 array(
53 'bar_title' => $this->h($title),
54 'bar_subtitle' => $this->h($subtitle),
55 'bar_icon' => 'bi-diagram-3',
56 'bar_actions' => $barActions,
57 'content' => $content,
58 )
59 ));
60 }
61
62 private function install() {
63 $oDD = dbx()->get_system_obj('dbxDD');
64 foreach (array('workflowDefinition', 'workflowInstance', 'workflowStep', 'workflowModuleBind') as $dd) {
65 $oDD->sync_dd_to_db('dbxWorkflow', $dd, 'reset');
66 for ($i = 0; $i < 80; $i++) {
67 $state = $oDD->sync_dd_to_db('dbxWorkflow', $dd, 'apply');
68 $status = (string)($state['status'] ?? '');
69 if ($status === 'finished' || $status === 'error') break;
70 }
71 }
72
73 $engine = dbx()->get_include_obj('dbxWorkflowEngine', 'dbxWorkflow');
74 $engine->seed_demo_definitions();
75
76 return $this->frame($this->alert('success', 'Workflow-Datenbank installiert. Definitionen, Modul-Bindings und Demo-Workflows wurden angelegt/aktualisiert.'));
77 }
78
79 private function default_definition_text() {
80 return dbx()->get_include_obj('dbxWorkflowEngine', 'dbxWorkflow')->default_definition_text();
81 }
82
83 private function load_row($rid) {
84 if ($rid === 'new' || (int)$rid <= 0) {
85 return array(
86 'workflow_key' => 'rechnung_erstellen',
87 'title' => 'Rechnung erstellen',
88 'result_label' => 'Rechnung',
89 'description' => 'Alle für eine richtige Rechnung benötigten Angaben werden geprüft und anschließend in klaren Schritten vervollständigt.',
90 'definition_json' => $this->default_definition_text(),
91 'active' => 1
92 );
93 }
94
95 $rows = $this->db()->select($this->ddDefinition, array('id' => (int)$rid), '*', 'id', 'DESC', '', 1, 0, 0);
96 return (is_array($rows) && isset($rows[0])) ? $rows[0] : array();
97 }
98
99 private function normalize_key($value) {
100 $value = strtolower(trim((string)$value));
101 $value = preg_replace('/[^a-z0-9_]+/', '_', $value);
102 return trim($value, '_');
103 }
104
105 private function workflow_action_options($selected, $texts) {
106 $items = array(
107 'form' => $texts->get_fd_message('action_form'),
108 'select' => $texts->get_fd_message('action_select'),
109 'create' => $texts->get_fd_message('action_create'),
110 'module' => $texts->get_fd_message('action_module'),
111 );
112 $html = '';
113 foreach ($items as $value => $label) {
114 $html .= '<option value="' . $this->h($value) . '"' . ((string)$selected === $value ? ' selected' : '') . '>' . $this->h($label) . '</option>';
115 }
116 return $html;
117 }
118
119 private function workflow_action_choices($index, array $selected, $texts) {
120 $items = array(
121 'form' => $texts->get_fd_message('action_form'),
122 'select' => $texts->get_fd_message('action_select'),
123 'create' => $texts->get_fd_message('action_create'),
124 'module' => $texts->get_fd_message('action_module_short'),
125 );
126 $selected = array_fill_keys(array_map('strval', $selected), true);
127 $html = '';
128 foreach ($items as $value => $label) {
129 $id = 'workflow-step-action-' . (int)$index . '-' . $value;
130 $html .= '<label class="dbx-workflow-action-choice" for="' . $id . '">'
131 . '<input class="form-check-input" type="checkbox" id="' . $id . '" name="workflow_step_actions[' . (int)$index . '][]" value="' . $value . '" data-workflow-action-choice'
132 . (isset($selected[$value]) ? ' checked' : '') . '>'
133 . '<span>' . $this->h($label) . '</span></label>';
134 }
135 return $html;
136 }
137
138 private function workflow_kind_labels($texts): array {
139 return array(
140 'input' => $texts->get_fd_message('kind_input'),
141 'action' => $texts->get_fd_message('kind_action'),
142 'check' => $texts->get_fd_message('kind_check'),
143 'decision' => $texts->get_fd_message('kind_decision'),
144 );
145 }
146
147 private function workflow_kind_options($selected, $texts) {
148 $items = $this->workflow_kind_labels($texts);
149 $html = '';
150 foreach ($items as $value => $label) {
151 $html .= '<option value="' . $this->h($value) . '"' . ((string)$selected === $value ? ' selected' : '') . '>' . $this->h($label) . '</option>';
152 }
153 return $html;
154 }
155
156 private function workflow_automation_options($selected, $texts) {
157 $items = array(
158 'manual' => $texts->get_fd_message('automation_manual'),
159 'observe' => $texts->get_fd_message('automation_observe'),
160 );
161 $html = '';
162 foreach ($items as $value => $label) {
163 $html .= '<option value="' . $this->h($value) . '"' . ((string)$selected === $value ? ' selected' : '') . '>' . $this->h($label) . '</option>';
164 }
165 return $html;
166 }
167
168 private function workflow_mode_options($selected, $texts) {
169 $items = array(
170 'single' => $texts->get_fd_message('mode_single'),
171 'multiple' => $texts->get_fd_message('mode_multiple'),
172 );
173 $html = '';
174 foreach ($items as $value => $label) {
175 $html .= '<option value="' . $this->h($value) . '"' . ((string)$selected === $value ? ' selected' : '') . '>' . $this->h($label) . '</option>';
176 }
177 return $html;
178 }
179
180 private function workflow_validation_labels($texts): array {
181 return array(
182 'exactly_one' => $texts->get_fd_message('validation_exactly_one'),
183 'at_least_one' => $texts->get_fd_message('validation_at_least_one'),
184 'not_empty' => $texts->get_fd_message('validation_not_empty'),
185 'positive_integer' => $texts->get_fd_message('validation_positive_integer'),
186 'confirmed' => $texts->get_fd_message('validation_confirmed'),
187 );
188 }
189
190 private function workflow_validation_options($selected, $texts) {
191 $items = $this->workflow_validation_labels($texts);
192 $html = '';
193 foreach ($items as $value => $label) {
194 $html .= '<option value="' . $this->h($value) . '"' . ((string)$selected === $value ? ' selected' : '') . '>' . $this->h($label) . '</option>';
195 }
196 return $html;
197 }
198
199 private function workflow_default_question(string $label, $texts): string {
200 $label = trim($label) !== '' ? trim($label) : $texts->get_fd_message('default_value_label');
201 return $texts->format_fd_message('default_question', array('label' => $label));
202 }
203
204 private function workflow_default_missing_message(string $label, $texts): string {
205 $label = trim($label) !== '' ? trim($label) : $texts->get_fd_message('default_value_label');
206 return $texts->format_fd_message('default_missing_message', array('label' => $label));
207 }
208
209 private function workflow_resolver_label(string $action, $texts): string {
210 $labels = array(
211 'form' => $texts->get_fd_message('resolver_form'),
212 'select' => $texts->get_fd_message('resolver_select'),
213 'create' => $texts->get_fd_message('resolver_create'),
214 'module' => $texts->get_fd_message('resolver_module'),
215 );
216 return $labels[$action] ?? $labels['form'];
217 }
218
219 private function workflow_depends_options(array $needs, $selected, $texts) {
220 $html = '<option value="">' . $this->h($texts->get_fd_message('depends_always')) . '</option>';
221 foreach ($needs as $need) {
222 $key = (string)($need['key'] ?? '');
223 if ($key === '') continue;
224 $html .= '<option value="' . $this->h($key) . '"' . ((string)$selected === $key ? ' selected' : '') . '>' . $this->h($need['label'] ?? $key) . '</option>';
225 }
226 return $html;
227 }
228
229 private function workflow_options_text($options) {
230 $lines = array();
231 foreach ((array)$options as $option) {
232 if (is_array($option)) {
233 $value = trim((string)($option['value'] ?? ''));
234 $label = trim((string)($option['label'] ?? $value));
235 if ($value !== '' || $label !== '') {
236 $lines[] = $value !== '' && $value !== $label ? $value . '=' . $label : $label;
237 }
238 } else {
239 $text = trim((string)$option);
240 if ($text !== '') $lines[] = $text;
241 }
242 }
243 return implode("\n", $lines);
244 }
245
246 private function parse_workflow_options($text) {
247 $options = array();
248 foreach (preg_split('/\r\n|\r|\n|,/', (string)$text) as $line) {
249 $line = trim($line);
250 if ($line === '') continue;
251 if (strpos($line, '=') !== false) {
252 list($value, $label) = explode('=', $line, 2);
253 $value = trim($value);
254 $label = trim($label);
255 if ($value !== '' || $label !== '') {
256 $options[] = array('value' => $value !== '' ? $value : $label, 'label' => $label !== '' ? $label : $value);
257 }
258 } else {
259 $options[] = $line;
260 }
261 }
262 return $options;
263 }
264
265 private function workflow_definition_from_data(array $data): array {
266 $engine = dbx()->get_include_obj('dbxWorkflowEngine', 'dbxWorkflow');
267 $definition = $engine->normalize_definition($data['definition_json'] ?? $this->default_definition_text(), $data['workflow_key'] ?? '');
268 if (trim((string)($data['workflow_key'] ?? '')) !== '') $definition['workflow_key'] = trim((string)$data['workflow_key']);
269 if (trim((string)($data['title'] ?? '')) !== '') $definition['title'] = trim((string)$data['title']);
270 if (trim((string)($data['result_label'] ?? '')) !== '') $definition['result'] = trim((string)$data['result_label']);
271 if (array_key_exists('description', $data)) $definition['description'] = (string)$data['description'];
272 return $definition;
273 }
274
275 private function workflow_builder_html(array $data, $texts) {
276 $definition = $this->workflow_definition_from_data($data);
277 $needs = array_values((array)($definition['needs'] ?? array()));
278 $rowCount = max(14, count($needs) + 6);
279 $rows = '';
280
281 for ($i = 0; $i < $rowCount; $i++) {
282 $need = $needs[$i] ?? array();
283 $isEmpty = empty($need);
284 $key = (string)($need['key'] ?? '');
285 $label = (string)($need['label'] ?? '');
286 $allowedActions = array('form', 'select', 'create', 'module');
287 $actions = array_values(array_unique(array_intersect(array_map('strval', (array)($need['actions'] ?? array('form'))), $allowedActions)));
288 if (!$actions) $actions = array('form');
289 $action = (string)($need['preferred'] ?? '');
290 if (!in_array($action, $actions, true)) $action = (string)$actions[0];
291 $kind = (string)($need['kind'] ?? '');
292 if (!in_array($kind, array('input', 'action', 'check', 'decision'), true)) {
293 $kind = $action === 'module' ? 'action' : 'input';
294 }
295 $automation = (string)($need['automation'] ?? 'manual');
296 if (!in_array($automation, array('manual', 'observe'), true)) $automation = 'manual';
297 $mode = (string)($need['mode'] ?? 'single');
298 $required = array_key_exists('required', $need) ? (int)((bool)$need['required']) : 1;
299 $rawQuestion = trim((string)($need['question'] ?? ''));
300 $question = $rawQuestion !== '' ? $rawQuestion : $this->workflow_default_question($label, $texts);
301 $validation = (string)($need['validation'] ?? ($mode === 'multiple' ? 'at_least_one' : 'exactly_one'));
302 if (!in_array($validation, array('exactly_one', 'at_least_one', 'not_empty', 'positive_integer', 'confirmed'), true)) {
303 $validation = $mode === 'multiple' ? 'at_least_one' : 'exactly_one';
304 }
305 $rawMissingMessage = trim((string)($need['missing_message'] ?? ''));
306 $missingMessage = $rawMissingMessage !== '' ? $rawMissingMessage : $this->workflow_default_missing_message($label, $texts);
307 $resolver = is_array($need['resolver'] ?? null) ? $need['resolver'] : array();
308 $resolverLabel = trim((string)($resolver['label'] ?? ''));
309 if ($resolverLabel === '') $resolverLabel = $this->workflow_resolver_label($action, $texts);
310 $hint = (string)($need['hint'] ?? '');
311 $event = (string)($need['event'] ?? ($need['result_event'] ?? ''));
312 $dependsOn = (string)($need['depends_on'] ?? '');
313 $dependsValue = (string)($need['depends_value'] ?? '');
314 $optionsText = $this->workflow_options_text($need['options'] ?? array());
315 $sourceJson = '';
316 if (!empty($need['source']) && is_array($need['source'])) {
317 $sourceJson = json_encode($need['source'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
318 }
319 $bindJson = '';
320 if (!empty($need['bind']) && is_array($need['bind'])) {
321 $bindJson = json_encode($need['bind'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
322 }
323 $moduleLinksJson = '';
324 if (!empty($need['module_links']) && is_array($need['module_links'])) {
325 $moduleLinksJson = json_encode($need['module_links'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
326 }
327 $completeLabel = (string)($need['complete_label'] ?? '');
328 $originalJson = $isEmpty ? '' : (json_encode($need, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '');
329
330 $rowClass = $isEmpty ? ' dbx-workflow-step-row-empty' : '';
331 $rowStyle = $isEmpty ? ' style="display:none;"' : '';
332 $kindIcon = array('input' => 'bi-input-cursor-text', 'action' => 'bi-lightning-charge', 'check' => 'bi-shield-check', 'decision' => 'bi-signpost-split')[$kind] ?? 'bi-input-cursor-text';
333 $kindLabels = $this->workflow_kind_labels($texts);
334 $kindLabel = $kindLabels[$kind] ?? $kindLabels['input'];
335 $rows .= '<article class="dbx-workflow-step-row' . $rowClass . '" data-workflow-step-row data-step-kind="' . $this->h($kind) . '" data-question-auto="' . ($rawQuestion === '' ? '1' : '0') . '" data-missing-auto="' . ($rawMissingMessage === '' ? '1' : '0') . '" draggable="true"' . $rowStyle . '>'
336 . '<input type="hidden" name="workflow_step_present[' . $i . ']" value="1">'
337 . '<input type="hidden" name="workflow_step_source[' . $i . ']" value="' . $this->h($sourceJson) . '">'
338 . '<input type="hidden" name="workflow_step_bind[' . $i . ']" value="' . $this->h($bindJson) . '">'
339 . '<input type="hidden" name="workflow_step_module_links[' . $i . ']" value="' . $this->h($moduleLinksJson) . '">'
340 . '<input type="hidden" name="workflow_step_original[' . $i . ']" value="' . $this->h($originalJson) . '">'
341 . '<input type="hidden" name="workflow_step_original_index[' . $i . ']" value="' . ($isEmpty ? '' : $i) . '">'
342 . '<input type="hidden" name="workflow_step_action_contract[' . $i . ']" value="1">'
343 . '<input class="visually-hidden" type="checkbox" name="workflow_step_active[' . $i . ']" value="1"' . (!$isEmpty ? ' checked' : '') . '>'
344 . '<span class="dbx-workflow-node-port is-in" aria-hidden="true"></span>'
345 . '<details class="dbx-workflow-step-details"' . (!$isEmpty ? ' open' : '') . '>'
346 . '<summary class="dbx-workflow-step-summary">'
347 . '<button class="dbx-workflow-drag-handle" type="button" draggable="true" data-workflow-drag-handle title="' . $this->h($texts->get_fd_message('step_move')) . '" aria-label="' . $this->h($texts->get_fd_message('step_move')) . '"><i class="bi bi-grip-vertical"></i></button>'
348 . '<span class="dbx-workflow-step-num" data-workflow-step-number>' . str_pad((string)($i + 1), 2, '0', STR_PAD_LEFT) . '</span>'
349 . '<span class="dbx-workflow-step-kind"><i class="bi ' . $this->h($kindIcon) . '" data-workflow-kind-icon></i><span data-workflow-kind-label>' . $this->h($kindLabel) . '</span></span>'
350 . '<span class="dbx-workflow-step-caption"><strong data-workflow-step-title>' . $this->h($label !== '' ? $label : $texts->get_fd_message('new_task')) . '</strong><small data-workflow-step-result>' . $this->h($event !== '' ? $event : $texts->get_fd_message('no_result')) . '</small></span>'
351 . '<span class="dbx-workflow-auto-badge" data-workflow-auto-badge' . ($automation === 'observe' ? '' : ' hidden') . '><i class="bi bi-stars"></i> ' . $this->h($texts->get_fd_message('automation_badge')) . '</span>'
352 . '<button class="dbx-workflow-remove-step" type="button" data-workflow-remove-step title="' . $this->h($texts->get_fd_message('step_remove')) . '" aria-label="' . $this->h($texts->get_fd_message('step_remove')) . '"><i class="bi bi-trash3"></i></button>'
353 . '</summary>'
354 . '<div class="dbx-workflow-step-main">'
355 . '<div class="row g-2 align-items-end">'
356 . '<div class="col-xl-2 col-md-4"><label class="form-label">' . $this->h($texts->get_fd_message('field_building_block')) . '</label><select class="form-select form-select-sm" name="workflow_step_kind[' . $i . ']">' . $this->workflow_kind_options($kind, $texts) . '</select></div>'
357 . '<div class="col-xl-4 col-md-8"><label class="form-label">' . $this->h($texts->get_fd_message('field_label')) . '</label><input class="form-control form-control-sm" name="workflow_step_label[' . $i . ']" value="' . $this->h($label) . '" placeholder="' . $this->h($texts->get_fd_message('placeholder_label')) . '"></div>'
358 . '<div class="col-xl-3 col-md-6"><label class="form-label">' . $this->h($texts->get_fd_message('field_key')) . '</label><input class="form-control form-control-sm font-monospace" name="workflow_step_key[' . $i . ']" value="' . $this->h($key) . '" placeholder="' . $this->h($texts->get_fd_message('placeholder_key')) . '"></div>'
359 . '<div class="col-xl-3 col-md-6"><label class="form-label">' . $this->h($texts->get_fd_message('field_result')) . '</label><input class="form-control form-control-sm" name="workflow_step_event[' . $i . ']" value="' . $this->h($event) . '" placeholder="' . $this->h($texts->get_fd_message('placeholder_result')) . '"></div>'
360 . '<div class="col-xl-5 col-md-12"><label class="form-label">' . $this->h($texts->get_fd_message('field_allowed_actions')) . '</label><div class="dbx-workflow-action-options">' . $this->workflow_action_choices($i, $actions, $texts) . '</div></div>'
361 . '<div class="col-xl-3 col-md-6"><label class="form-label">' . $this->h($texts->get_fd_message('field_default_action')) . '</label><select class="form-select form-select-sm" name="workflow_step_action[' . $i . ']">' . $this->workflow_action_options($action, $texts) . '</select></div>'
362 . '<div class="col-xl-4 col-md-6"><label class="form-label">' . $this->h($texts->get_fd_message('field_automation')) . '</label><select class="form-select form-select-sm" name="workflow_step_automation[' . $i . ']">' . $this->workflow_automation_options($automation, $texts) . '</select></div>'
363 . '<div class="col-xl-3 col-md-4"><label class="form-label">' . $this->h($texts->get_fd_message('field_values')) . '</label><select class="form-select form-select-sm" name="workflow_step_mode[' . $i . ']">' . $this->workflow_mode_options($mode, $texts) . '</select></div>'
364 . '<div class="col-xl-3 col-md-4"><label class="form-label">' . $this->h($texts->get_fd_message('field_required')) . '</label><select class="form-select form-select-sm" name="workflow_step_required[' . $i . ']"><option value="1"' . ($required ? ' selected' : '') . '>' . $this->h($texts->get_fd_message('required_yes')) . '</option><option value="0"' . (!$required ? ' selected' : '') . '>' . $this->h($texts->get_fd_message('required_no')) . '</option></select></div>'
365 . '<div class="col-xl-6 col-md-8" data-workflow-module-setting' . (!in_array('module', $actions, true) ? ' hidden' : '') . '><label class="form-label">' . $this->h($texts->get_fd_message('field_module_confirmation')) . '</label><input class="form-control form-control-sm" name="workflow_step_complete_label[' . $i . ']" value="' . $this->h($completeLabel) . '" placeholder="' . $this->h($texts->get_fd_message('placeholder_module_confirmation')) . '"></div>'
366 . '<div class="col-12"><span class="dbx-workflow-safety-note"><i class="bi bi-shield-lock"></i> ' . $this->h($texts->get_fd_message('external_actions_confirmed')) . '</span></div>'
367 . '<div class="col-12"><section class="dbx-workflow-step-contract">'
368 . '<header><i class="bi bi-clipboard2-check"></i><div><strong>' . $this->h($texts->get_fd_message('automatic_check')) . '</strong><small>' . $this->h($texts->get_fd_message('automatic_check_hint')) . '</small></div></header>'
369 . '<div class="row g-2">'
370 . '<div class="col-xl-5 col-md-12"><label class="form-label">' . $this->h($texts->get_fd_message('field_question')) . '</label><input class="form-control form-control-sm" name="workflow_step_question[' . $i . ']" value="' . $this->h($question) . '" placeholder="' . $this->h($texts->get_fd_message('placeholder_question')) . '"></div>'
371 . '<div class="col-xl-3 col-md-6"><label class="form-label">' . $this->h($texts->get_fd_message('field_validation')) . '</label><select class="form-select form-select-sm" name="workflow_step_validation[' . $i . ']">' . $this->workflow_validation_options($validation, $texts) . '</select></div>'
372 . '<div class="col-xl-4 col-md-6"><label class="form-label">' . $this->h($texts->get_fd_message('field_resolver')) . '</label><input class="form-control form-control-sm" name="workflow_step_resolver_label[' . $i . ']" value="' . $this->h($resolverLabel) . '" placeholder="' . $this->h($texts->get_fd_message('placeholder_resolver')) . '"></div>'
373 . '<div class="col-12"><label class="form-label">' . $this->h($texts->get_fd_message('field_missing_message')) . '</label><input class="form-control form-control-sm" name="workflow_step_missing_message[' . $i . ']" value="' . $this->h($missingMessage) . '" placeholder="' . $this->h($texts->get_fd_message('placeholder_missing')) . '"></div>'
374 . '</div></section></div>'
375 . '<div class="col-xl-4 col-md-6"><label class="form-label">' . $this->h($texts->get_fd_message('field_depends')) . '</label><select class="form-select form-select-sm" name="workflow_step_depends_on[' . $i . ']">' . $this->workflow_depends_options($needs, $dependsOn, $texts) . '</select></div>'
376 . '<div class="col-xl-4 col-md-6"><label class="form-label">' . $this->h($texts->get_fd_message('field_depends_value')) . '</label><input class="form-control form-control-sm" name="workflow_step_depends_value[' . $i . ']" value="' . $this->h($dependsValue) . '" placeholder="' . $this->h($texts->get_fd_message('placeholder_depends_value')) . '"></div>'
377 . '<div class="col-xl-4 col-md-12"><label class="form-label">' . $this->h($texts->get_fd_message('field_options')) . '</label><textarea class="form-control form-control-sm" name="workflow_step_options[' . $i . ']" rows="3" placeholder="' . $this->h($texts->get_fd_message('placeholder_options')) . '">' . $this->h($optionsText) . '</textarea></div>'
378 . '<div class="col-12"><label class="form-label">' . $this->h($texts->get_fd_message('field_hint')) . '</label><textarea class="form-control form-control-sm" name="workflow_step_hint[' . $i . ']" rows="2" placeholder="' . $this->h($texts->get_fd_message('placeholder_hint')) . '">' . $this->h($hint) . '</textarea></div>'
379 . '</div></div></details>'
380 . '<span class="dbx-workflow-node-port is-out" aria-hidden="true"></span>'
381 . '</article>';
382 }
383
384 $checkRows = '';
385 foreach ($needs as $need) {
386 $label = trim((string)($need['label'] ?? $need['key'] ?? $texts->get_fd_message('default_value_label')));
387 $question = trim((string)($need['question'] ?? '')) ?: $this->workflow_default_question($label, $texts);
388 $required = array_key_exists('required', $need) ? (bool)$need['required'] : true;
389 $validation = (string)($need['validation'] ?? (($need['mode'] ?? 'single') === 'multiple' ? 'at_least_one' : 'exactly_one'));
390 $action = (string)($need['preferred'] ?? (((array)($need['actions'] ?? array('form')))[0] ?? 'form'));
391 $resolver = is_array($need['resolver'] ?? null) ? $need['resolver'] : array();
392 $resolverLabel = trim((string)($resolver['label'] ?? '')) ?: $this->workflow_resolver_label($action, $texts);
393 $validationLabels = $this->workflow_validation_labels($texts);
394 $checkRows .= $this->tpl()->get_tpl('dbxWorkflow_admin|workflow-check-preview-item', array(
395 'question' => $this->h($question),
396 'label' => $this->h($label),
397 'validation' => $this->h($validationLabels[$validation] ?? $validation),
398 'required' => $required ? $texts->get_fd_message('preview_required') : $texts->get_fd_message('preview_optional'),
399 'resolver_label' => $this->h($resolverLabel),
400 ));
401 }
402
403 $finishLabel = (string)($definition['finish']['label'] ?? $texts->format_fd_message(
404 'finish_default',
405 array('result' => ($data['result_label'] ?? 'Workflow'))
406 ));
407 $bindRef = (string)($definition['bind_ref'] ?? '');
408 $technicalPreview = json_encode($definition, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
409 $designerMessageKeys = array(
410 'kind_input', 'kind_action', 'kind_check', 'kind_decision',
411 'validation_exactly_one', 'validation_at_least_one', 'validation_not_empty',
412 'validation_positive_integer', 'validation_confirmed',
413 'resolver_form', 'resolver_select', 'resolver_create', 'resolver_module',
414 'default_question', 'default_missing_message', 'new_task', 'no_result',
415 'preview_required', 'preview_optional',
416 'js_new_input', 'js_new_action', 'js_new_check', 'js_new_decision',
417 'js_input_complete', 'js_action_complete', 'js_check_complete', 'js_decision_complete',
418 'js_requirement_singular', 'js_requirement_plural', 'js_direct_after_previous',
419 'js_error_label_missing', 'js_error_key_missing', 'js_error_key_duplicate',
420 'js_error_question_missing', 'js_error_decision_options', 'js_warning_automation',
421 'js_warning_select_source', 'js_warning_module_link',
422 'js_error_dependency_missing', 'js_error_dependency_order',
423 'validation_minimum_step', 'js_status_invalid', 'js_status_warning',
424 'js_status_valid', 'js_status_valid_detail', 'js_no_free_step',
425 'js_no_free_step_detail', 'js_default_event', 'js_yes', 'js_no',
426 'js_payment_label', 'js_payment_event', 'js_payment_question',
427 'js_payment_missing', 'js_payment_resolver', 'js_payment_hint',
428 'js_payment_options'
429 );
430 $designerMessages = array();
431 foreach ($designerMessageKeys as $messageKey) {
432 $designerMessages[$messageKey] = $texts->get_fd_message($messageKey);
433 }
434
435 return $this->tpl()->get_tpl('dbxWorkflow_admin|workflow-builder', array(
436 'need_count' => count($needs),
437 'check_rows' => $checkRows,
438 'step_rows' => $rows,
439 'finish_label' => $this->h($finishLabel),
440 'bind_ref' => $this->h($bindRef),
441 'technical_preview' => $this->h($technicalPreview),
442 'designer_messages' => $this->h(json_encode(
443 $designerMessages,
444 JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
445 )),
446 ));
447 }
448
449 private function workflow_json_array($value): array {
450 if (is_array($value)) return $value;
451 $value = trim((string)$value);
452 if ($value === '' || strlen($value) > 200000) return array();
453 $decoded = json_decode($value, true);
454 return is_array($decoded) ? $decoded : array();
455 }
456
457 private function workflow_actions_from_post($index, array $original, string $preferred): array {
458 $allowed = array('form', 'select', 'create', 'module');
459 $hasActionContract = isset($_POST['workflow_step_action_contract'])
460 && array_key_exists($index, (array)$_POST['workflow_step_action_contract']);
461 $posted = $_POST['workflow_step_actions'][$index] ?? array();
462 if (!is_array($posted)) $posted = array($posted);
463 $selected = array_values(array_unique(array_intersect(array_map('strval', $posted), $allowed)));
464
465 // Offene Formulare aus einer älteren Version senden noch keine
466 // Mehrfachauswahl. In diesem Fall bleibt die bestehende Aktionsliste
467 // vollständig erhalten.
468 if (!$hasActionContract) {
469 $selected = array_values(array_unique(array_intersect(array_map('strval', (array)($original['actions'] ?? array())), $allowed)));
470 }
471 if (!$selected) $selected = array(in_array($preferred, $allowed, true) ? $preferred : 'form');
472
473 // Bestehende Reihenfolge erhalten; neu aktivierte Aktionen folgen in der
474 // stabilen UI-Reihenfolge. Damit verändert reines Öffnen und Speichern
475 // auch die Priorisierung alter Definitionen nicht.
476 $selectedMap = array_fill_keys($selected, true);
477 $actions = array();
478 foreach ((array)($original['actions'] ?? array()) as $action) {
479 $action = (string)$action;
480 if (isset($selectedMap[$action]) && !in_array($action, $actions, true)) $actions[] = $action;
481 }
482 foreach ($allowed as $action) {
483 if (isset($selectedMap[$action]) && !in_array($action, $actions, true)) $actions[] = $action;
484 }
485 return $actions;
486 }
487
488 private function workflow_definition_from_post($oForm, array $baseDefinition = array()) {
489 $steps = array();
490 $checkKeyRenames = array();
491 $baseNeeds = array_values((array)($baseDefinition['needs'] ?? array()));
492 $present = $_POST['workflow_step_present'] ?? array();
493 foreach ((array)$present as $idx => $unused) {
494 $active = isset($_POST['workflow_step_active'][$idx]);
495 $label = trim((string)($_POST['workflow_step_label'][$idx] ?? ''));
496 $key = $this->normalize_key($_POST['workflow_step_key'][$idx] ?? $label);
497 if (!$active && $label === '' && $key === '') continue;
498 if (!$active) continue;
499 if ($key === '') $key = 'schritt_' . ((int)$idx + 1);
500 if ($label === '') $label = $key;
501
502 $action = (string)($_POST['workflow_step_action'][$idx] ?? 'form');
503 if (!in_array($action, array('form', 'select', 'create', 'module'), true)) $action = 'form';
504 $kind = (string)($_POST['workflow_step_kind'][$idx] ?? 'input');
505 if (!in_array($kind, array('input', 'action', 'check', 'decision'), true)) $kind = 'input';
506 $automation = (string)($_POST['workflow_step_automation'][$idx] ?? 'manual');
507 if (!in_array($automation, array('manual', 'observe'), true)) $automation = 'manual';
508 $mode = (string)($_POST['workflow_step_mode'][$idx] ?? 'single') === 'multiple' ? 'multiple' : 'single';
509
510 $need = $this->workflow_json_array($_POST['workflow_step_original'][$idx] ?? '');
511 $hasActionContract = isset($_POST['workflow_step_action_contract'])
512 && array_key_exists($idx, (array)$_POST['workflow_step_action_contract']);
513 $originalIndexRaw = trim((string)($_POST['workflow_step_original_index'][$idx] ?? ''));
514 $originalIndex = ctype_digit($originalIndexRaw) ? (int)$originalIndexRaw : null;
515 if (!$need && $originalIndex !== null && isset($baseNeeds[$originalIndex]) && is_array($baseNeeds[$originalIndex])) {
516 $need = $baseNeeds[$originalIndex];
517 } elseif (!$need && !$hasActionContract && isset($baseNeeds[(int)$idx]) && is_array($baseNeeds[(int)$idx])) {
518 $need = $baseNeeds[(int)$idx];
519 }
520 $originalKey = $this->normalize_key($need['key'] ?? '');
521 if ($originalKey !== '' && $originalKey !== $key) $checkKeyRenames[$originalKey] = $key;
522 $actions = $this->workflow_actions_from_post($idx, $need, $action);
523 if (!in_array($action, $actions, true)) $action = (string)$actions[0];
524 $resolver = is_array($need['resolver'] ?? null) ? $need['resolver'] : array();
525 $resolver['type'] = $action;
526 $resolver['label'] = trim((string)($_POST['workflow_step_resolver_label'][$idx] ?? '')) ?: $this->workflow_resolver_label($action, $oForm);
527
528 // Der vorhandene Need ist die Basis. Nur Felder, die der Designer
529 // tatsächlich verwaltet, werden überschrieben. Erweiterungen von
530 // Fachmodulen und zukünftige Schemafelder bleiben dadurch erhalten.
531 $need['key'] = $key;
532 $need['label'] = $label;
533 $need['kind'] = $kind;
534 $need['automation'] = $automation;
535 $need['mode'] = $mode;
536 $need['required'] = ((string)($_POST['workflow_step_required'][$idx] ?? '1') === '1');
537 $need['actions'] = $actions;
538 $need['preferred'] = $action;
539 $need['question'] = trim((string)($_POST['workflow_step_question'][$idx] ?? '')) ?: $this->workflow_default_question($label, $oForm);
540 $need['validation'] = (string)($_POST['workflow_step_validation'][$idx] ?? ($mode === 'multiple' ? 'at_least_one' : 'exactly_one'));
541 $need['missing_message'] = trim((string)($_POST['workflow_step_missing_message'][$idx] ?? '')) ?: $this->workflow_default_missing_message($label, $oForm);
542 $need['resolver'] = $resolver;
543 $need['hint'] = trim((string)($_POST['workflow_step_hint'][$idx] ?? ''));
544
545 $event = trim((string)($_POST['workflow_step_event'][$idx] ?? ''));
546 if ($event !== '') $need['event'] = $event;
547 else unset($need['event'], $need['result_event']);
548
549 $dependsOn = $this->normalize_key($_POST['workflow_step_depends_on'][$idx] ?? '');
550 if ($dependsOn !== '') {
551 $need['depends_on'] = $dependsOn;
552 $dependsValue = trim((string)($_POST['workflow_step_depends_value'][$idx] ?? ''));
553 if ($dependsValue !== '') $need['depends_value'] = $dependsValue;
554 else unset($need['depends_value']);
555 }
556 else unset($need['depends_on'], $need['depends_value']);
557
558 $options = $this->parse_workflow_options($_POST['workflow_step_options'][$idx] ?? '');
559 if ($options) $need['options'] = $options;
560 else unset($need['options']);
561
562 if (isset($_POST['workflow_step_complete_label']) && array_key_exists($idx, (array)$_POST['workflow_step_complete_label'])) {
563 $completeLabel = trim((string)$_POST['workflow_step_complete_label'][$idx]);
564 if ($completeLabel !== '') $need['complete_label'] = $completeLabel;
565 else unset($need['complete_label']);
566 }
567
568 $sourceJson = trim((string)($_POST['workflow_step_source'][$idx] ?? ''));
569 $source = $sourceJson !== '' ? json_decode($sourceJson, true) : null;
570 if (is_array($source)) $need['source'] = $source;
571 elseif ($sourceJson === '') unset($need['source']);
572
573 $bindJson = trim((string)($_POST['workflow_step_bind'][$idx] ?? ''));
574 $bind = $bindJson !== '' ? json_decode($bindJson, true) : null;
575 if (is_array($bind)) $need['bind'] = $bind;
576 elseif ($bindJson === '') unset($need['bind']);
577
578 $moduleLinksJson = trim((string)($_POST['workflow_step_module_links'][$idx] ?? ''));
579 $moduleLinks = $moduleLinksJson !== '' ? json_decode($moduleLinksJson, true) : null;
580 if (is_array($moduleLinks)) $need['module_links'] = $moduleLinks;
581 elseif ($moduleLinksJson === '') unset($need['module_links']);
582
583 $steps[] = $need;
584 }
585
587 $definition['workflow_key'] = $oForm->get_post('workflow_key', '', 'parameter|min=2|max=80');
588 $definition['title'] = $oForm->get_post('title', '', '*|min=2|max=160');
589 $definition['result'] = $oForm->get_post('result_label', '', '*|min=2|max=160');
590 $definition['description'] = $oForm->get_post_data('description', '', '*');
591 $definition['needs'] = $steps;
592 if ($checkKeyRenames && !empty($definition['checks']) && is_array($definition['checks'])) {
593 foreach ($definition['checks'] as &$check) {
594 if (!is_array($check)) continue;
595 $checkKey = $this->normalize_key($check['key'] ?? '');
596 if (isset($checkKeyRenames[$checkKey])) $check['key'] = $checkKeyRenames[$checkKey];
597 }
598 unset($check);
599 }
600 $finish = is_array($definition['finish'] ?? null) ? $definition['finish'] : array();
601 $finish['label'] = trim((string)($_POST['workflow_finish_label'] ?? '')) ?: $oForm->format_fd_message(
602 'finish_default',
603 array('result' => $definition['result'] ?: 'Workflow')
604 );
605 $definition['finish'] = $finish;
606
607 $bindRef = trim((string)($_POST['workflow_bind_ref'] ?? ''));
608 if ($bindRef !== '') $definition['bind_ref'] = $bindRef;
609 else unset($definition['bind_ref']);
610
611 return $definition;
612 }
613
614 private function validate_workflow_definition(array $definition, $texts): array {
615 $errors = array();
616 $needs = array_values((array)($definition['needs'] ?? array()));
617 $positions = array();
618
619 foreach ($needs as $index => $need) {
620 $key = $this->normalize_key($need['key'] ?? '');
621 $label = trim((string)($need['label'] ?? $key));
622 if ($key === '') {
623 $errors[] = $texts->format_fd_message(
624 'validation_step_key_missing',
625 array('step' => $index + 1)
626 );
627 continue;
628 }
629 if (array_key_exists($key, $positions)) {
630 $errors[] = $texts->format_fd_message(
631 'validation_step_key_duplicate',
632 array('key' => $key)
633 );
634 } else {
635 $positions[$key] = $index;
636 }
637 if ((string)($need['kind'] ?? '') === 'decision' && count((array)($need['options'] ?? array())) < 2) {
638 $errors[] = $texts->format_fd_message(
639 'validation_decision_options',
640 array('label' => $label)
641 );
642 }
643 }
644
645 foreach ($needs as $index => $need) {
646 $key = $this->normalize_key($need['key'] ?? '');
647 $dependsOn = $this->normalize_key($need['depends_on'] ?? '');
648 if ($dependsOn === '') continue;
649 if (!array_key_exists($dependsOn, $positions)) {
650 $errors[] = $texts->format_fd_message(
651 'validation_dependency_missing',
652 array('dependency' => $dependsOn, 'key' => $key)
653 );
654 } elseif ($positions[$dependsOn] >= $index) {
655 $errors[] = $texts->format_fd_message(
656 'validation_dependency_order',
657 array('dependency' => $dependsOn, 'key' => $key)
658 );
659 }
660 }
661
662 if (!$needs) {
663 $errors[] = $texts->get_fd_message('validation_minimum_step');
664 }
665
666 return array_values(array_unique($errors));
667 }
668
669 private function edit() {
670 $rid = dbx()->get_modul_var('rid', 'new', 'parameter');
671 $isNew = ($rid === 'new' || (int)$rid <= 0);
672
673 $oForm = dbx()->get_system_obj('dbxForm');
674 $oForm->init('workflow-definition', 'workflow-definition-form');
675 $oForm->_dd = $this->ddDefinition;
676 $oForm->_fd = 'dbxWorkflow|workflow-definition';
677 $oForm->load_fd_messages();
678
679 $data = $this->load_row($rid);
680 if (!$data) {
681 return $this->frame(
682 $this->alert('warning', $oForm->get_fd_message('definition_not_found')),
683 $oForm
684 );
685 }
686
687 $oForm->add_rep('bar_title', $oForm->get_fd_message($isNew ? 'form_new_title' : 'form_edit_title'));
688 $oForm->add_rep('bar_subtitle', $oForm->get_fd_message('form_subtitle'));
689 $oForm->add_obj('bar_actions', 'obj-value',
690 '<a class="btn btn-outline-secondary btn-sm" href="?dbx_modul=dbxWorkflow_admin"><i class="bi bi-list-ul"></i> ' . $this->h($oForm->get_fd_message('list_label')) . '</a>'
691 . '<button class="btn btn-primary btn-sm" type="submit"><i class="bi bi-save"></i> ' . $this->h($oForm->get_fd_message('save_label')) . '</button>'
692 );
693 $oForm->_data = $data;
694 $oForm->_action = '?dbx_modul=dbxWorkflow_admin&dbx_run1=edit&rid=' . ($isNew ? 'new' : (int)$rid);
695 $oForm->set_activ_id($isNew ? 0 : (int)$rid);
696 $oForm->_msg_info = $oForm->get_fd_message($isNew ? 'form_new_info' : 'form_edit_info');
697
698 $oForm->add_fld('workflow_key');
699 $oForm->add_fld('title');
700 $oForm->add_fld('result_label');
701 $oForm->add_fld('description');
702 $oForm->add_fld('active');
703 $oForm->add_obj('workflow_builder', 'obj-value', $this->workflow_builder_html($data, $oForm));
704
705 if ($oForm->submit()) {
706 if (!$oForm->errors()) {
707 $engine = dbx()->get_include_obj('dbxWorkflowEngine', 'dbxWorkflow');
708 $baseDefinition = $this->workflow_definition_from_data($data);
709 $definition = $engine->normalize_definition($this->workflow_definition_from_post($oForm, $baseDefinition), $oForm->get_post('workflow_key', '', 'parameter|max=80'));
710 $definitionErrors = $this->validate_workflow_definition($definition, $oForm);
711 if ($definitionErrors) {
712 $errorText = implode(' ', $definitionErrors);
713 $oForm->add_fld_error('title', $errorText);
714 $oForm->_msg_error = $errorText;
715 $oForm->add_obj('workflow_builder', 'obj-value', $this->workflow_builder_html(array_merge($data, array(
716 'workflow_key' => $oForm->get_post('workflow_key', '', 'parameter|max=80'),
717 'title' => $oForm->get_post('title', '', '*'),
718 'result_label' => $oForm->get_post('result_label', '', '*'),
719 'description' => $oForm->get_post_data('description', '', '*'),
720 'definition_json' => json_encode($definition, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
721 )), $oForm));
722 return $oForm->run();
723 }
724 $definitionText = json_encode($definition, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
725
726 $values = array(
727 'workflow_key' => $oForm->get_post('workflow_key', '', 'parameter|min=2|max=80'),
728 'title' => $oForm->get_post('title', '', '*|min=2|max=160'),
729 'result_label' => $oForm->get_post('result_label', '', '*|min=2|max=160'),
730 'description' => $oForm->get_post_data('description', '', '*'),
731 'definition_json' => $definitionText,
732 'active' => $oForm->get_post('active', 0, 'int')
733 );
734
735 $db = $this->db();
736 $key = $db->escape($values['workflow_key'], $db->get_dd_server($this->ddDefinition));
737 $where = "workflow_key='" . $key . "'";
738 if (!$isNew) $where .= ' AND id <> ' . (int)$rid;
739
740 if ($db->count($this->ddDefinition, $where) > 0) {
741 $oForm->add_fld_error('workflow_key', $oForm->get_fd_message('duplicate_workflow_key'));
742 $oForm->_msg_error = $oForm->get_fd_message('duplicate_workflow_key');
743 } else {
744 $ok = $db->save($this->ddDefinition, $values, $isNew ? 0 : (int)$rid, 0, 1, 1, 1);
745 $oForm->_msg_success = $oForm->get_fd_message($ok ? 'save_success' : 'save_error');
746 if ($ok) {
747 $oForm->_data = array_merge($oForm->_data, $values);
748 $oForm->add_obj('workflow_builder', 'obj-value', $this->workflow_builder_html($oForm->_data, $oForm));
749 }
750 }
751 } else {
752 $oForm->_msg_error = $oForm->get_fd_message('validation_error');
753 }
754 }
755
756 return $oForm->run();
757 }
758
759 private function decorate_rows($rows) {
760 $out = array();
761 foreach ((array)$rows as $row) {
762 $row['action'] = $this->tpl()->get_tpl('dbxWorkflow_admin|workflow-admin-row-action', array(
763 'id' => (int)($row['id'] ?? 0),
764 'workflow_key' => rawurlencode((string)($row['workflow_key'] ?? '')),
765 'dbx_token' => rawurlencode(dbx()->action_token('dbxWorkflow.start'))
766 ));
767 $out[] = $row;
768 }
769 return $out;
770 }
771
772 private function report_search_where(string $rwhere): array|string {
773 $rwhere = trim($rwhere);
774 if ($rwhere === '') {
775 return '';
776 }
777
778 return array(
779 'search' => array(
780 'value' => $rwhere,
781 'like' => array('workflow_key', 'title', 'result_label', 'description', 'status', 'message', 'current_need'),
782 'mode' => 'contains',
783 ),
784 );
785 }
786
787 private function list_definitions() {
788 $db = $this->db();
789
790 $oReport = dbx()->get_system_obj('dbxReport');
791 $oReport->init('workflow-definitions');
792 $oReport->_dd = $this->ddDefinition;
793 $oReport->_fd = 'dbxWorkflow_admin|rpt-workflow-definitions-selection';
794 $oReport->load_fd_messages();
795 $oReport->_action = '?dbx_modul=dbxWorkflow_admin';
796 $oReport->_pages = true;
797 $oReport->_create_row_select = false;
798 $oReport->_create_row_edit = false;
799 $oReport->_create_row_delete = false;
800 $oReport->_but_pagination = 5;
801 $oReport->set_form_help_enabled(false);
802 $oReport->_msg_info = $oReport->get_fd_message('report_info');
803 $oReport->add_module_bar(
804 $oReport->get_fd_message('bar_title'),
805 'bi-diagram-3',
806 $oReport->get_fd_message('bar_subtitle'),
807 true
808 );
809 $oReport->add_rep(
810 'bar_subtitle',
811 $oReport->get_fd_message('bar_subtitle')
812 );
813 $oReport->add_rep(
814 'bar_actions',
815 '<a class="btn btn-outline-secondary btn-sm" href="?dbx_modul=dbxWorkflow_admin&dbx_run1=binds"><i class="bi bi-plug"></i> '
816 . $this->h($oReport->get_fd_message('module_bindings'))
817 . '</a>'
818 . '<a class="btn btn-primary btn-sm" href="?dbx_modul=dbxWorkflow_admin&dbx_run1=edit_bind&rid=new"><i class="bi bi-plus-circle"></i> '
819 . $this->h($oReport->get_fd_message('new_binding'))
820 . '</a>'
821 . '<a class="btn btn-primary btn-sm" href="?dbx_modul=dbxWorkflow_admin&dbx_run1=edit&rid=new"><i class="bi bi-plus-circle"></i> '
822 . $this->h($oReport->get_fd_message('new_workflow'))
823 . '</a>'
824 );
825 $oReport->create_selection_fields('dbxWorkflow_admin|rpt-workflow-definitions-selection');
826
827 if ($oReport->submit()) {
828 if (!$oReport->errors()) {
829 $oReport->_msg_info = $oReport->get_fd_message(
830 'filter_applied'
831 );
832 } else {
833 $oReport->_msg_error = $oReport->get_fd_message(
834 'validation_error'
835 );
836 }
837 }
838
839 $rwhere = $oReport->get_fld_val('dbx_rwhere', '', 'sqlsearch|max=64');
840 $rrows = $oReport->get_fld_val('dbx_rrows', 30, 'int');
841 $rpos = $oReport->get_fld_val('dbx_rpos', 0, 'int');
842 $rsort = $oReport->get_fld_val('dbx_rsort', 'title', 'parameter');
843 $rdesc = $oReport->get_fld_val('dbx_rdesc', 'ASC', 'parameter');
844
845 $where = $this->report_search_where($rwhere);
846 $cols = array('id', 'workflow_key', 'title', 'result_label', 'active', 'update_date');
847
848 $oReport->_rflds = array(
849 'id' => 'ID',
850 'workflow_key' => $oReport->get_fd_message('column_key'),
851 'title' => $oReport->get_fd_message('column_title'),
852 'result_label' => $oReport->get_fd_message('column_goal'),
853 'active' => $oReport->get_fd_message('column_active'),
854 'update_date' => $oReport->get_fd_message('column_updated'),
855 'action' => $oReport->get_fd_message('column_action')
856 );
857 $oReport->_rpt_format = array(
858 'update_date' => 'php-datetime-usr',
859 'action' => 'html',
860 );
861 $oReport->_rrows = $rrows;
862 $oReport->_rpos = $rpos;
863 $oReport->_count_all = $db->count($this->ddDefinition, '');
864 $oReport->_rcount = $db->count($this->ddDefinition, $where);
865 $oReport->_rdata = $this->decorate_rows($db->select($this->ddDefinition, $where, $cols, $rsort, $rdesc, '', $rrows, $rpos));
866
867 return $this->frame($oReport->run(), $oReport);
868 }
869
870 private function list_instances() {
871 $db = $this->db();
872 $oReport = dbx()->get_system_obj('dbxReport');
873 $oReport->init('workflow-instances');
874 $oReport->_dd = $this->ddInstance;
875 $oReport->_fd = 'dbxWorkflow_admin|rpt-workflow-instances-selection';
876 $oReport->load_fd_messages();
877 $oReport->_action = '?dbx_modul=dbxWorkflow_admin&dbx_run1=instances';
878 $oReport->_pages = true;
879 $oReport->_create_row_select = false;
880 $oReport->_create_row_edit = false;
881 $oReport->_create_row_delete = false;
882 $oReport->_but_pagination = 5;
883 $oReport->set_form_help_enabled(false);
884 $oReport->_msg_info = $oReport->get_fd_message('report_info');
885 $oReport->create_selection_fields('dbxWorkflow_admin|rpt-workflow-instances-selection');
886
887 if ($oReport->submit()) {
888 if (!$oReport->errors()) {
889 $oReport->_msg_info = $oReport->get_fd_message(
890 'filter_applied'
891 );
892 } else {
893 $oReport->_msg_error = $oReport->get_fd_message(
894 'validation_error'
895 );
896 }
897 }
898
899 $rwhere = $oReport->get_fld_val('dbx_rwhere', '', 'sqlsearch|max=64');
900 $rrows = $oReport->get_fld_val('dbx_rrows', 50, 'int');
901 $rpos = $oReport->get_fld_val('dbx_rpos', 0, 'int');
902 $rsort = $oReport->get_fld_val('dbx_rsort', 'create_date', 'parameter');
903 $rdesc = $oReport->get_fld_val('dbx_rdesc', 'DESC', 'parameter');
904
905 $where = $this->report_search_where($rwhere);
906 $cols = array('id', 'create_date', 'workflow_key', 'result_label', 'status', 'current_need', 'percent', 'message');
907
908 $oReport->_rflds = array(
909 'id' => 'ID',
910 'create_date' => $oReport->get_fd_message('column_start'),
911 'workflow_key' => $oReport->get_fd_message('column_workflow'),
912 'result_label' => $oReport->get_fd_message('column_goal'),
913 'status' => $oReport->get_fd_message('column_status'),
914 'current_need' => $oReport->get_fd_message('column_task'),
915 'percent' => '%',
916 'message' => $oReport->get_fd_message('column_message'),
917 'instance_action' => $oReport->get_fd_message('column_action')
918 );
919 $oReport->_rpt_format = array(
920 'create_date' => 'php-datetime-usr',
921 'status' => 'html',
922 'percent' => 'html',
923 'message' => 'html',
924 'instance_action' => 'html'
925 );
926 $oReport->_rrows = $rrows;
927 $oReport->_rpos = $rpos;
928 $oReport->_count_all = $db->count($this->ddInstance, '');
929 $oReport->_rcount = $db->count($this->ddInstance, $where);
930 $oReport->_rdata = $this->decorate_instance_rows(
931 $db->select($this->ddInstance, $where, $cols, $rsort, $rdesc, '', $rrows, $rpos),
932 $oReport
933 );
934
935 return $this->frame($oReport->run(), $oReport);
936 }
937
938 private function decorate_instance_rows($rows, $texts) {
939 $out = array();
940 foreach ((array)$rows as $row) {
941 $row['instance_action'] = $this->instance_action_button($row, $texts);
942 $row['status'] = $this->instance_status_badge((string)($row['status'] ?? ''), $texts);
943 $row['percent'] = $this->instance_percent_bar((int)($row['percent'] ?? 0));
944 $row['message'] = $this->instance_message((string)($row['message'] ?? ''));
945 $out[] = $row;
946 }
947 return $out;
948 }
949
950 private function instance_status_badge(string $status, $texts): string {
951 $status = strtolower(trim($status));
952 $labels = array(
953 'running' => array($texts->get_fd_message('status_running'), 'primary'),
954 'finishing' => array($texts->get_fd_message('status_finishing'), 'warning'),
955 'paused' => array($texts->get_fd_message('status_paused'), 'warning'),
956 'finished' => array($texts->get_fd_message('status_finished'), 'success'),
957 'canceled' => array($texts->get_fd_message('status_canceled'), 'secondary'),
958 'error' => array($texts->get_fd_message('status_error'), 'danger'),
959 );
960 $data = $labels[$status] ?? array(($status !== '' ? $status : $texts->get_fd_message('status_unknown')), 'secondary');
961 return '<span class="badge text-bg-' . $this->h($data[1]) . '">' . $this->h($data[0]) . '</span>';
962 }
963
964 private function instance_percent_bar(int $percent): string {
965 $percent = max(0, min(100, $percent));
966 return '<div class="dbx-workflow-instance-progress" title="' . $percent . '%">'
967 . '<div class="dbx-workflow-instance-progress-bar" style="width:' . $percent . '%"></div>'
968 . '<span>' . $percent . '%</span>'
969 . '</div>';
970 }
971
972 private function instance_message(string $message): string {
973 $message = trim($message);
974 if ($message === '') {
975 return '<span class="text-muted">-</span>';
976 }
977 return '<span class="dbx-workflow-instance-message">' . $this->h($message) . '</span>';
978 }
979
980 private function instance_action_button(array $row, $texts): string {
981 $id = (int)($row['id'] ?? 0);
982 if ($id <= 0) {
983 return '';
984 }
985
986 $status = strtolower(trim((string)($row['status'] ?? '')));
987 $isFinished = in_array($status, array('finished', 'canceled'), true);
988 $url = '?dbx_modul=dbxWorkflow&dbx_run1=run&iid=' . $id;
989 if ($status === 'paused') {
990 $url .= '&dbx_token=' . rawurlencode(dbx()->action_token('dbxWorkflow.instance.' . $id)) . '&proc_cmd=resume';
991 }
992
993 $label = $isFinished
994 ? $texts->get_fd_message('action_view')
995 : $texts->get_fd_message('action_continue');
996 $icon = $isFinished ? 'bi-search' : 'bi-play-fill';
997 $class = $isFinished ? 'btn btn-outline-primary btn-sm' : 'btn btn-primary btn-sm';
998 $title = $texts->format_fd_message(
999 'action_title',
1000 array('action' => $label, 'id' => $id)
1001 );
1002
1003 return '<a class="' . $class . ' openWin dbx-win dbx-workflow-instance-action" href="' . $this->h($url) . '"'
1004 . ' data-url="' . $this->h($url) . '" data-title="' . $this->h($title) . '" data-width="82%" data-height="88%"'
1005 . ' title="' . $this->h($title) . '"><i class="bi ' . $icon . '"></i> ' . $this->h($label) . '</a>';
1006 }
1007
1008 private function bindRegistry() {
1009 return dbx()->get_include_obj('dbxWorkflowBindRegistry', 'dbxWorkflow');
1010 }
1011
1012 private function load_bind_row($rid) {
1013 if ($rid === 'new' || (int)$rid <= 0) {
1014 return array(
1015 'modul' => 'dbxContact',
1016 'bind_key' => 'contact_reply',
1017 'title' => 'Neues Modul-Binding',
1018 'description' => 'Einheitliches Binding: dbxWorkflow nutzt DD/FD/TPL/Config des Moduls.',
1019 'bind_json' => '',
1020 'active' => 1,
1021 );
1022 }
1023
1024 $rows = $this->db()->select($this->ddBind, array('id' => (int)$rid), '*', 'id', 'DESC', '', 1, 0, 0);
1025 return (is_array($rows) && isset($rows[0])) ? $rows[0] : array();
1026 }
1027
1028 private function edit_bind() {
1029 $rid = dbx()->get_modul_var('rid', 'new', 'parameter');
1030 $isNew = ($rid === 'new' || (int)$rid <= 0);
1031 $data = $this->load_bind_row($rid);
1032
1033 $oForm = dbx()->get_system_obj('dbxForm');
1034 $oForm->init('workflow-module-bind', 'workflow-module-bind-form');
1035 $oForm->_dd = $this->ddBind;
1036 $oForm->_fd = 'dbxWorkflow|workflow-module-bind';
1037 $oForm->load_fd_messages();
1038 if ($isNew) {
1039 $data['title'] = $oForm->get_fd_message('default_binding_title');
1040 $data['description'] = $oForm->get_fd_message('default_binding_description');
1041 }
1042
1043 if (!$data) {
1044 return $this->frame(
1045 $this->alert('warning', $oForm->get_fd_message('binding_not_found')),
1046 $oForm
1047 );
1048 }
1049
1050 $generatorData = array(
1051 'gen_modul' => trim((string)dbx()->get_modul_var('gen_modul', (string)($data['modul'] ?? ''), 'parameter')),
1052 'gen_dd' => trim((string)dbx()->get_modul_var('gen_dd', '', 'parameter')),
1053 );
1054 $generatorDds = array();
1055 foreach ($this->bindRegistry()->listModuleDds($generatorData['gen_modul']) as $ddRef) {
1056 $generatorDds[(string)$ddRef] = (string)$ddRef;
1057 }
1058
1059 // Zwei Formulare werden auf derselben Seite gerendert. Ein eigenes
1060 // Objekt verhindert, dass init() des Hauptformulars den Zustand des
1061 // Generatorformulars im global gecachten dbxForm-Objekt ueberschreibt.
1062 dbx()->get_system_obj('dbxForm', 'use');
1063 $generatorForm = new \dbxForm();
1064 $generatorForm->init('workflow-bind-generator', 'workflow-bind-generator');
1065 $generatorForm->_fd = 'dbxWorkflow|workflow-module-bind';
1066 $generatorForm->load_fd_messages();
1067 $generatorForm->set_form_help_enabled(false);
1068 $generatorForm->_action = '?dbx_modul=dbxWorkflow_admin&dbx_run1=edit_bind&rid=' . ($isNew ? 'new' : (int)$rid);
1069 $generatorForm->_data = array_merge($generatorForm->_data, $generatorData);
1070 $generatorForm->_msg_info = '';
1071 $generatorForm->add_fld(
1072 'gen_modul',
1073 'text-label',
1074 label: $generatorForm->get_fd_message('generator_module_label'),
1075 rules: 'parameter|min=2|max=80',
1076 data: array('placeholder' => 'dbxContact')
1077 );
1078 $generatorForm->add_fld(
1079 'gen_dd',
1080 'select-single-label',
1081 label: $generatorForm->get_fd_message('generator_dd_label'),
1082 rules: 'parameter|min=2|max=160',
1083 options: array('' => $generatorForm->get_fd_message('generator_dd_select')) + $generatorDds
1084 );
1085
1086 if ($generatorForm->submit()) {
1087 if (!$generatorForm->errors()) {
1088 $generateModul = trim((string)$generatorForm->get_post('gen_modul', '', 'parameter|min=2|max=80'));
1089 $generateDd = trim((string)$generatorForm->get_post('gen_dd', '', 'parameter|min=2|max=160'));
1090 $generated = $this->bindRegistry()->generateBindSkeleton($generateModul, $generateDd);
1091 if ($generated) {
1092 $data = array_merge($data, $generated);
1093 $generatorForm->_msg_success = $generatorForm->get_fd_message('generator_success');
1094 } else {
1095 $generatorForm->_msg_error = $generatorForm->get_fd_message('generator_error');
1096 }
1097 } else {
1098 $generatorForm->_msg_error = $generatorForm->get_fd_message('generator_validation_error');
1099 }
1100 }
1101
1102 $oForm->add_rep('bar_title', $oForm->get_fd_message($isNew ? 'form_new_title' : 'form_edit_title'));
1103 $oForm->add_rep('bar_subtitle', $oForm->get_fd_message('form_subtitle'));
1104 $oForm->add_obj('bar_actions', 'obj-value',
1105 '<a class="btn btn-outline-secondary btn-sm" href="?dbx_modul=dbxWorkflow_admin&dbx_run1=binds"><i class="bi bi-list-ul"></i> ' . $this->h($oForm->get_fd_message('list_label')) . '</a>'
1106 . '<button class="btn btn-primary btn-sm" type="submit"><i class="bi bi-save"></i> ' . $this->h($oForm->get_fd_message('save_label')) . '</button>'
1107 );
1108 $oForm->_data = array_merge($oForm->_data, $data);
1109 $oForm->_action = '?dbx_modul=dbxWorkflow_admin&dbx_run1=edit_bind&rid=' . ($isNew ? 'new' : (int)$rid);
1110 $oForm->set_activ_id($isNew ? 0 : (int)$rid);
1111 $oForm->_msg_info = $oForm->get_fd_message($isNew ? 'form_new_info' : 'form_edit_info');
1112
1113 $oForm->add_fld('modul');
1114 $oForm->add_fld('bind_key');
1115 $oForm->add_fld('title');
1116 $oForm->add_fld('description');
1117 $oForm->add_fld('bind_json');
1118 $oForm->add_fld('active');
1119
1120 if ($oForm->submit()) {
1121 if (!$oForm->errors()) {
1122 $bindJson = trim((string)$oForm->get_post_data('bind_json', '', '*'));
1123 $decoded = json_decode($bindJson, true);
1124 if (!is_array($decoded)) {
1125 $oForm->add_fld_error('bind_json', $oForm->get_fd_message('json_invalid'));
1126 $oForm->_msg_error = $oForm->get_fd_message('json_invalid');
1127 } else {
1128 $values = array(
1129 'modul' => $oForm->get_post('modul', '', 'parameter|min=2|max=80'),
1130 'bind_key' => $oForm->get_post('bind_key', '', 'parameter|min=2|max=80'),
1131 'title' => $oForm->get_post('title', '', '*|min=2|max=160'),
1132 'description' => $oForm->get_post_data('description', '', '*'),
1133 'bind_json' => json_encode($decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
1134 'active' => $oForm->get_post('active', 0, 'int'),
1135 );
1136
1137 $db = $this->db();
1138 $where = "modul='" . $db->escape($values['modul'], $db->get_dd_server($this->ddBind))
1139 . "' AND bind_key='" . $db->escape($values['bind_key'], $db->get_dd_server($this->ddBind)) . "'";
1140 if (!$isNew) {
1141 $where .= ' AND id <> ' . (int)$rid;
1142 }
1143
1144 if ($db->count($this->ddBind, $where) > 0) {
1145 $oForm->add_fld_error('bind_key', $oForm->get_fd_message('duplicate_bind_key'));
1146 $oForm->_msg_error = $oForm->get_fd_message('duplicate_bind_key');
1147 } else {
1148 $ok = $db->save($this->ddBind, $values, $isNew ? 0 : (int)$rid, 0, 1, 1, 1);
1149 $oForm->_msg_success = $oForm->get_fd_message($ok ? 'save_success' : 'save_error');
1150 if ($ok) {
1151 $oForm->_data = array_merge($oForm->_data, $values);
1152 }
1153 }
1154 }
1155 } else {
1156 $oForm->_msg_error = $oForm->get_fd_message('validation_error');
1157 }
1158 }
1159
1160 $oForm->add_obj('bind_generator', 'obj-value', $generatorForm->run());
1161
1162 return $oForm->run();
1163 }
1164
1165 private function decorate_bind_rows($rows) {
1166 $out = array();
1167 foreach ((array)$rows as $row) {
1168 $row['bind_ref'] = (string)($row['modul'] ?? '') . '|' . (string)($row['bind_key'] ?? '');
1169 $row['action'] = $this->tpl()->get_tpl('dbxWorkflow_admin|workflow-bind-row-action', array(
1170 'id' => (int)($row['id'] ?? 0),
1171 'bind_ref' => $this->h($row['bind_ref']),
1172 ));
1173 $out[] = $row;
1174 }
1175 return $out;
1176 }
1177
1178 private function list_binds() {
1179 $db = $this->db();
1180 $oReport = dbx()->get_system_obj('dbxReport');
1181 $oReport->init('workflow-module-binds', 'dbxWorkflow_admin|workflow-module-binds');
1182 $oReport->_dd = $this->ddBind;
1183 $oReport->_fd = 'dbxWorkflow|workflow-module-bind';
1184 $oReport->load_fd_messages();
1185 $oReport->_action = '?dbx_modul=dbxWorkflow_admin&dbx_run1=binds';
1186 $oReport->_pages = true;
1187 $oReport->_create_row_select = false;
1188 $oReport->_create_row_edit = false;
1189 $oReport->_create_row_delete = false;
1190 $oReport->_but_pagination = 5;
1191 $oReport->set_form_help_enabled(false);
1192 $oReport->_msg_info = $oReport->get_fd_message('list_info');
1193
1194 $cols = array('id', 'modul', 'bind_key', 'title', 'active', 'update_date');
1195 $oReport->_rflds = array(
1196 'id' => 'ID',
1197 'modul' => $oReport->get_fd_message('column_module'),
1198 'bind_key' => $oReport->get_fd_message('column_bind_key'),
1199 'title' => $oReport->get_fd_message('column_title'),
1200 'active' => $oReport->get_fd_message('column_active'),
1201 'update_date' => $oReport->get_fd_message('column_update'),
1202 'bind_ref' => $oReport->get_fd_message('column_reference'),
1203 'action' => $oReport->get_fd_message('column_action'),
1204 );
1205 $oReport->_rpt_format = array(
1206 'update_date' => 'php-datetime-usr',
1207 'action' => 'html',
1208 );
1209 $oReport->_rrows = 30;
1210 $oReport->_rpos = 0;
1211 $oReport->_count_all = $db->count($this->ddBind, '');
1212 $oReport->_rcount = $oReport->_count_all;
1213 $oReport->_rdata = $this->decorate_bind_rows($db->select($this->ddBind, '', $cols, 'modul', 'ASC', '', 30, 0));
1214
1215 return $this->frame($oReport->run(), $oReport);
1216 }
1217
1218 public function run() {
1219 $run = dbx()->get_modul_var('dbx_run1', 'list', 'parameter');
1220
1221 if ($run === 'install') return $this->install();
1222 if ($run === 'edit') return $this->edit();
1223 if ($run === 'edit_bind') return $this->edit_bind();
1224 if ($run === 'binds') return $this->list_binds();
1225 if ($run === 'instances') return $this->list_instances();
1226
1227 return $this->list_definitions();
1228 }
1229}
1230?>
action_token(string $scope='global')
Liefert ein sessiongebundenes Token fuer zustandsaendernde Link-Aktionen.
Definition dbxApi.php:1189
if(($result['name'] ?? '') !=='customer-design'||!is_dir($target)) if(substr_count( $defaultHtml, '[dbx:content]') !==1) foreach(array('[dbx:logo]', '[dbx:branding]', '[dbx:footer]') as $slot) if(( $metadata[ 'contract'] ?? '') !==\dbx\dbxDesign_admin\dbxDesignService::CONTRACT) if((dbx() ->config['dbx']['default_design_user'] ?? '') !=='customer-design') $validation
if( $demoId<=0) foreach(array('create_date', 'create_uid', 'update_date', 'update_uid', 'owner',) as $systemField) $items
DBX schema administration.