dbxapp 4.1.3
CMS, Shop, Workflows und modulare Geschäftsanwendungen
Loading...
Searching...
No Matches
dbxWorkflowEngine.class.php
Go to the documentation of this file.
1<?php
2namespace dbx\dbxWorkflow;
3
5
6 private $ddDefinition = 'dbxWorkflow|workflowDefinition';
7 private $ddInstance = 'dbxWorkflow|workflowInstance';
8 private $ddStep = 'dbxWorkflow|workflowStep';
9
10 private function tpl() {
11 return dbx()->get_system_obj('dbxTPL');
12 }
13
14 private function db() {
15 return dbx()->get_system_obj('dbxDB');
16 }
17
18 private function h($value) {
19 return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
20 }
21
22 private function now() {
23 return date('Y-m-d H:i:s');
24 }
25
26 private function read_json($value, $default = array()) {
27 $value = trim((string)$value);
28 if ($value === '') return $default;
29 $data = json_decode($value, true);
30 return is_array($data) ? $data : $default;
31 }
32
33 private function write_json($value) {
34 return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
35 }
36
37 private function default_definition() {
38 return array(
39 'workflow_key' => 'invoice_demo',
40 'title' => 'Rechnung erstellen',
41 'result' => 'Rechnung',
42 'description' => 'Zielorientierter Beispiel-Workflow: Alle für eine richtige Rechnung benötigten Angaben werden geprüft und anschließend in klaren Schritten vervollständigt.',
43 'needs' => array(
44 array(
45 'key' => 'customer',
46 'label' => 'Kunde',
47 'mode' => 'single',
48 'required' => true,
49 'actions' => array('select', 'create'),
50 'preferred' => 'select',
51 'question' => 'Welcher Kunde erhält die Rechnung?',
52 'validation' => 'exactly_one',
53 'missing_message' => 'Kein Kunde ausgewählt – der Workflow ist unvollständig.',
54 'resolver' => array('type' => 'select', 'label' => 'Kundenauswahl öffnen'),
55 'hint' => 'Waehle einen vorhandenen Kunden oder erfasse direkt einen neuen Kunden.',
56 'options' => array('Muster GmbH', 'Beispiel AG', 'Max Mustermann')
57 ),
58 array(
59 'key' => 'billing_address',
60 'label' => 'Rechnungsadresse',
61 'mode' => 'single',
62 'required' => true,
63 'actions' => array('form'),
64 'preferred' => 'form',
65 'question' => 'Welche vollständige Rechnungsadresse wird verwendet?',
66 'validation' => 'not_empty',
67 'missing_message' => 'Keine vollständige Rechnungsadresse vorhanden – der Workflow ist unvollständig.',
68 'resolver' => array('type' => 'form', 'label' => 'Adressformular öffnen'),
69 'depends_on' => 'customer',
70 'hint' => 'Prüfe Name, Straße, Postleitzahl und Ort der Rechnungsadresse.'
71 ),
72 array(
73 'key' => 'articles',
74 'label' => 'Artikel',
75 'mode' => 'multiple',
76 'required' => true,
77 'actions' => array('select', 'create'),
78 'preferred' => 'select',
79 'question' => 'Welche Artikel werden berechnet?',
80 'validation' => 'at_least_one',
81 'missing_message' => 'Es ist kein Artikel ausgewählt – der Workflow ist unvollständig.',
82 'resolver' => array('type' => 'select', 'label' => 'Artikelauswahl öffnen'),
83 'hint' => 'Ein oder mehrere Artikel sind fuer die Rechnung notwendig.',
84 'options' => array('Beratung 1 Stunde', 'Software Lizenz', 'Einrichtungspaket')
85 ),
86 array(
87 'key' => 'payment_terms',
88 'label' => 'Zahlungsbedingung',
89 'mode' => 'single',
90 'required' => true,
91 'actions' => array('select'),
92 'preferred' => 'select',
93 'question' => 'Welche Zahlungsbedingung ist ausgewählt?',
94 'validation' => 'exactly_one',
95 'missing_message' => 'Keine Zahlungsbedingung ausgewählt – der Workflow ist unvollständig.',
96 'resolver' => array('type' => 'select', 'label' => 'Zahlungsbedingung auswählen'),
97 'hint' => 'Wähle die für diese Rechnung geltende Zahlungsfrist.',
98 'options' => array(
99 array('value' => 'sofort', 'label' => 'Sofort fällig'),
100 array('value' => '7_tage', 'label' => '7 Tage netto'),
101 array('value' => '14_tage', 'label' => '14 Tage netto'),
102 array('value' => '30_tage', 'label' => '30 Tage netto')
103 )
104 ),
105 array(
106 'key' => 'invoice_number',
107 'label' => 'Rechnungsnummer',
108 'mode' => 'single',
109 'required' => true,
110 'actions' => array('form'),
111 'preferred' => 'form',
112 'question' => 'Welche eindeutige Rechnungsnummer wird verwendet?',
113 'validation' => 'not_empty',
114 'missing_message' => 'Keine Rechnungsnummer vorhanden – der Workflow ist unvollständig.',
115 'resolver' => array('type' => 'form', 'label' => 'Rechnungsnummer erzeugen'),
116 'depends_on' => 'customer',
117 'event' => 'Rechnungsnummer erzeugt',
118 'hint' => 'Erzeuge oder übernimm eine eindeutige Rechnungsnummer für den ausgewählten Kunden.'
119 )
120 ),
121 'finish' => array(
122 'label' => 'Rechnung erzeugen'
123 )
124 );
125 }
126
127 private function workflowModule() {
128 return dbx()->get_include_obj('dbxWorkflowModule', 'dbxWorkflow');
129 }
130
131 private function workflow_bar_data(string $topic, string $actions, string $title, string $subtitle = ''): array {
132 $help = dbx()->get_include_obj('dbxAdminHelp', 'dbxAdmin');
133 if (is_object($help) && method_exists($help, 'moduleBarTemplateData')) {
134 $data = $help->moduleBarTemplateData($topic, $actions, $title, 'bi-diagram-3', $subtitle);
135 if (is_array($data) && $data) return $data;
136 }
137
138 return array(
139 'bar_class' => 'dbx-module-bar',
140 'bar_title_class' => 'dbx-module-bar-titleblock',
141 'bar_actions_class' => 'dbx-module-bar-actions',
142 'bar_title' => $title,
143 'bar_icon' => 'bi-diagram-3',
144 'bar_subtitle' => $subtitle,
145 'bar_title_pre' => '',
146 'bar_title_heading_attrs' => '',
147 'bar_middle' => '',
148 'bar_extra' => '',
149 'bar_actions' => $actions,
150 );
151 }
152
168 private function runtime_form(int $iid, string $kind, string $template, array $replacements = array()) {
169 $form = dbx()->get_system_obj('dbxForm');
170 $form->init('workflow-' . $kind . '-' . max(0, $iid), $template);
171 $form->_action = $this->instance_action_url(
172 '?dbx_modul=dbxWorkflow&dbx_run1=run&iid=' . max(0, $iid),
173 $iid
174 );
175 $form->_msg_info = '';
176 foreach ($replacements as $key => $value) {
177 $form->add_rep((string)$key, $value);
178 }
179 return $form;
180 }
181
183 private function instance_action_scope(int $iid): string {
184 return 'dbxWorkflow.instance.' . max(0, $iid);
185 }
186
188 private function action_url(string $url, string $scope): string {
189 $separator = strpos($url, '?') === false ? '?' : '&';
190 return $url . $separator . 'dbx_token=' . rawurlencode(dbx()->action_token($scope));
191 }
192
193 private function instance_action_url(string $url, int $iid): string {
194 return $this->action_url($url, $this->instance_action_scope($iid));
195 }
196
198 private function has_instance_action_token(int $iid): bool {
199 $token = (string)dbx()->get_modul_var('dbx_token', '', 'varchar|max=128');
200 return dbx()->check_action_token($this->instance_action_scope($iid), $token);
201 }
202
209 private function register_guest_instance(int $iid): void {
210 if ($iid <= 0 || (int)dbx()->user() > 0) return;
211 $ids = dbx()->get_session_var('instance_ids', array(), 'access', 'dbxWorkflow');
212 $ids = is_array($ids) ? array_map('intval', $ids) : array();
213 $ids[] = $iid;
214 $ids = array_slice(array_values(array_unique(array_filter($ids))), -100);
215 dbx()->set_session_var('instance_ids', $ids, 'access', 'dbxWorkflow');
216 }
217
218 private function guest_can_access_instance(int $iid): bool {
219 if ($iid <= 0 || (int)dbx()->user() > 0) return false;
220 $ids = dbx()->get_session_var('instance_ids', array(), 'access', 'dbxWorkflow');
221 return is_array($ids) && in_array($iid, array_map('intval', $ids), true);
222 }
223
225 private function instance_write_access(): int {
226 return (int)dbx()->user() > 0 ? 1 : 0;
227 }
228
229 private function unavailable_definition_message(string $workflowKey): string {
230 $workflowKey = trim($workflowKey);
231 $detail = $workflowKey !== '' ? ' <code>' . $this->h($workflowKey) . '</code>' : '';
232 return $this->tpl()->get_tpl('dbx|alert-warning', array(
233 'msg' => 'Workflow' . $detail . ' wurde nicht gefunden oder ist nicht aktiv.',
234 ));
235 }
236
237 private function enrich_definition(array $definition, array $values = array()) {
238 return $this->workflowModule()->enrichDefinition($definition, $values);
239 }
240
241 private function ticket_demo_definition() {
242 return array(
243 'workflow_key' => 'ticket_demo',
244 'title' => 'Kontaktanfrage bearbeiten',
245 'result' => 'Kontaktanfrage-Antwort',
246 'description' => 'Bearbeitet Kontaktanfragen aus dbxContact ueber Modul-Binding contact_reply.',
247 'bind_ref' => 'dbxContact|contact_reply',
248 'needs' => array(
249 array(
250 'key' => 'contact_request',
251 'label' => 'Kontaktanfrage',
252 'mode' => 'single',
253 'required' => true,
254 'actions' => array('select'),
255 'preferred' => 'select',
256 'hint' => 'Offene Kontaktanfrage aus dbxContact auswaehlen.',
257 'options' => array()
258 ),
259 array(
260 'key' => 'status',
261 'label' => 'Status',
262 'mode' => 'single',
263 'required' => true,
264 'actions' => array('select'),
265 'preferred' => 'select',
266 'hint' => 'Bearbeitungsstatus in dbxContact (offen, beantwortet, geschlossen).',
267 'options' => array()
268 ),
269 array(
270 'key' => 'internal_note',
271 'label' => 'Interne Notiz',
272 'mode' => 'single',
273 'required' => false,
274 'actions' => array('form'),
275 'hint' => 'Optional: interne Bearbeitungsnotiz (nur im Workflow gespeichert).'
276 ),
277 array(
278 'key' => 'customer_reply',
279 'label' => 'Rueckmeldung',
280 'mode' => 'single',
281 'required' => true,
282 'actions' => array('form'),
283 'hint' => 'Antwort an den Anfragenden. Wird in dbxContact als reply_text gespeichert.'
284 ),
285 array(
286 'key' => 'send_mail',
287 'label' => 'Antwort per E-Mail senden',
288 'mode' => 'single',
289 'required' => true,
290 'actions' => array('select'),
291 'preferred' => 'select',
292 'hint' => 'Versand nur wenn in dbxContact mail_on_reply aktiviert ist.',
293 'options' => array(
294 array('value' => '1', 'label' => 'Ja, E-Mail senden'),
295 array('value' => '0', 'label' => 'Nein, nur speichern'),
296 )
297 )
298 ),
299 'finish' => array(
300 'label' => 'Kontaktanfrage abschliessen'
301 )
302 );
303 }
304
305 private function shop_article_publish_definition() {
306 return array(
307 'workflow_key' => 'shop_article_publish',
308 'title' => 'Neuen Artikel im Shop veroeffentlichen',
309 'result' => 'Neuer Artikel ist im Shop veroeffentlicht',
310 'description' => 'Fuehrt durch Artikelanlage, Gruppenzuordnung, Bilder, Verkaufsdaten und Freigabe. Die eigentliche Bearbeitung erfolgt in den dafuer vorgesehenen Shop-Formularen.',
311 'needs' => array(
312 array(
313 'key' => 'product_create',
314 'label' => 'Artikel anlegen',
315 'mode' => 'single',
316 'required' => true,
317 'actions' => array('module'),
318 'preferred' => 'module',
319 'hint' => 'Lege den Artikel im Shop-Artikel-Formular an. Danach hier bestaetigen und den gespeicherten Artikel im naechsten Schritt auswaehlen.',
320 'event' => 'Artikel angelegt',
321 'complete_label' => 'Artikel wurde im Formular gespeichert.',
322 'module_links' => array(
323 array(
324 'label' => 'Artikel anlegen',
325 'icon' => 'bi-plus-square',
326 'url' => '?dbx_modul=dbxShop_admin&dbx_run1=product_edit&id=0&workflow_preset=shop_article_publish',
327 'title' => 'Neuen Artikel anlegen',
328 'width' => '92%',
329 'height' => '90%'
330 )
331 )
332 ),
333 array(
334 'key' => 'product',
335 'label' => 'Gespeicherten Artikel auswaehlen',
336 'mode' => 'single',
337 'required' => true,
338 'actions' => array('select'),
339 'preferred' => 'select',
340 'hint' => 'Waehle den gerade gespeicherten Artikel. Dadurch kennt der Workflow den Datensatz fuer alle folgenden Aufgaben.',
341 'event' => 'Artikel ausgewaehlt',
342 'source' => array(
343 'dd' => 'dbxShop|shopProduct',
344 'where' => array('trash' => 0, 'active' => 1),
345 'fields' => array('id', 'sku', 'title'),
346 'value' => 'id',
347 'label' => '{sku} - {title}',
348 'order_field' => 'id',
349 'order_dir' => 'DESC',
350 'limit' => 300
351 )
352 ),
353 array(
354 'key' => 'product_group',
355 'label' => 'Artikelgruppe zuordnen',
356 'mode' => 'single',
357 'required' => true,
358 'actions' => array('module'),
359 'preferred' => 'module',
360 'hint' => 'Ordne den Artikel einer passenden Artikelgruppe zu. Falls die Gruppe noch fehlt, kannst du die Gruppenverwaltung direkt oeffnen.',
361 'event' => 'Artikelgruppe zugeordnet',
362 'depends_on' => 'product',
363 'complete_label' => 'Artikelgruppe ist zugeordnet.',
364 'module_links' => array(
365 array(
366 'label' => 'Artikel bearbeiten',
367 'icon' => 'bi-pencil-square',
368 'url' => '?dbx_modul=dbxShop_admin&dbx_run1=product_edit&id={product}',
369 'title' => 'Artikel bearbeiten',
370 'width' => '92%',
371 'height' => '90%'
372 ),
373 array(
374 'label' => 'Artikelgruppen',
375 'icon' => 'bi-diagram-3',
376 'url' => '?dbx_modul=dbxShop_admin&dbx_run1=groups',
377 'title' => 'Artikelgruppen bearbeiten',
378 'width' => '54%',
379 'height' => '84%'
380 )
381 )
382 ),
383 array(
384 'key' => 'images',
385 'label' => 'Bilder zuordnen',
386 'mode' => 'single',
387 'required' => true,
388 'actions' => array('module'),
389 'preferred' => 'module',
390 'hint' => 'Ordne Primaerbild und weitere Artikelbilder im Artikel-Formular zu. Dateien bleiben im CMS-Medienbrowser; hier wird nur die Zuordnung gepflegt.',
391 'event' => 'Bilder zugeordnet',
392 'depends_on' => 'product_group',
393 'complete_label' => 'Artikelbilder sind geprueft und zugeordnet.',
394 'module_links' => array(
395 array(
396 'label' => 'Artikelbilder bearbeiten',
397 'icon' => 'bi-images',
398 'url' => '?dbx_modul=dbxShop_admin&dbx_run1=product_edit&id={product}',
399 'title' => 'Artikelbilder bearbeiten',
400 'width' => '92%',
401 'height' => '90%'
402 )
403 )
404 ),
405 array(
406 'key' => 'sales_data',
407 'label' => 'Preis, Versand und Sichtbarkeit pruefen',
408 'mode' => 'single',
409 'required' => true,
410 'actions' => array('module'),
411 'preferred' => 'module',
412 'hint' => 'Pruefe Bruttopreis, MwSt.-Quelle, Versand-Quelle, Lagerbestand und ob der Artikel aktiv ist.',
413 'event' => 'Verkaufsdaten geprueft',
414 'depends_on' => 'images',
415 'complete_label' => 'Preis, Versand, Lager und Sichtbarkeit sind korrekt.',
416 'module_links' => array(
417 array(
418 'label' => 'Artikel pruefen',
419 'icon' => 'bi-clipboard-check',
420 'url' => '?dbx_modul=dbxShop_admin&dbx_run1=product_edit&id={product}',
421 'title' => 'Artikel pruefen',
422 'width' => '92%',
423 'height' => '90%'
424 )
425 )
426 ),
427 array(
428 'key' => 'final_check',
429 'label' => 'Freigabe',
430 'mode' => 'single',
431 'required' => true,
432 'actions' => array('select'),
433 'preferred' => 'select',
434 'hint' => 'Bestaetige, wie der Artikel nach dem Workflow behandelt werden soll. Beim Abschluss markiert der Workflow den Artikel als aktiv.',
435 'event' => 'Freigabe entschieden',
436 'depends_on' => 'sales_data',
437 'options' => array(
438 array('value' => 'draft', 'label' => 'Als Entwurf vorbereiten'),
439 array('value' => 'shop', 'label' => 'Im Shop veroeffentlichen')
440 )
441 )
442 ),
443 'finish' => array(
444 'label' => 'Artikel im Shop freigeben'
445 )
446 );
447 }
448
449 private function shop_ebay_publish_definition() {
450 return array(
451 'workflow_key' => 'shop_ebay_publish',
452 'title' => 'Bestehenden Artikel auf eBay veroeffentlichen',
453 'result' => 'Bestehender Artikel ist bei eBay veroeffentlicht',
454 'description' => 'Fuehrt durch die wenigen notwendigen Aufgaben: Artikel auswaehlen, eBay-Daten bestaetigen, Export ausfuehren und Statusmeldungen pruefen.',
455 'needs' => array(
456 array(
457 'key' => 'product',
458 'label' => 'Artikel auswaehlen',
459 'kind' => 'input',
460 'automation' => 'manual',
461 'mode' => 'single',
462 'required' => true,
463 'actions' => array('select'),
464 'preferred' => 'select',
465 'hint' => 'Waehle den bestehenden Shop-Artikel, der bei eBay veroeffentlicht werden soll.',
466 'event' => 'Artikel ausgewaehlt',
467 'source' => array(
468 'dd' => 'dbxShop|shopProduct',
469 'where' => array('trash' => 0, 'active' => 1),
470 'fields' => array('id', 'sku', 'title'),
471 'value' => 'id',
472 'label' => '{sku} - {title}',
473 'order_field' => 'sorter',
474 'order_dir' => 'ASC',
475 'limit' => 300
476 )
477 ),
478 array(
479 'key' => 'readiness_check',
480 'label' => 'eBay-Bereitschaft automatisch prüfen',
481 'kind' => 'check',
482 'automation' => 'observe',
483 'mode' => 'single',
484 'required' => true,
485 'actions' => array('select'),
486 'preferred' => 'select',
487 'hint' => 'Prüft automatisch, ob Channel, Zugang, Zuordnung, Kategorie und Business Policies grundsätzlich vollständig sind. Nur wenn keine eindeutige Prüfung möglich ist, bleibt eine manuelle Auswahl sichtbar.',
488 'event' => 'Technische Bereitschaft geprüft',
489 'depends_on' => 'product',
490 'options' => array(
491 array('value' => 'ready', 'label' => 'Grunddaten vollständig'),
492 array('value' => 'needs_work', 'label' => 'Daten oder Zuordnung ergänzen')
493 )
494 ),
495 array(
496 'key' => 'ebay_data',
497 'label' => 'eBay-Daten bearbeiten und bestaetigen',
498 'kind' => 'action',
499 'automation' => 'manual',
500 'mode' => 'single',
501 'required' => true,
502 'actions' => array('module'),
503 'preferred' => 'module',
504 'hint' => 'Pruefe und bestaetige alle eBay-relevanten Daten: eBay-Channel, Kategorie, Pflichtmerkmale, Business Policies, Preis, Versand und ggf. abweichende Channel-Werte.',
505 'event' => 'eBay-Daten bestaetigt',
506 'depends_on' => 'readiness_check',
507 'complete_label' => 'eBay-Daten sind geprueft und gespeichert.',
508 'module_links' => array(
509 array(
510 'label' => 'Artikel bearbeiten',
511 'icon' => 'bi-pencil-square',
512 'url' => '?dbx_modul=dbxShop_admin&dbx_run1=product_edit&id={product}',
513 'title' => 'Artikel bearbeiten',
514 'width' => '92%',
515 'height' => '90%'
516 ),
517 array(
518 'label' => 'eBay-Mapping',
519 'icon' => 'bi-sliders2',
520 'url' => '?dbx_modul=dbxShop_admin&dbx_run1=product_channel_mapping&id={product}&channel=ebay',
521 'title' => 'Channel-Mapping: eBay',
522 'width' => '68%',
523 'height' => '84%'
524 )
525 )
526 ),
527 array(
528 'key' => 'export_run',
529 'label' => 'Export durchfuehren',
530 'kind' => 'action',
531 'automation' => 'manual',
532 'mode' => 'single',
533 'required' => true,
534 'actions' => array('module'),
535 'preferred' => 'module',
536 'hint' => 'Fuehre den eBay-Export ueber den Shop-Connector aus. Der Connector speichert Exportstatus, Rueckmeldung und externe IDs beim Artikel-Channel.',
537 'event' => 'Export ausgefuehrt',
538 'depends_on' => 'ebay_data',
539 'complete_label' => 'eBay-Export wurde ausgefuehrt.',
540 'module_links' => array(
541 array(
542 'label' => 'eBay exportieren',
543 'icon' => 'bi-broadcast',
544 'url' => '?dbx_modul=dbxShop_admin&dbx_run1=product_edit&id={product}&export_channel=ebay',
545 'title' => 'eBay exportieren',
546 'width' => '92%',
547 'height' => '90%'
548 )
549 )
550 ),
551 array(
552 'key' => 'status_check',
553 'label' => 'Statusmeldungen pruefen',
554 'kind' => 'decision',
555 'automation' => 'observe',
556 'mode' => 'single',
557 'required' => true,
558 'actions' => array('select'),
559 'preferred' => 'select',
560 'hint' => 'Die Connector-Rückmeldung wird automatisch ausgewertet. Wenn noch keine eindeutige Rückmeldung vorliegt, kann der Status manuell eingeordnet werden.',
561 'event' => 'Status geprueft',
562 'depends_on' => 'export_run',
563 'options' => array(
564 array('value' => 'ok', 'label' => 'Erfolgreich / Listing-ID vorhanden'),
565 array('value' => 'open', 'label' => 'Offen / spaeter erneut pruefen'),
566 array('value' => 'error', 'label' => 'Fehler vorhanden')
567 )
568 ),
569 array(
570 'key' => 'ebay_view',
571 'label' => 'eBay-Angebot ansehen',
572 'kind' => 'check',
573 'automation' => 'manual',
574 'mode' => 'single',
575 'required' => false,
576 'actions' => array('module'),
577 'preferred' => 'module',
578 'hint' => 'Optionaler Kontrollschritt: Oeffne das veroeffentlichte eBay-Angebot und pruefe, ob Titel, Bilder, Preis, Versand und Beschreibung richtig angezeigt werden.',
579 'event' => 'eBay-Angebot angesehen',
580 'depends_on' => 'status_check',
581 'depends_value' => 'ok',
582 'complete_label' => 'eBay-Angebot wurde angesehen oder bewusst uebersprungen.',
583 'module_links' => array(
584 array(
585 'label' => 'eBay-Mapping',
586 'icon' => 'bi-sliders2',
587 'url' => '?dbx_modul=dbxShop_admin&dbx_run1=product_channel_mapping&id={product}&channel=ebay',
588 'title' => 'Channel-Mapping: eBay',
589 'width' => '68%',
590 'height' => '84%'
591 )
592 )
593 )
594 ),
595 'finish' => array(
596 'label' => 'eBay-Veroeffentlichung abschliessen'
597 )
598 );
599 }
600
601 public function default_definition_text() {
602 return json_encode($this->default_definition(), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
603 }
604
605 public function ticket_demo_definition_text() {
606 return json_encode($this->ticket_demo_definition(), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
607 }
608
609 private function normalize_key($value) {
610 $value = strtolower(trim((string)$value));
611 $value = preg_replace('/[^a-z0-9_]+/', '_', $value);
612 return trim($value, '_');
613 }
614
615 private function normalize_validation_rule($rule, $mode = 'single') {
616 $rule = strtolower(trim((string)$rule));
617 $allowed = array('not_empty', 'exactly_one', 'at_least_one', 'positive_integer', 'confirmed');
618 if (!in_array($rule, $allowed, true)) {
619 $rule = ((string)$mode === 'multiple') ? 'at_least_one' : 'exactly_one';
620 }
621 return $rule;
622 }
623
624 private function requirement_question(array $need): string {
625 $question = trim((string)($need['question'] ?? ''));
626 if ($question !== '') return $question;
627 $label = trim((string)($need['label'] ?? $need['key'] ?? 'Angabe'));
628 return 'Ist „' . $label . '“ vollständig und richtig?';
629 }
630
631 private function requirement_missing_message(array $need): string {
632 $message = trim((string)($need['missing_message'] ?? ''));
633 if ($message !== '') return $message;
634 $label = trim((string)($need['label'] ?? $need['key'] ?? 'Angabe'));
635 return $label . ' fehlt oder ist nicht vollständig – der Workflow ist unvollständig.';
636 }
637
638 private function requirement_resolver(array $need): array {
639 $resolver = is_array($need['resolver'] ?? null) ? $need['resolver'] : array();
640 $actions = array_values((array)($need['actions'] ?? array('form')));
641 $type = (string)($resolver['type'] ?? ($need['preferred'] ?? ($actions[0] ?? 'form')));
642 if (!in_array($type, array('form', 'select', 'create', 'module'), true)) $type = 'form';
643 $labels = array(
644 'form' => 'Eingabeformular öffnen',
645 'select' => 'Auswahlformular öffnen',
646 'create' => 'Erfassungsformular öffnen',
647 'module' => 'Modulformular öffnen',
648 );
649 $label = trim((string)($resolver['label'] ?? ''));
650 if ($label === '') $label = $labels[$type];
651 return array_merge($resolver, array('type' => $type, 'label' => $label));
652 }
653
654 private function derived_checks(array $needs, array $existingChecks = array()): array {
655 $existingByKey = array();
656 foreach ($existingChecks as $check) {
657 if (!is_array($check)) continue;
658 $key = $this->normalize_key($check['key'] ?? '');
659 if ($key !== '') $existingByKey[$key] = $check;
660 }
661
662 $checks = array();
663 foreach ($needs as $need) {
664 if (!is_array($need)) continue;
665 $key = $this->normalize_key($need['key'] ?? '');
666 $existing = $existingByKey[$key] ?? array();
667 $existingResolver = is_array($existing['resolver'] ?? null) ? $existing['resolver'] : array();
668 $resolver = array_merge($existingResolver, $this->requirement_resolver($need));
669 $checks[] = array_merge($existing, array(
670 'key' => $key,
671 'label' => (string)($need['label'] ?? $need['key'] ?? ''),
672 'question' => $this->requirement_question($need),
673 'validation' => $this->normalize_validation_rule($need['validation'] ?? '', $need['mode'] ?? 'single'),
674 'required' => !empty($need['required']),
675 'missing_message' => $this->requirement_missing_message($need),
676 'resolver' => $resolver,
677 ));
678 }
679 return $checks;
680 }
681
682 private function parse_need_line($line) {
683 $line = trim((string)$line);
684 if ($line === '' || strpos($line, '=') === false) return array();
685
686 list($name, $ruleText) = explode('=', $line, 2);
687 $name = trim($name);
688 $need = array(
689 'key' => $this->normalize_key($name),
690 'label' => $name,
691 'mode' => 'single',
692 'required' => true,
693 'actions' => array(),
694 'preferred' => '',
695 'hint' => '',
696 'options' => array()
697 );
698
699 foreach (explode('|', $ruleText) as $token) {
700 $token = trim($token);
701 if ($token === '') continue;
702
703 if ($token === 'single' || $token === 'multiple' || $token === 'multible') {
704 $need['mode'] = ($token === 'single') ? 'single' : 'multiple';
705 continue;
706 }
707
708 if ($token === 'required' || $token === 'need' || $token === 'must') {
709 $need['required'] = true;
710 continue;
711 }
712
713 if ($token === 'optional') {
714 $need['required'] = false;
715 continue;
716 }
717
718 if (strpos($token, 'label=') === 0) {
719 $need['label'] = trim(substr($token, 6));
720 continue;
721 }
722
723 if (strpos($token, 'hint=') === 0) {
724 $need['hint'] = trim(substr($token, 5));
725 continue;
726 }
727
728 if (strpos($token, 'options=') === 0) {
729 $items = array_map('trim', explode(',', substr($token, 8)));
730 $need['options'] = array_values(array_filter($items, 'strlen'));
731 continue;
732 }
733
734 $preferred = false;
735 if (substr($token, -1) === '!') {
736 $preferred = true;
737 $token = substr($token, 0, -1);
738 }
739
740 if (in_array($token, array('select', 'create', 'form', 'module'), true)) {
741 $need['actions'][] = $token;
742 if ($preferred) $need['preferred'] = $token;
743 }
744 }
745
746 if (!$need['actions']) $need['actions'] = array('form');
747 if (!$need['preferred']) $need['preferred'] = $need['actions'][0];
748
749 return $need;
750 }
751
752 public function normalize_definition($source, $workflowKey = '') {
753 if (is_array($source)) {
755 } else {
756 $text = trim((string)$source);
757 $json = $this->read_json($text, null);
758 if (is_array($json)) {
760 } else {
761 $definition = array('needs' => array());
762 foreach (preg_split('/\r\n|\r|\n/', $text) as $line) {
763 if (stripos(trim($line), 'result=') === 0) {
764 $definition['result'] = trim(substr(trim($line), 7));
765 continue;
766 }
767 $need = $this->parse_need_line($line);
768 if ($need) $definition['needs'][] = $need;
769 }
770 }
771 }
772
773 if (empty($definition['workflow_key'])) $definition['workflow_key'] = $workflowKey ?: 'workflow';
774 if (empty($definition['title'])) $definition['title'] = $definition['result'] ?? 'Workflow';
775 if (empty($definition['result'])) $definition['result'] = $definition['result_label'] ?? $definition['title'];
776 if (empty($definition['description'])) $definition['description'] = '';
777 if (empty($definition['finish']) || !is_array($definition['finish'])) {
778 $definition['finish'] = array('label' => $definition['result'] . ' erzeugen');
779 }
780
781 $needs = array();
782 foreach ((array)($definition['needs'] ?? array()) as $need) {
783 if (!is_array($need)) continue;
784 $key = $this->normalize_key($need['key'] ?? ($need['label'] ?? ''));
785 if ($key === '') continue;
786
787 $actions = $need['actions'] ?? array('form');
788 if (!is_array($actions)) $actions = array_filter(array_map('trim', explode(',', (string)$actions)));
789
790 $mode = (($need['mode'] ?? 'single') === 'multiple') ? 'multiple' : 'single';
791 $normalizedNeed = array_merge($need, array(
792 'key' => $key,
793 'label' => (string)($need['label'] ?? $key),
794 'kind' => in_array((string)($need['kind'] ?? ''), array('input', 'action', 'check', 'decision'), true)
795 ? (string)$need['kind']
796 : (in_array('module', $actions, true) ? 'action' : 'input'),
797 'automation' => ((string)($need['automation'] ?? 'manual') === 'observe') ? 'observe' : 'manual',
798 'mode' => $mode,
799 'required' => array_key_exists('required', $need) ? (bool)$need['required'] : true,
800 'actions' => array_values(array_intersect($actions, array('select', 'create', 'form', 'module'))) ?: array('form'),
801 'preferred' => (string)($need['preferred'] ?? ''),
802 'hint' => (string)($need['hint'] ?? ''),
803 'options' => array_values((array)($need['options'] ?? array())),
804 'event' => (string)($need['event'] ?? ($need['result_event'] ?? '')),
805 'depends_on' => $this->normalize_key($need['depends_on'] ?? ''),
806 'depends_value' => (string)($need['depends_value'] ?? ''),
807 'validation' => $this->normalize_validation_rule($need['validation'] ?? '', $mode),
808 'question' => $this->requirement_question(array_merge($need, array('key' => $key))),
809 'missing_message' => $this->requirement_missing_message(array_merge($need, array('key' => $key))),
810 'resolver' => $this->requirement_resolver($need),
811 ));
812 $needs[] = $normalizedNeed;
813 }
814
815 $definition['needs'] = $needs;
816 // Die Prüfung ist absichtlich abgeleitet. Ein Schritt wird nur einmal
817 // gepflegt und erscheint dadurch automatisch in der Prüfliste.
818 $definition['checks'] = $this->derived_checks($needs, (array)($definition['checks'] ?? array()));
819 if (!isset($definition['bind_ref'])) {
820 $definition['bind_ref'] = '';
821 }
822 return $definition;
823 }
824
825 public function load_definition($workflowKey, bool $activeOnly = true) {
826 $workflowKey = $workflowKey ?: (string)dbx()->get_config('dbxWorkflow', 'default_workflow');
827 if (!$workflowKey) $workflowKey = 'invoice_demo';
828
829 $where = array('workflow_key' => $workflowKey);
830 if ($activeOnly) $where['active'] = 1;
831 $rows = $this->db()->select($this->ddDefinition, $where, '*', 'id', 'DESC', '', 1, 0, 0);
832 if (is_array($rows) && isset($rows[0])) {
833 $row = $rows[0];
834 $definition = $this->normalize_definition($row['definition_json'] ?? '', $workflowKey);
835 $definition['workflow_key'] = $row['workflow_key'] ?? $workflowKey;
836 $definition['title'] = $row['title'] ?? ($definition['title'] ?? $workflowKey);
837 $definition['result'] = $row['result_label'] ?? ($definition['result'] ?? $definition['title']);
838 $definition['description'] = $row['description'] ?? ($definition['description'] ?? '');
839 return $this->enrich_definition($definition);
840 }
841
842 // Laufzeitdefinitionen stammen ausschliesslich aus der Verwaltung.
843 // Built-ins werden bei der Installation einmalig als editierbare
844 // Datensaetze angelegt und duerfen hier weder reaktiviert noch ueber
845 // eine Admin-Aenderung geschrieben werden.
846 return array();
847 }
848
849 private function definition_record(array $definition): array {
850 return array(
851 'workflow_key' => $definition['workflow_key'],
852 'title' => $definition['title'],
853 'result_label' => $definition['result'],
854 'description' => $definition['description'],
855 'definition_json' => json_encode($definition, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
856 'active' => 1
857 );
858 }
859
860 private function ensure_definition(array $definition): int {
861 $db = $this->db();
862 $workflowKey = (string)($definition['workflow_key'] ?? '');
863 if ($workflowKey === '') {
864 return 0;
865 }
866
867 $rows = $db->select($this->ddDefinition, array('workflow_key' => $workflowKey), array('id'), 'id', 'DESC', '', 1, 0, 0);
868 if (isset($rows[0]['id'])) {
869 return (int)$rows[0]['id'];
870 }
871
872 return (int)$db->insert($this->ddDefinition, $this->definition_record($definition), 0, 1, 1, 1);
873 }
874
875 public function seed_default_definition() {
876 return $this->ensure_definition($this->default_definition());
877 }
878
879 public function seed_ticket_demo_definition() {
880 return $this->ensure_definition($this->ticket_demo_definition());
881 }
882
883 public function seed_shop_definitions() {
884 return $this->ensure_shop_definitions();
885 }
886
887 public function ensure_shop_definitions() {
888 $this->ensure_definition($this->shop_article_publish_definition());
889 return $this->ensure_definition($this->shop_ebay_publish_definition());
890 }
891
892 public function seed_demo_definitions() {
893 dbx()->get_include_obj('dbxWorkflowBindRegistry', 'dbxWorkflow')->seedDefaultBinds();
894 $this->ensure_definition($this->default_definition());
895 $this->ensure_definition($this->ticket_demo_definition());
896 return $this->ensure_shop_definitions();
897 }
898
899 public function start($workflowKey) {
900 $definition = $this->load_definition($workflowKey);
901 if (!$definition) return $this->unavailable_definition_message((string)$workflowKey);
902 $startToken = (string)dbx()->get_modul_var('dbx_token', '', 'varchar|max=128');
903 if (!dbx()->check_action_token('dbxWorkflow.start', $startToken)) {
904 // Alte Direktlinks bleiben aufrufbar, mutieren aber nicht mehr blind:
905 // Die Uebersicht liefert einen frischen, bestaetigbaren Startlink.
906 return $this->overview((string)$workflowKey);
907 }
908
909 $uid = (int)dbx()->user();
910 $values = $this->workflowModule()->prefillStart(
912 (int)dbx()->get_modul_var('rid', 0, 'int')
913 );
914
915 $record = array(
916 'workflow_key' => $definition['workflow_key'],
917 'result_label' => $definition['result'],
918 'status' => 'running',
919 'current_need' => '',
920 'percent' => 0,
921 'step_percent' => 0,
922 'message' => 'Workflow gestartet.',
923 'definition_json' => $this->write_json($definition),
924 'data_json' => $this->write_json($values),
925 'owner' => $uid,
926 );
927
928 $db = $this->db();
929 $iid = ($db->insert($this->ddInstance, $record, 1, 1, 1, 1) === 1) ? (int)$db->get_insert_id() : 0;
930 if ($iid <= 0) {
931 return $this->tpl()->get_tpl('dbx|alert-warning', array('msg' => 'Workflow konnte nicht gestartet werden.'));
932 }
933 $this->register_guest_instance($iid);
934 return $this->render($iid, true);
935 }
936
937 private function load_instance($iid) {
938 $iid = (int)$iid;
939 if ($iid <= 0) return array();
940
941 if ((int)dbx()->user() > 0) {
942 // DD liefert fuer normale Nutzer automatisch den Owner-Filter und
943 // fuer Administratoren den vollen, konfigurierten Zugriff.
944 $rows = $this->db()->select($this->ddInstance, array('id' => $iid), '*', 'id', 'DESC', '', 1, 0, 1);
945 } else {
946 if (!$this->guest_can_access_instance($iid)) return array();
947 $rows = $this->db()->select($this->ddInstance, array('id' => $iid, 'owner' => 0), '*', 'id', 'DESC', '', 1, 0, 0);
948 }
949
950 return (is_array($rows) && isset($rows[0])) ? $rows[0] : array();
951 }
952
953 private function values_from_instance($instance) {
954 return $this->read_json($instance['data_json'] ?? '', array());
955 }
956
957 private function validate_need_value($need, $value): bool {
958 if (is_array($value) && !empty($value['skipped'])) return empty($need['required']);
959
960 $rule = $this->normalize_validation_rule($need['validation'] ?? '', $need['mode'] ?? 'single');
961 $items = is_array($value)
962 ? array_values(array_filter($value, fn($item) => trim((string)$item) !== ''))
963 : (trim((string)$value) !== '' ? array($value) : array());
964
965 $allowed = array();
966 foreach ((array)($need['options'] ?? array()) as $option) {
967 $optionValue = is_array($option) ? (string)($option['value'] ?? '') : (string)$option;
968 if ($optionValue !== '') $allowed[] = $optionValue;
969 }
970 $onlyAllowedValues = function(array $selected) use ($allowed): bool {
971 if (!$allowed) return true;
972 foreach ($selected as $item) {
973 if (!in_array((string)$item, $allowed, true)) return false;
974 }
975 return true;
976 };
977 $actions = (array)($need['actions'] ?? array());
978 $enforceAllowed = in_array('select', $actions, true)
979 && !array_intersect($actions, array('create', 'form', 'module'));
980
981 if ($rule === 'positive_integer') {
982 return count($items) === 1 && filter_var($items[0], FILTER_VALIDATE_INT) !== false && (int)$items[0] > 0;
983 }
984 if ($rule === 'confirmed') {
985 if (count($items) !== 1) return false;
986 return in_array(strtolower(trim((string)$items[0])), array('1', 'true', 'yes', 'ja', 'ok', 'confirmed', 'bestaetigt', 'bestätigt'), true);
987 }
988 if ($rule === 'exactly_one') return count($items) === 1 && (!$enforceAllowed || $onlyAllowedValues($items));
989 if ($rule === 'at_least_one') return count($items) >= 1 && (!$enforceAllowed || $onlyAllowedValues($items));
990 return count($items) >= 1;
991 }
992
993 private function is_done($values, $need) {
994 $key = $need['key'];
995 if (!array_key_exists($key, $values)) return false;
996 return $this->validate_need_value($need, $values[$key]);
997 }
998
999 private function value_matches($actual, $expected) {
1000 $expected = trim((string)$expected);
1001 if ($expected === '') return true;
1002 if (is_array($actual) && !empty($actual['skipped'])) return false;
1003 if (is_array($actual)) {
1004 foreach ($actual as $item) {
1005 if (trim((string)$item) === $expected) return true;
1006 }
1007 return false;
1008 }
1009 return trim((string)$actual) === $expected;
1010 }
1011
1012 private function dependency_has_value(string $key, array $values): bool {
1013 if ($key === '' || !array_key_exists($key, $values)) return false;
1014 $value = $values[$key];
1015 if (is_array($value) && !empty($value['skipped'])) return false;
1016 if (is_array($value)) {
1017 return count(array_filter($value, fn($item) => trim((string)$item) !== '')) > 0;
1018 }
1019 return trim((string)$value) !== '';
1020 }
1021
1022 private function need_is_applicable($need, $values) {
1023 $dependsOn = $this->normalize_key($need['depends_on'] ?? '');
1024 if ($dependsOn === '') return true;
1025 if (!$this->dependency_has_value($dependsOn, $values)) return false;
1026 return $this->value_matches($values[$dependsOn], $need['depends_value'] ?? '');
1027 }
1028
1029 private function next_need($definition, $values) {
1030 foreach ((array)$definition['needs'] as $need) {
1031 if (!$this->need_is_applicable($need, $values)) continue;
1032 if (!$this->is_done($values, $need)) return $need;
1033 }
1034 return array();
1035 }
1036
1037 private function completed_count($definition, $values) {
1038 $count = 0;
1039 foreach ((array)$definition['needs'] as $need) {
1040 if (!$this->need_is_applicable($need, $values)) continue;
1041 if ($this->is_done($values, $need)) $count++;
1042 }
1043 return $count;
1044 }
1045
1046 private function applicable_count($definition, $values) {
1047 $count = 0;
1048 foreach ((array)$definition['needs'] as $need) {
1049 if ($this->need_is_applicable($need, $values)) $count++;
1050 }
1051 return $count;
1052 }
1053
1059 private function progress_total_count($definition, $values) {
1060 $count = 0;
1061 foreach ((array)$definition['needs'] as $need) {
1062 $dependsOn = $this->normalize_key($need['depends_on'] ?? '');
1063 if ($dependsOn === '') {
1064 $count++;
1065 continue;
1066 }
1067 if (!$this->dependency_has_value($dependsOn, $values)) {
1068 $count++;
1069 continue;
1070 }
1071 if ($this->value_matches($values[$dependsOn], $need['depends_value'] ?? '')) $count++;
1072 }
1073 return $count;
1074 }
1075
1076 private function apply_command($instance, $cmd) {
1077 $cmd = strtolower(trim((string)$cmd));
1078 if (!$cmd) return $instance;
1079
1080 $iid = (int)($instance['id'] ?? 0);
1081 if (!$this->has_instance_action_token($iid)) {
1082 $instance['_transient_message'] = 'Aktion nicht ausgefuehrt: ungueltiges oder abgelaufenes Aktionstoken.';
1083 return $instance;
1084 }
1085
1086 $values = $this->values_from_instance($instance);
1087 $update = array();
1088
1089 if ($cmd === 'pause') {
1090 $update = array('status' => 'paused', 'message' => 'Workflow angehalten.');
1091 } elseif ($cmd === 'resume' || $cmd === 'continue') {
1092 $update = array('status' => 'running', 'message' => 'Workflow fortgesetzt.');
1093 } elseif ($cmd === 'cancel') {
1094 $update = array('status' => 'canceled', 'message' => 'Workflow abgebrochen.');
1095 } elseif ($cmd === 'restart') {
1096 $values = array();
1097 $update = array('status' => 'running', 'message' => 'Workflow neu gestartet.', 'data_json' => $this->write_json($values), 'percent' => 0, 'step_percent' => 0);
1098 }
1099
1100 if ($update) {
1101 $ok = $this->db()->update(
1102 $this->ddInstance,
1103 $update,
1104 array('id' => $iid),
1105 $this->instance_write_access(),
1106 1,
1107 1,
1108 1
1109 );
1110 if ($ok === 1) {
1111 $instance = array_merge($instance, $update);
1112 } else {
1113 $instance['message'] = 'Workflow-Aktion konnte nicht gespeichert werden.';
1114 }
1115 }
1116
1117 return $instance;
1118 }
1119
1120 private function post_value($need) {
1121 $value = '';
1122
1123 if (isset($_POST['workflow_skip']) && !$need['required']) {
1124 return array('skipped' => 1, 'label' => 'Uebersprungen');
1125 }
1126
1127 $created = trim((string)($_POST['created_value'] ?? ''));
1128 if ($created !== '') return $created;
1129
1130 if (isset($_POST['form_value'])) {
1131 return trim((string)$_POST['form_value']);
1132 }
1133
1134 if (isset($_POST['selected_value'])) {
1135 $value = $_POST['selected_value'];
1136 if (is_array($value)) {
1137 return array_values(array_filter(array_map('trim', $value), 'strlen'));
1138 }
1139 return trim((string)$value);
1140 }
1141
1142 return '';
1143 }
1144
1145 private function apply_submit($instance, $definition) {
1146 if (empty($_POST['workflow_submit']) && empty($_POST['workflow_finish'])) return $instance;
1147 if (($instance['status'] ?? '') !== 'running') return $instance;
1148
1149 $iid = (int)($instance['id'] ?? 0);
1150 $isFinish = !empty($_POST['workflow_finish']);
1151 $form = $this->runtime_form(
1152 $iid,
1153 $isFinish ? 'review' : 'step',
1154 $isFinish ? 'workflow-review' : 'workflow-step-choice'
1155 );
1156 if (!$form->submit()) {
1157 $instance['message'] = 'Ungueltiger oder abgelaufener Formular-Token.';
1158 return $instance;
1159 }
1160 if (!$this->has_instance_action_token($iid)) {
1161 $instance['message'] = 'Ungueltiger oder abgelaufener Aktions-Token.';
1162 return $instance;
1163 }
1164
1165 $values = $this->values_from_instance($instance);
1166
1167 if (!empty($_POST['workflow_finish'])) {
1168 $missing = $this->missing_required_labels($definition, $values);
1169 if ($missing) {
1170 $instance['message'] = 'Workflow unvollständig. Es fehlen: ' . implode(', ', $missing) . '.';
1171 return $instance;
1172 }
1173
1174 // Der Abschluss kann Modulupdates oder E-Mail-Versand ausloesen.
1175 // Vor dem externen Effekt wird die Instanz daher atomar beansprucht.
1176 // Ein paralleler oder wiederholter Request sieht "finishing" und
1177 // fuehrt applyFinish() nicht ein zweites Mal aus.
1178 $db = $this->db();
1179 if ($db->begin($this->ddInstance) !== 1) {
1180 $instance['message'] = 'Workflow-Abschluss konnte nicht atomar gestartet werden.';
1181 return $instance;
1182 }
1183 $claim = $db->update(
1184 $this->ddInstance,
1185 array('status' => 'finishing', 'message' => 'Workflow wird abgeschlossen.'),
1186 array('id' => $iid, 'status' => 'running'),
1187 $this->instance_write_access(),
1188 1,
1189 1,
1190 1
1191 );
1192 if ($claim !== 1 || (int)$db->_update_count !== 1 || $db->commit($this->ddInstance) !== 1) {
1193 $db->rollback($this->ddInstance);
1194 $current = $this->load_instance($iid);
1195 return $current ?: array_merge($instance, array('message' => 'Workflow wird bereits abgeschlossen.'));
1196 }
1197 $instance['status'] = 'finishing';
1198 $instance['message'] = 'Workflow wird abgeschlossen.';
1199
1200 $finishMessage = 'Workflow abgeschlossen.';
1201 try {
1202 $moduleResult = $this->workflowModule()->applyFinish($definition, $values);
1203 } catch (\Throwable $e) {
1204 dbx()->debug('#Workflow finish failed iid=(' . $iid . ') error=(' . $e->getMessage() . ')');
1205 $errorUpdate = array(
1206 'status' => 'error',
1207 'message' => 'Workflow-Abschluss ist fehlgeschlagen und muss geprueft werden.',
1208 );
1209 $db->update($this->ddInstance, $errorUpdate, array('id' => $iid), $this->instance_write_access(), 1, 1, 1);
1210 return array_merge($instance, $errorUpdate);
1211 }
1212 if (is_array($moduleResult)) {
1213 if (empty($moduleResult['ok'])) {
1214 $failedUpdate = array(
1215 'status' => 'running',
1216 'message' => $moduleResult['message'] ?? 'Workflow konnte nicht abgeschlossen werden.',
1217 );
1218 $db->update($this->ddInstance, $failedUpdate, array('id' => $iid), $this->instance_write_access(), 1, 1, 1);
1219 return array_merge($instance, $failedUpdate);
1220 }
1221 $finishMessage = (string)($moduleResult['message'] ?? $finishMessage);
1222 }
1223
1224 $update = array(
1225 'status' => 'finished',
1226 'percent' => 100,
1227 'step_percent' => 100,
1228 'current_need' => '',
1229 'message' => $finishMessage,
1230 'data_json' => $this->write_json($values)
1231 );
1232 if ($db->update($this->ddInstance, $update, array('id' => $iid), $this->instance_write_access(), 1, 1, 1) !== 1) {
1233 $instance['message'] = 'Abschluss wurde ausgefuehrt; der Instanzstatus muss administrativ geprueft werden.';
1234 return $instance;
1235 }
1236 return array_merge($instance, $update);
1237 }
1238
1239 $needKey = $this->normalize_key($_POST['need_key'] ?? '');
1240 $need = array();
1241 foreach ((array)$definition['needs'] as $candidate) {
1242 if ($candidate['key'] === $needKey) {
1243 $need = $candidate;
1244 break;
1245 }
1246 }
1247
1248 if (!$need) return $instance;
1249 $value = $this->post_value($need);
1250
1251 if (!$this->validate_need_value($need, $value)) {
1252 $instance['message'] = (string)($need['missing_message'] ?? ('Bitte einen gültigen Wert für „' . $need['label'] . '“ eintragen.'));
1253 return $instance;
1254 }
1255
1256 $values[$need['key']] = $value;
1257 $stepNo = $this->completed_count($definition, $values);
1258 $stepRecord = array(
1259 'instance_id' => (int)$instance['id'],
1260 'step_pos' => $stepNo,
1261 'need_key' => $need['key'],
1262 'action' => !empty($_POST['workflow_skip']) ? 'skip' : 'set',
1263 'status' => 'finished',
1264 'value_json' => $this->write_json($value),
1265 'message' => $need['label'] . ' erfasst.',
1266 'owner' => (int)dbx()->user()
1267 );
1268
1269 $total = max(1, $this->progress_total_count($definition, $values));
1270 $percent = (int)floor(($stepNo / $total) * 100);
1271 $update = array(
1272 'data_json' => $this->write_json($values),
1273 'percent' => $percent,
1274 'step_percent' => 100,
1275 'message' => $need['label'] . ' wurde uebernommen.'
1276 );
1277
1278 $db = $this->db();
1279 if ($db->begin($this->ddInstance) !== 1) {
1280 $instance['message'] = 'Workflow-Schritt konnte nicht atomar gespeichert werden.';
1281 return $instance;
1282 }
1283
1284 try {
1285 // Browser-Wiederholung desselben POSTs erzeugt keinen zweiten Step.
1286 $stepAccess = (int)dbx()->user() > 0 ? 1 : 0;
1287 $previous = $db->select(
1288 $this->ddStep,
1289 array('instance_id' => $iid, 'need_key' => $need['key'], 'status' => 'finished'),
1290 '*',
1291 'id',
1292 'DESC',
1293 '',
1294 1,
1295 0,
1296 $stepAccess
1297 );
1298 $sameStep = is_array($previous)
1299 && isset($previous[0])
1300 && (string)($previous[0]['value_json'] ?? '') === (string)$stepRecord['value_json'];
1301
1302 if (!$sameStep && $db->insert($this->ddStep, $stepRecord, 1, 1, 1, 1) !== 1) {
1303 throw new \RuntimeException('step_insert_failed');
1304 }
1305 if ($db->update(
1306 $this->ddInstance,
1307 $update,
1308 array('id' => $iid, 'status' => 'running'),
1309 $this->instance_write_access(),
1310 1,
1311 1,
1312 1
1313 ) !== 1) {
1314 throw new \RuntimeException('instance_update_failed');
1315 }
1316 if ($db->commit($this->ddInstance) !== 1) {
1317 throw new \RuntimeException('commit_failed');
1318 }
1319 } catch (\Throwable $e) {
1320 $db->rollback($this->ddInstance);
1321 dbx()->debug('#Workflow step failed iid=(' . $iid . ') error=(' . $e->getMessage() . ')');
1322 $instance['message'] = 'Workflow-Schritt konnte nicht gespeichert werden.';
1323 return $instance;
1324 }
1325
1326 return array_merge($instance, $update);
1327 }
1328
1329 private function apply_automations(array $instance, array $definition): array {
1330 if (($instance['status'] ?? '') !== 'running') return $instance;
1331
1332 $values = $this->values_from_instance($instance);
1333 $messages = array();
1334 $stepRecords = array();
1335 $limit = max(1, count((array)($definition['needs'] ?? array())));
1336
1337 for ($i = 0; $i < $limit; $i++) {
1338 $need = $this->next_need($definition, $values);
1339 if (!$need || (string)($need['automation'] ?? 'manual') !== 'observe') break;
1340
1341 $result = $this->workflowModule()->automateNeed($definition, $need, $values);
1342 if (!is_array($result) || !array_key_exists('value', $result)) break;
1343 $value = $result['value'];
1344 if ((is_array($value) && !$value) || (!is_array($value) && trim((string)$value) === '')) break;
1345
1346 $values[$need['key']] = $value;
1347 $stepNo = $this->completed_count($definition, $values);
1348 $message = trim((string)($result['message'] ?? ($need['label'] . ' automatisch geprüft.')));
1349 $messages[] = $message;
1350 $stepRecords[] = array(
1351 'instance_id' => (int)$instance['id'],
1352 'step_pos' => $stepNo,
1353 'need_key' => $need['key'],
1354 'action' => 'automation',
1355 'status' => 'finished',
1356 'value_json' => $this->write_json($value),
1357 'message' => $message,
1358 'owner' => (int)dbx()->user()
1359 );
1360 }
1361
1362 if (!$messages) return $instance;
1363
1364 $completed = $this->completed_count($definition, $values);
1365 $total = max(1, $this->progress_total_count($definition, $values));
1366 $update = array(
1367 'data_json' => $this->write_json($values),
1368 'percent' => (int)floor(($completed / $total) * 100),
1369 'step_percent' => 100,
1370 'message' => implode(' ', $messages),
1371 );
1372
1373 $db = $this->db();
1374 if ($db->begin($this->ddInstance) !== 1) {
1375 $instance['message'] = 'Automatische Workflow-Schritte konnten nicht atomar gespeichert werden.';
1376 return $instance;
1377 }
1378 try {
1379 foreach ($stepRecords as $stepRecord) {
1380 if ($db->insert($this->ddStep, $stepRecord, 1, 1, 1, 1) !== 1) {
1381 throw new \RuntimeException('automation_step_insert_failed');
1382 }
1383 }
1384 if ($db->update(
1385 $this->ddInstance,
1386 $update,
1387 array('id' => (int)$instance['id'], 'status' => 'running'),
1388 $this->instance_write_access(),
1389 1,
1390 1,
1391 1
1392 ) !== 1) {
1393 throw new \RuntimeException('automation_instance_update_failed');
1394 }
1395 if ($db->commit($this->ddInstance) !== 1) {
1396 throw new \RuntimeException('automation_commit_failed');
1397 }
1398 } catch (\Throwable $e) {
1399 $db->rollback($this->ddInstance);
1400 dbx()->debug('#Workflow automation failed iid=(' . (int)$instance['id'] . ') error=(' . $e->getMessage() . ')');
1401 $instance['message'] = 'Automatische Workflow-Schritte konnten nicht gespeichert werden.';
1402 return $instance;
1403 }
1404
1405 return array_merge($instance, $update);
1406 }
1407
1408 private function render_options($need, $currentValue = null) {
1409 $html = '';
1410 $currentValues = is_array($currentValue) ? array_map('strval', $currentValue) : array((string)$currentValue);
1411 foreach ((array)$need['options'] as $opt) {
1412 if (is_array($opt)) {
1413 $value = (string)($opt['value'] ?? '');
1414 $label = (string)($opt['label'] ?? $value);
1415 } else {
1416 $value = $label = (string)$opt;
1417 }
1418 if ($value === '') {
1419 continue;
1420 }
1421 $selected = in_array($value, $currentValues, true) ? ' selected' : '';
1422 $html .= $this->tpl()->get_tpl('dbxWorkflow|workflow-option', array(
1423 'value' => $this->h($value),
1424 'label' => $this->h($label),
1425 'selected' => $selected
1426 ));
1427 }
1428 return $html;
1429 }
1430
1431 private function render_step_context($definition, $need, $values) {
1432 return $this->workflowModule()->renderStepContext($definition, $need, $values);
1433 }
1434
1435 private function render_form_value($definition, $need, $values) {
1436 if (!in_array('form', (array)($need['actions'] ?? array()), true)) {
1437 return '';
1438 }
1439
1440 if (array_key_exists($need['key'], $values)) {
1441 return $this->h($values[$need['key']]);
1442 }
1443
1444 return $this->workflowModule()->renderFormValue($definition, $need, $values);
1445 }
1446
1447 private function workflow_value_for_token($key, array $values) {
1448 $key = $this->normalize_key($key);
1449 if ($key === '' || !array_key_exists($key, $values)) {
1450 return '';
1451 }
1452 $value = $values[$key];
1453 if (is_array($value) && !empty($value['skipped'])) {
1454 return '';
1455 }
1456 if (is_array($value)) {
1457 return implode(',', array_map('strval', $value));
1458 }
1459 return (string)$value;
1460 }
1461
1462 private function resolve_workflow_text($text, array $values, $urlEncode = false) {
1463 return preg_replace_callback('/\{([a-zA-Z0-9_]+)\}/', function($match) use ($values, $urlEncode) {
1464 $value = $this->workflow_value_for_token($match[1], $values);
1465 return $urlEncode ? rawurlencode($value) : $value;
1466 }, (string)$text);
1467 }
1468
1469 private function render_module_links($need, array $values) {
1470 $links = '';
1471 foreach ((array)($need['module_links'] ?? array()) as $link) {
1472 if (!is_array($link)) {
1473 continue;
1474 }
1475 $url = trim((string)($link['url'] ?? ''));
1476 if ($url === '') {
1477 continue;
1478 }
1479 $url = $this->resolve_workflow_text($url, $values, true);
1480 $label = trim((string)($link['label'] ?? 'Oeffnen'));
1481 $title = trim((string)($link['title'] ?? $label));
1482 $icon = trim((string)($link['icon'] ?? 'bi-box-arrow-up-right'));
1483 $width = trim((string)($link['width'] ?? '86%'));
1484 $height = trim((string)($link['height'] ?? '86%'));
1485 $links .= '<a class="btn btn-outline-primary btn-sm openWin dbx-win" href="' . $this->h($url)
1486 . '" data-url="' . $this->h($url)
1487 . '" data-title="' . $this->h($this->resolve_workflow_text($title, $values, false))
1488 . '" data-width="' . $this->h($width)
1489 . '" data-height="' . $this->h($height)
1490 . '" title="' . $this->h($this->resolve_workflow_text($title, $values, false))
1491 . '"><i class="' . $this->h($icon) . '"></i> ' . $this->h($label) . '</a>';
1492 }
1493 return $links;
1494 }
1495
1496 private function render_module_block($definition, $need, array $values) {
1497 if (!in_array('module', (array)($need['actions'] ?? array()), true)) {
1498 return '';
1499 }
1500
1501 $links = $this->render_module_links($need, $values);
1502 if ($links === '') {
1503 $links = '<div class="alert alert-warning mb-0">Fuer diese Aufgabe ist noch kein Modulformular hinterlegt.</div>';
1504 }
1505
1506 return $this->tpl()->get_tpl('dbxWorkflow|workflow-module-block', array(
1507 'label' => $this->h($need['label']),
1508 'module_links' => $links,
1509 'complete_value' => $this->h($need['event'] ?: ($need['label'] . ' erledigt')),
1510 'complete_label' => $this->h((string)($need['complete_label'] ?? 'Aufgabe wurde im geoeffneten Formular erledigt.'))
1511 ));
1512 }
1513
1514 private function render_step($iid, $definition, $need, $values, $targetId) {
1515 $actions = (array)$need['actions'];
1516 $selectBlock = '';
1517 $createBlock = '';
1518 $formBlock = '';
1519 $moduleBlock = '';
1520 $stepNo = $this->completed_count($definition, $values) + 1;
1521 $stepCount = max(1, $this->progress_total_count($definition, $values));
1522 $hint = (string)($need['hint'] ?? '');
1523 if (!empty($need['event'])) {
1524 $hint .= ($hint !== '' ? ' ' : '') . 'Zwischenergebnis: ' . (string)$need['event'];
1525 }
1526
1527 if (in_array('select', $actions, true)) {
1528 $selectBlock = $this->tpl()->get_tpl('dbxWorkflow|workflow-select-block', array(
1529 'label' => $this->h($need['label']),
1530 'options' => $this->render_options($need, $values[$need['key']] ?? null),
1531 'select_hint' => $need['mode'] === 'multiple' ? 'Mehrfachauswahl moeglich.' : 'Eine Auswahl ist notwendig.',
1532 'name_suffix' => $need['mode'] === 'multiple' ? '[]' : '',
1533 'select_attrs' => $need['mode'] === 'multiple' ? 'multiple size="5"' : ''
1534 ));
1535 }
1536
1537 if (in_array('create', $actions, true)) {
1538 $createBlock = $this->tpl()->get_tpl('dbxWorkflow|workflow-create-block', array(
1539 'label' => $this->h($need['label']),
1540 'create_placeholder' => $this->h($need['label'] . ' neu erfassen')
1541 ));
1542 }
1543
1544 if (in_array('form', $actions, true)) {
1545 $formBlock = $this->tpl()->get_tpl('dbxWorkflow|workflow-form-block', array(
1546 'label' => $this->h($need['label']),
1547 'form_placeholder' => $this->h($need['hint'] ?: $need['label']),
1548 'form_value' => $this->render_form_value($definition, $need, $values)
1549 ));
1550 }
1551
1552 if (in_array('module', $actions, true)) {
1553 $moduleBlock = $this->render_module_block($definition, $need, $values);
1554 }
1555
1556 $skip = !$need['required'] ? $this->tpl()->get_tpl('dbxWorkflow|workflow-skip-button') : '';
1557 $contextBlock = $this->render_step_context($definition, $need, $values);
1558
1559 $data = array(
1560 'action' => $this->h($this->instance_action_url(
1561 '?dbx_modul=dbxWorkflow&dbx_run1=run&iid=' . (int)$iid,
1562 (int)$iid
1563 )),
1564 'target_id' => $this->h($targetId),
1565 'need_key' => $this->h($need['key']),
1566 'step_no' => $stepNo,
1567 'step_count' => $stepCount,
1568 'label' => $this->h($need['label']),
1569 'question' => $this->h($this->requirement_question($need)),
1570 'requirement_badge' => !empty($need['required']) ? 'Pflichtangabe' : 'Optional',
1571 'validation_hint' => $this->h((string)($need['missing_message'] ?? '')),
1572 'hint' => $this->h($hint),
1573 'context_block' => $contextBlock,
1574 'select_block' => $selectBlock,
1575 'create_block' => $createBlock,
1576 'form_block' => $formBlock,
1577 'module_block' => $moduleBlock,
1578 'skip_button' => $skip
1579 );
1580
1581 return $this->runtime_form(
1582 (int)$iid,
1583 'step',
1584 'workflow-step-choice',
1585 $data
1586 )->run();
1587 }
1588
1589 private function render_requirements_check($iid, $definition, $values, string $status): string {
1590 $items = '';
1591 $requiredTotal = 0;
1592 $requiredDone = 0;
1593
1594 foreach ((array)($definition['needs'] ?? array()) as $need) {
1595 $key = (string)($need['key'] ?? '');
1596 if ($key === '') continue;
1597 $required = !empty($need['required']);
1598 if ($required) $requiredTotal++;
1599 $applicable = $this->need_is_applicable($need, $values);
1600 $done = $applicable && $this->is_done($values, $need);
1601 if ($required && $done) $requiredDone++;
1602
1603 $state = 'locked';
1604 $stateLabel = 'Wartet auf Voraussetzung';
1605 $icon = 'bi-lock';
1606 if ($done) {
1607 $state = 'done';
1608 $stateLabel = 'Vollständig';
1609 $icon = 'bi-check2-circle';
1610 } elseif ($applicable && $required) {
1611 $state = 'missing';
1612 $stateLabel = 'Unvollständig';
1613 $icon = 'bi-exclamation-circle';
1614 } elseif ($applicable) {
1615 $state = 'optional';
1616 $stateLabel = 'Optional';
1617 $icon = 'bi-circle';
1618 }
1619
1620 $resolver = $this->requirement_resolver($need);
1621 $action = '';
1622 if ($applicable && !$done && $status === 'running') {
1623 $url = '?dbx_modul=dbxWorkflow&dbx_run1=run&iid=' . (int)$iid . '&need=' . rawurlencode($key);
1624 $action = '<a class="btn btn-sm btn-outline-primary dbx-workflow-requirement-action" href="' . $this->h($url) . '"><i class="bi bi-pencil-square"></i> ' . $this->h((string)$resolver['label']) . '</a>';
1625 }
1626
1627 $items .= '<article class="dbx-workflow-requirement is-' . $state . '">'
1628 . '<span class="dbx-workflow-requirement-icon"><i class="bi ' . $icon . '"></i></span>'
1629 . '<div class="dbx-workflow-requirement-copy"><strong>' . $this->h($this->requirement_question($need)) . '</strong>'
1630 . '<small>' . $this->h((string)($need['label'] ?? $key)) . ' · ' . $this->h((string)($need['missing_message'] ?? '')) . '</small></div>'
1631 . '<span class="dbx-workflow-requirement-state">' . $this->h($stateLabel) . '</span>'
1632 . $action
1633 . '</article>';
1634 }
1635
1636 $complete = $requiredTotal > 0 && $requiredDone >= $requiredTotal;
1637 $summary = $complete
1638 ? 'Alle ' . $requiredTotal . ' Pflichtangaben sind vollständig.'
1639 : $requiredDone . ' von ' . $requiredTotal . ' Pflichtangaben sind vollständig.';
1640
1641 return '<section class="dbx-workflow-requirements' . ($complete ? ' is-complete' : '') . '">'
1642 . '<header><span class="dbx-workflow-stage-number">2</span><div><small>Automatisch aus den Schritten abgeleitet</small><h3>Prüfung</h3><p>' . $this->h($summary) . '</p></div></header>'
1643 . '<div class="dbx-workflow-requirement-list">' . $items . '</div>'
1644 . '</section>';
1645 }
1646
1647 private function value_label($definition, $need, $value) {
1648 $formatted = $this->workflowModule()->formatValueLabel($definition, $need, $value);
1649 if ($formatted !== null) {
1650 return $formatted;
1651 }
1652
1653 if (is_array($value) && !empty($value['skipped'])) return '<em>Uebersprungen</em>';
1654 if (is_array($value)) return $this->h(implode(', ', $value));
1655 return nl2br($this->h($value));
1656 }
1657
1658 private function render_review_rows($definition, $values) {
1659 $rows = '';
1660 foreach ((array)$definition['needs'] as $need) {
1661 if (!$this->need_is_applicable($need, $values)) continue;
1662 $rows .= $this->tpl()->get_tpl('dbxWorkflow|workflow-review-row', array(
1663 'label' => $this->h($need['label']),
1664 'value' => $this->value_label($definition, $need, $values[$need['key']] ?? '')
1665 ));
1666 }
1667 return $rows;
1668 }
1669
1670 private function missing_required_labels($definition, $values): array {
1671 $missing = array();
1672 foreach ((array)$definition['needs'] as $need) {
1673 if (!$this->need_is_applicable($need, $values)) {
1674 continue;
1675 }
1676 if (empty($need['required'])) {
1677 continue;
1678 }
1679 if (!$this->is_done($values, $need)) {
1680 $missing[] = (string)($need['label'] ?? $need['key'] ?? '');
1681 }
1682 }
1683 return array_values(array_filter($missing, fn($value) => trim((string)$value) !== ''));
1684 }
1685
1686 private function render_final_status($definition, $values, string $status): string {
1687 $completed = $this->completed_count($definition, $values);
1688 $total = max(1, $this->progress_total_count($definition, $values));
1689 $missing = $this->missing_required_labels($definition, $values);
1690 return $this->workflowModule()->renderFinalStatus($definition, $values, $status, $completed, $total, $missing);
1691 }
1692
1693 private function render_review($iid, $definition, $values, $targetId) {
1694 return $this->runtime_form((int)$iid, 'review', 'workflow-review', array(
1695 'action' => $this->h($this->instance_action_url(
1696 '?dbx_modul=dbxWorkflow&dbx_run1=run&iid=' . (int)$iid,
1697 (int)$iid
1698 )),
1699 'target_id' => $this->h($targetId),
1700 'result_label' => $this->h($definition['result']),
1701 'finish_label' => $this->h($definition['finish']['label'] ?? ($definition['result'] . ' erzeugen')),
1702 'rows' => $this->render_review_rows($definition, $values),
1703 'final_status' => $this->render_final_status($definition, $values, 'running')
1704 ))->run();
1705 }
1706
1707 private function status_label($status) {
1708 $map = array(
1709 'running'=>'Laeuft',
1710 'finishing'=>'Wird abgeschlossen',
1711 'paused'=>'Angehalten',
1712 'canceled'=>'Abgebrochen',
1713 'finished'=>'Fertig',
1714 'error'=>'Fehler'
1715 );
1716 return $map[$status] ?? $status;
1717 }
1718
1719 private function status_class($status) {
1720 if ($status === 'finished') return 'bg-success';
1721 if ($status === 'paused' || $status === 'finishing') return 'bg-warning';
1722 if ($status === 'canceled' || $status === 'error') return 'bg-danger';
1723 return 'bg-primary';
1724 }
1725
1726 private function status_icon($status) {
1727 if ($status === 'finished') return 'bi bi-check2-circle';
1728 if ($status === 'paused') return 'bi bi-pause-fill';
1729 if ($status === 'finishing') return 'bi bi-hourglass-split';
1730 if ($status === 'canceled' || $status === 'error') return 'bi bi-exclamation-triangle';
1731 return 'bi bi-play-fill';
1732 }
1733
1734 public function render($iid, bool $allowAutomations = false) {
1735 $iid = (int)$iid;
1736 $instance = $this->load_instance($iid);
1737 if (!$instance) {
1738 return $this->tpl()->get_tpl('dbx|alert-warning', array('msg' => 'Workflow-Instanz nicht gefunden.'));
1739 }
1740
1741 $workflowKey = (string)($instance['workflow_key'] ?? '');
1742 // Neue Instanzen besitzen einen Definitions-Snapshot. Dadurch wirken
1743 // Admin-Aenderungen nur auf neue Starts. Alte Instanzen ohne Snapshot
1744 // verwenden fuer die Abwaertskompatibilitaet weiterhin den Datensatz.
1745 $snapshot = $this->read_json($instance['definition_json'] ?? '', array());
1746 if ($snapshot) {
1747 $baseDefinition = $this->normalize_definition($snapshot, $workflowKey);
1748 // Das beim Start eingebettete Binding gehoert zum Snapshot. Eine
1749 // spaetere Aenderung desselben bind_ref darf es nicht ersetzen.
1750 $baseDefinition['bind_ref'] = '';
1751 } else {
1752 $baseDefinition = $this->load_definition($workflowKey, false);
1753 }
1754 if (!$baseDefinition) return $this->unavailable_definition_message($workflowKey);
1755 $definition = $this->enrich_definition($baseDefinition, $this->values_from_instance($instance));
1756 $instance = $this->apply_command($instance, dbx()->get_modul_var('proc_cmd', '', 'parameter'));
1757 $instance = $this->apply_submit($instance, $definition);
1758 $values = $this->values_from_instance($instance);
1759 $definition = $this->enrich_definition($baseDefinition, $values);
1760 if ($allowAutomations || $this->has_instance_action_token($iid)) {
1761 $instance = $this->apply_automations($instance, $definition);
1762 }
1763 $values = $this->values_from_instance($instance);
1764 $definition = $this->enrich_definition($baseDefinition, $values);
1765 $status = (string)($instance['status'] ?? 'running');
1766 $targetId = 'dbx_workflow_' . $iid;
1767
1768 $stepContent = '';
1769 if ($status === 'finished') {
1770 $stepContent = $this->tpl()->get_tpl('dbxWorkflow|workflow-finished', array(
1771 'result_label' => $this->h($definition['result']),
1772 'rows' => $this->render_review_rows($definition, $values),
1773 'final_status' => $this->render_final_status($definition, $values, $status),
1774 'new_url' => $this->h($this->action_url(
1775 '?dbx_modul=dbxWorkflow&dbx_run1=start&workflow=' . rawurlencode($definition['workflow_key']),
1776 'dbxWorkflow.start'
1777 ))
1778 ));
1779 } elseif ($status === 'paused' || $status === 'finishing' || $status === 'canceled' || $status === 'error') {
1780 $stepContent = $this->tpl()->get_tpl('dbx|alert-info', array('msg' => $instance['message'] ?? $this->status_label($status)));
1781 } else {
1782 $requested = $this->normalize_key(dbx()->get_modul_var('need', '', 'parameter'));
1783 $canReview = $this->completed_count($definition, $values) >= max(1, $this->applicable_count($definition, $values));
1784 $need = ($requested === 'review') ? array() : $this->requested_need($definition, $values);
1785 if (!$need && $requested !== 'review') {
1786 $need = $this->next_need($definition, $values);
1787 }
1788 if ($need) {
1789 $stepContent = $this->render_step($iid, $definition, $need, $values, $targetId);
1790 $instance['current_need'] = $need['key'];
1791 } elseif ($canReview) {
1792 $stepContent = $this->render_review($iid, $definition, $values, $targetId);
1793 $instance['current_need'] = 'review';
1794 } else {
1795 $need = $this->next_need($definition, $values);
1796 if ($need) {
1797 $stepContent = $this->render_step($iid, $definition, $need, $values, $targetId);
1798 $instance['current_need'] = $need['key'];
1799 }
1800 }
1801 }
1802
1803 $completed = $this->completed_count($definition, $values);
1804 $total = max(1, $this->progress_total_count($definition, $values));
1805 $percent = ($status === 'finished') ? 100 : (int)floor(($completed / $total) * 100);
1806 $message = $instance['_transient_message'] ?? ($instance['message'] ?? '');
1807 if ($status === 'running' && $message === '') $message = 'Naechsten Schritt ausfuellen.';
1808 $stepsNav = $this->render_steps_nav($iid, $definition, $values, (string)($instance['current_need'] ?? ''), $status);
1809 $requirementsCheck = $this->render_requirements_check($iid, $definition, $values, $status);
1810
1811 $stateUpdate = array(
1812 'current_need' => $instance['current_need'] ?? '',
1813 'percent' => $percent,
1814 'step_percent' => ($status === 'finished') ? 100 : 0,
1815 'message' => $message
1816 );
1817 if (isset($instance['_transient_message'])) {
1818 unset($stateUpdate['message']);
1819 }
1820 foreach ($stateUpdate as $field => $value) {
1821 if ((string)($instance[$field] ?? '') === (string)$value) {
1822 unset($stateUpdate[$field]);
1823 }
1824 }
1825 if ($stateUpdate) {
1826 $this->db()->update(
1827 $this->ddInstance,
1828 $stateUpdate,
1829 array('id' => $iid),
1830 $this->instance_write_access(),
1831 1,
1832 1,
1833 0
1834 );
1835 $instance = array_merge($instance, $stateUpdate);
1836 }
1837
1838 $nextUrl = '?dbx_modul=dbxWorkflow&dbx_run1=run&iid=' . $iid;
1839 $instanceActionUrl = $this->instance_action_url($nextUrl, $iid);
1840 $restartUrl = $instanceActionUrl . '&proc_cmd=restart';
1841 $autostart = 0;
1842 $interval = (int)dbx()->get_config('dbxWorkflow', 'step_interval');
1843 $stepPercent = ($status === 'finished') ? 100 : 0;
1844 $statusBadge = '<span class="badge ' . $this->h($this->status_class($status)) . '"><i class="' . $this->h($this->status_icon($status)) . '"></i> ' . $this->h($this->status_label($status)) . '</span>';
1845 $processAttrs = 'data-dbx="lib=process|id=' . $this->h($targetId)
1846 . '|url=' . $this->h($nextUrl)
1847 . '|interval=' . $interval
1848 . '|autostart=' . $autostart . '"'
1849 . ' data-process-status="' . $this->h($status) . '"'
1850 . ' data-process-percent="' . $percent . '"'
1851 . ' data-process-step-percent="' . $stepPercent . '"'
1852 . ' data-process-next-url="' . $this->h($nextUrl) . '"'
1853 . ' data-process-pause-url="' . $this->h($instanceActionUrl . '&proc_cmd=pause') . '"'
1854 . ' data-process-resume-url="' . $this->h($instanceActionUrl . '&proc_cmd=resume') . '"'
1855 . ' data-process-continue-url="' . $this->h($instanceActionUrl . '&proc_cmd=continue') . '"'
1856 . ' data-process-cancel-url="' . $this->h($instanceActionUrl . '&proc_cmd=cancel') . '"'
1857 . ' data-process-restart-url="' . $this->h($restartUrl) . '"'
1858 . ' data-process-autostart="' . $autostart . '"'
1859 . ' data-process-interval="' . $interval . '"';
1860
1861 return $this->tpl()->get_tpl('dbxWorkflow|workflow-frame', array_merge(
1862 $this->workflow_bar_data('workflow_run', $statusBadge, $this->h($definition['title']), $this->h($definition['result'])),
1863 array(
1864 'target_id' => $this->h($targetId),
1865 'title' => $this->h($definition['title']),
1866 'result_label' => $this->h($definition['result']),
1867 'status' => $this->h($status),
1868 'status_label' => $this->h($this->status_label($status)),
1869 'status_class' => $this->h($this->status_class($status)),
1870 'status_icon' => $this->h($this->status_icon($status)),
1871 'process_bar_class' => $this->h($status === 'finished' ? 'bg-success' : 'bg-primary'),
1872 'percent' => $percent,
1873 'step_percent' => $stepPercent,
1874 'message' => $this->h($message),
1875 'requirements_check' => $requirementsCheck,
1876 'steps_nav' => $stepsNav,
1877 'step_content' => $stepContent,
1878 'next_url' => $this->h($nextUrl),
1879 'pause_url' => $this->h($instanceActionUrl . '&proc_cmd=pause'),
1880 'resume_url' => $this->h($instanceActionUrl . '&proc_cmd=resume'),
1881 'continue_url' => $this->h($instanceActionUrl . '&proc_cmd=continue'),
1882 'cancel_url' => $this->h($instanceActionUrl . '&proc_cmd=cancel'),
1883 'restart_url' => $this->h($restartUrl),
1884 'autostart' => $autostart,
1885 'interval' => $interval,
1886 'frame_id' => $this->h($targetId),
1887 'frame_panel_class' => 'py-3 dbx-workflow dbx-process',
1888 'frame_panel_attrs' => $processAttrs,
1889 'frame_subbar' => '',
1890 'frame_form_open' => '',
1891 'frame_form_close' => '',
1892 'frame_body_class' => '',
1893 'frame_body_head' => '',
1894 'frame_body_tail' => '',
1895 )
1896 ));
1897 }
1898
1899 private function requested_need($definition, $values) {
1900 $requested = $this->normalize_key(dbx()->get_modul_var('need', '', 'parameter'));
1901 if ($requested === '') {
1902 return array();
1903 }
1904
1905 foreach ((array)$definition['needs'] as $need) {
1906 if (($need['key'] ?? '') !== $requested) {
1907 continue;
1908 }
1909 if (!$this->need_is_applicable($need, $values)) {
1910 return array();
1911 }
1912 return $need;
1913 }
1914
1915 return array();
1916 }
1917
1918 private function render_steps_nav($iid, $definition, $values, $activeKey, $status) {
1919 $html = '<div class="dbx-workflow-step-nav" aria-label="Workflow Schritte">';
1920 $pos = 0;
1921 $allDone = true;
1922
1923 foreach ((array)$definition['needs'] as $need) {
1924 $pos++;
1925 $key = (string)($need['key'] ?? '');
1926 $applicable = $this->need_is_applicable($need, $values);
1927 $done = $applicable && $this->is_done($values, $need);
1928 if ($applicable && !$done) {
1929 $allDone = false;
1930 }
1931
1932 $state = 'locked';
1933 $icon = 'bi-lock';
1934 $stateLabel = 'Gesperrt';
1935 if ($applicable) {
1936 if ($key === $activeKey) {
1937 $state = 'active';
1938 $icon = 'bi-pencil-square';
1939 $stateLabel = 'Aktuell';
1940 } elseif ($done) {
1941 $state = 'done';
1942 $icon = 'bi-check2';
1943 $stateLabel = 'Erledigt';
1944 } else {
1945 $state = 'open';
1946 $icon = 'bi-circle';
1947 $stateLabel = 'Offen';
1948 }
1949 }
1950
1951 $dependsOn = $this->normalize_key($need['depends_on'] ?? '');
1952 $title = (string)($need['label'] ?? $key);
1953 if (!$applicable && $dependsOn !== '') {
1954 $title .= ' - erst nach "' . $dependsOn . '" moeglich';
1955 }
1956
1957 $inner = '<span class="dbx-workflow-step-nav-no">' . $pos . '</span>'
1958 . '<span class="dbx-workflow-step-nav-text">'
1959 . '<strong>' . $this->h((string)($need['label'] ?? $key)) . '</strong>'
1960 . '<small><i class="bi ' . $icon . '"></i> ' . $this->h($stateLabel) . '</small>'
1961 . '</span>';
1962
1963 if ($applicable && $status === 'running') {
1964 $url = '?dbx_modul=dbxWorkflow&dbx_run1=run&iid=' . (int)$iid . '&need=' . rawurlencode($key);
1965 $html .= '<a class="dbx-workflow-step-nav-item is-' . $state . '" href="' . $this->h($url) . '" title="' . $this->h($title) . '">' . $inner . '</a>';
1966 } else {
1967 $html .= '<span class="dbx-workflow-step-nav-item is-' . $state . '" title="' . $this->h($title) . '">' . $inner . '</span>';
1968 }
1969 }
1970
1971 $reviewState = ($activeKey === 'review') ? 'active' : ($allDone ? 'open' : 'locked');
1972 $reviewIcon = ($activeKey === 'review') ? 'bi-pencil-square' : ($allDone ? 'bi-check2-circle' : 'bi-lock');
1973 $reviewLabel = ($activeKey === 'review') ? 'Aktuell' : ($allDone ? 'Pruefen' : 'Gesperrt');
1974 $reviewInner = '<span class="dbx-workflow-step-nav-no"><i class="bi bi-flag"></i></span>'
1975 . '<span class="dbx-workflow-step-nav-text"><strong>Abschluss</strong><small><i class="bi ' . $reviewIcon . '"></i> ' . $this->h($reviewLabel) . '</small></span>';
1976 if ($allDone && $status === 'running') {
1977 $url = '?dbx_modul=dbxWorkflow&dbx_run1=run&iid=' . (int)$iid . '&need=review';
1978 $html .= '<a class="dbx-workflow-step-nav-item is-' . $reviewState . '" href="' . $this->h($url) . '" title="Workflow pruefen und abschliessen">' . $reviewInner . '</a>';
1979 } else {
1980 $html .= '<span class="dbx-workflow-step-nav-item is-' . $reviewState . '" title="Workflow pruefen und abschliessen">' . $reviewInner . '</span>';
1981 }
1982
1983 return $html . '</div>';
1984 }
1985
1986 public function overview($workflowKey) {
1987 $definition = $this->load_definition($workflowKey);
1988 if (!$definition) return $this->unavailable_definition_message((string)$workflowKey);
1989 return $this->tpl()->get_tpl('dbxWorkflow|workflow-overview', array_merge(
1990 $this->workflow_bar_data('workflow_use', '', $this->h($definition['title']), 'Workflow starten'),
1991 array(
1992 'title' => $this->h($definition['title']),
1993 'description' => $this->h($definition['description']),
1994 'result_label' => $this->h($definition['result']),
1995 'start_url' => $this->h($this->action_url(
1996 '?dbx_modul=dbxWorkflow&dbx_run1=start&workflow=' . rawurlencode($definition['workflow_key']),
1997 'dbxWorkflow.start'
1998 )),
1999 'frame_id' => 'dbx_workflow_overview',
2000 'frame_panel_class' => 'dbx-workflow',
2001 'frame_panel_attrs' => '',
2002 'frame_subbar' => '',
2003 'frame_form_open' => '',
2004 'frame_form_close' => '',
2005 'frame_body_class' => '',
2006 'frame_body_head' => '',
2007 'frame_body_tail' => '',
2008 )
2009 ));
2010 }
2011}
2012?>
load_definition($workflowKey, bool $activeOnly=true)
normalize_definition($source, $workflowKey='')
render($iid, bool $allowAutomations=false)
$messages
Definition config.fd.php:2
action_url(string $url, string $action='', array $bindings=array())
Tokenisiert eine automatisch erkannte zustandsaendernde Route.
Definition dbxApi.php:1270
user($key='id')
Liest einen Wert des aktuellen Benutzers.
Definition dbxApi.php:1305
action_token(string $scope='global')
Liefert ein sessiongebundenes Token fuer zustandsaendernde Link-Aktionen.
Definition dbxApi.php:1189
check_action_token(string $scope='global', string $token='')
Prueft ein sessiongebundenes Aktions-Token.
Definition dbxApi.php:1206
if(!is_string($cms)||!is_string($process)) $checks
if(!is_object( $db)) if($db->connect_db_server($server) !==1) $created
foreach( $iterator as $file) if(! $groups) $expected
if($out !==$expected) $missing
if(!defined( 'IMG_WEBP')) define( 'IMG_WEBP'
if( $demoId<=0) foreach(array('create_date', 'create_uid', 'update_date', 'update_uid', 'owner',) as $systemField) $items
DBX schema administration.
$requested
Definition run.php:7