6 private $ddDefinition =
'dbxWorkflow|workflowDefinition';
7 private $ddInstance =
'dbxWorkflow|workflowInstance';
8 private $ddStep =
'dbxWorkflow|workflowStep';
10 private function tpl() {
11 return dbx()->get_system_obj(
'dbxTPL');
14 private function db() {
15 return dbx()->get_system_obj(
'dbxDB');
18 private function h(
$value) {
19 return htmlspecialchars((
string)
$value, ENT_QUOTES,
'UTF-8');
22 private function now() {
23 return date(
'Y-m-d H:i:s');
26 private function read_json(
$value, $default = array()) {
28 if (
$value ===
'')
return $default;
29 $data = json_decode(
$value,
true);
30 return is_array($data) ? $data : $default;
33 private function write_json(
$value) {
34 return json_encode(
$value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
37 private function default_definition() {
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.',
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')
59 'key' =>
'billing_address',
60 'label' =>
'Rechnungsadresse',
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.'
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')
87 'key' =>
'payment_terms',
88 'label' =>
'Zahlungsbedingung',
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.',
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')
106 'key' =>
'invoice_number',
107 'label' =>
'Rechnungsnummer',
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.'
122 'label' =>
'Rechnung erzeugen'
127 private function workflowModule() {
128 return dbx()->get_include_obj(
'dbxWorkflowModule',
'dbxWorkflow');
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;
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' =>
'',
168 private function runtime_form(
int $iid,
string $kind,
string $template, array $replacements = array()) {
169 $form =
dbx()->get_system_obj(
'dbxForm');
171 $form->_action = $this->instance_action_url(
172 '?dbx_modul=dbxWorkflow&dbx_run1=run&iid=' . max(0, $iid),
175 $form->_msg_info =
'';
176 foreach ($replacements as $key =>
$value) {
183 private function instance_action_scope(
int $iid):
string {
184 return 'dbxWorkflow.instance.' . max(0, $iid);
188 private function action_url(
string $url,
string $scope):
string {
189 $separator = strpos(
$url,
'?') ===
false ?
'?' :
'&';
193 private function instance_action_url(
string $url,
int $iid):
string {
194 return $this->action_url(
$url, $this->instance_action_scope($iid));
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);
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();
214 $ids = array_slice(array_values(array_unique(array_filter($ids))), -100);
215 dbx()->set_session_var(
'instance_ids', $ids,
'access',
'dbxWorkflow');
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);
225 private function instance_write_access():
int {
226 return (
int)
dbx()->user() > 0 ? 1 : 0;
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.',
237 private function enrich_definition(array
$definition, array $values = array()) {
238 return $this->workflowModule()->enrichDefinition(
$definition, $values);
241 private function ticket_demo_definition() {
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',
250 'key' =>
'contact_request',
251 'label' =>
'Kontaktanfrage',
254 'actions' => array(
'select'),
255 'preferred' =>
'select',
256 'hint' =>
'Offene Kontaktanfrage aus dbxContact auswaehlen.',
264 'actions' => array(
'select'),
265 'preferred' =>
'select',
266 'hint' =>
'Bearbeitungsstatus in dbxContact (offen, beantwortet, geschlossen).',
270 'key' =>
'internal_note',
271 'label' =>
'Interne Notiz',
274 'actions' => array(
'form'),
275 'hint' =>
'Optional: interne Bearbeitungsnotiz (nur im Workflow gespeichert).'
278 'key' =>
'customer_reply',
279 'label' =>
'Rueckmeldung',
282 'actions' => array(
'form'),
283 'hint' =>
'Antwort an den Anfragenden. Wird in dbxContact als reply_text gespeichert.'
286 'key' =>
'send_mail',
287 'label' =>
'Antwort per E-Mail senden',
290 'actions' => array(
'select'),
291 'preferred' =>
'select',
292 'hint' =>
'Versand nur wenn in dbxContact mail_on_reply aktiviert ist.',
294 array(
'value' =>
'1',
'label' =>
'Ja, E-Mail senden'),
295 array(
'value' =>
'0',
'label' =>
'Nein, nur speichern'),
300 'label' =>
'Kontaktanfrage abschliessen'
305 private function shop_article_publish_definition() {
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.',
313 'key' =>
'product_create',
314 'label' =>
'Artikel anlegen',
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(
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',
335 'label' =>
'Gespeicherten Artikel auswaehlen',
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',
343 'dd' =>
'dbxShop|shopProduct',
344 'where' => array(
'trash' => 0,
'active' => 1),
345 'fields' => array(
'id',
'sku',
'title'),
347 'label' =>
'{sku} - {title}',
348 'order_field' =>
'id',
349 'order_dir' =>
'DESC',
354 'key' =>
'product_group',
355 'label' =>
'Artikelgruppe zuordnen',
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(
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',
374 'label' =>
'Artikelgruppen',
375 'icon' =>
'bi-diagram-3',
376 'url' =>
'?dbx_modul=dbxShop_admin&dbx_run1=groups',
377 'title' =>
'Artikelgruppen bearbeiten',
385 'label' =>
'Bilder zuordnen',
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(
396 'label' =>
'Artikelbilder bearbeiten',
397 'icon' =>
'bi-images',
398 'url' =>
'?dbx_modul=dbxShop_admin&dbx_run1=product_edit&id={product}',
399 'title' =>
'Artikelbilder bearbeiten',
406 'key' =>
'sales_data',
407 'label' =>
'Preis, Versand und Sichtbarkeit pruefen',
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(
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',
428 'key' =>
'final_check',
429 'label' =>
'Freigabe',
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',
438 array(
'value' =>
'draft',
'label' =>
'Als Entwurf vorbereiten'),
439 array(
'value' =>
'shop',
'label' =>
'Im Shop veroeffentlichen')
444 'label' =>
'Artikel im Shop freigeben'
449 private function shop_ebay_publish_definition() {
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.',
458 'label' =>
'Artikel auswaehlen',
460 'automation' =>
'manual',
463 'actions' => array(
'select'),
464 'preferred' =>
'select',
465 'hint' =>
'Waehle den bestehenden Shop-Artikel, der bei eBay veroeffentlicht werden soll.',
466 'event' =>
'Artikel ausgewaehlt',
468 'dd' =>
'dbxShop|shopProduct',
469 'where' => array(
'trash' => 0,
'active' => 1),
470 'fields' => array(
'id',
'sku',
'title'),
472 'label' =>
'{sku} - {title}',
473 'order_field' =>
'sorter',
474 'order_dir' =>
'ASC',
479 'key' =>
'readiness_check',
480 'label' =>
'eBay-Bereitschaft automatisch prüfen',
482 'automation' =>
'observe',
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',
491 array(
'value' =>
'ready',
'label' =>
'Grunddaten vollständig'),
492 array(
'value' =>
'needs_work',
'label' =>
'Daten oder Zuordnung ergänzen')
496 'key' =>
'ebay_data',
497 'label' =>
'eBay-Daten bearbeiten und bestaetigen',
499 'automation' =>
'manual',
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(
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',
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',
528 'key' =>
'export_run',
529 'label' =>
'Export durchfuehren',
531 'automation' =>
'manual',
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(
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',
552 'key' =>
'status_check',
553 'label' =>
'Statusmeldungen pruefen',
554 'kind' =>
'decision',
555 'automation' =>
'observe',
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',
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')
570 'key' =>
'ebay_view',
571 'label' =>
'eBay-Angebot ansehen',
573 'automation' =>
'manual',
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(
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',
596 'label' =>
'eBay-Veroeffentlichung abschliessen'
602 return json_encode($this->default_definition(), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
606 return json_encode($this->ticket_demo_definition(), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
609 private function normalize_key(
$value) {
615 private function normalize_validation_rule(
$rule,
$mode =
'single') {
617 $allowed = array(
'not_empty',
'exactly_one',
'at_least_one',
'positive_integer',
'confirmed');
619 $rule = ((string)
$mode ===
'multiple') ?
'at_least_one' :
'exactly_one';
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?';
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.';
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';
644 'form' =>
'Eingabeformular öffnen',
645 'select' =>
'Auswahlformular öffnen',
646 'create' =>
'Erfassungsformular öffnen',
647 'module' =>
'Modulformular öffnen',
649 $label = trim((
string)($resolver[
'label'] ??
''));
650 if ($label ===
'') $label = $labels[$type];
651 return array_merge($resolver, array(
'type' => $type,
'label' => $label));
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;
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(
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,
682 private function parse_need_line($line) {
683 $line = trim((
string)$line);
684 if ($line ===
'' || strpos($line,
'=') ===
false)
return array();
686 list($name, $ruleText) = explode(
'=', $line, 2);
689 'key' => $this->normalize_key($name),
693 'actions' => array(),
699 foreach (explode(
'|', $ruleText) as
$token) {
701 if (
$token ===
'')
continue;
704 $need[
'mode'] = (
$token ===
'single') ?
'single' :
'multiple';
709 $need[
'required'] =
true;
713 if (
$token ===
'optional') {
714 $need[
'required'] =
false;
718 if (strpos(
$token,
'label=') === 0) {
723 if (strpos(
$token,
'hint=') === 0) {
728 if (strpos(
$token,
'options=') === 0) {
729 $items = array_map(
'trim', explode(
',', substr(
$token, 8)));
730 $need[
'options'] = array_values(array_filter(
$items,
'strlen'));
735 if (substr(
$token, -1) ===
'!') {
740 if (in_array(
$token, array(
'select',
'create',
'form',
'module'),
true)) {
746 if (!
$need[
'actions'])
$need[
'actions'] = array(
'form');
757 $json = $this->read_json($text,
null);
758 if (is_array(
$json)) {
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));
767 $need = $this->parse_need_line($line);
783 if (!is_array(
$need))
continue;
784 $key = $this->normalize_key(
$need[
'key'] ?? (
$need[
'label'] ??
''));
785 if ($key ===
'')
continue;
790 $mode = ((
$need[
'mode'] ??
'single') ===
'multiple') ?
'multiple' :
'single';
791 $normalizedNeed = array_merge(
$need, array(
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',
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),
812 $needs[] = $normalizedNeed;
826 $workflowKey = $workflowKey ?: (string)
dbx()->get_config(
'dbxWorkflow',
'default_workflow');
827 if (!$workflowKey) $workflowKey =
'invoice_demo';
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);
835 $definition[
'workflow_key'] = $row[
'workflow_key'] ?? $workflowKey;
849 private function definition_record(array
$definition): array {
855 'definition_json' => json_encode(
$definition, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
860 private function ensure_definition(array
$definition): int {
862 $workflowKey = (string)(
$definition[
'workflow_key'] ??
'');
863 if ($workflowKey ===
'') {
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'];
872 return (
int)
$db->insert($this->ddDefinition, $this->definition_record(
$definition), 0, 1, 1, 1);
876 return $this->ensure_definition($this->default_definition());
880 return $this->ensure_definition($this->ticket_demo_definition());
888 $this->ensure_definition($this->shop_article_publish_definition());
889 return $this->ensure_definition($this->shop_ebay_publish_definition());
893 dbx()->get_include_obj(
'dbxWorkflowBindRegistry',
'dbxWorkflow')->seedDefaultBinds();
894 $this->ensure_definition($this->default_definition());
895 $this->ensure_definition($this->ticket_demo_definition());
899 public function start($workflowKey) {
901 if (!
$definition)
return $this->unavailable_definition_message((
string)$workflowKey);
902 $startToken = (string)
dbx()->get_modul_var(
'dbx_token',
'',
'varchar|max=128');
906 return $this->
overview((
string)$workflowKey);
909 $uid = (int)
dbx()->user();
910 $values = $this->workflowModule()->prefillStart(
912 (
int)
dbx()->get_modul_var(
'rid', 0,
'int')
918 'status' =>
'running',
919 'current_need' =>
'',
922 'message' =>
'Workflow gestartet.',
923 'definition_json' => $this->write_json(
$definition),
924 'data_json' => $this->write_json($values),
929 $iid = (
$db->insert($this->ddInstance, $record, 1, 1, 1, 1) === 1) ? (int)
$db->get_insert_id() : 0;
931 return $this->tpl()->get_tpl(
'dbx|alert-warning', array(
'msg' =>
'Workflow konnte nicht gestartet werden.'));
933 $this->register_guest_instance($iid);
934 return $this->
render($iid,
true);
937 private function load_instance($iid) {
939 if ($iid <= 0)
return array();
944 $rows = $this->db()->select($this->ddInstance, array(
'id' => $iid),
'*',
'id',
'DESC',
'', 1, 0, 1);
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);
953 private function values_from_instance($instance) {
954 return $this->read_json($instance[
'data_json'] ??
'', array());
957 private function validate_need_value(
$need,
$value): bool {
960 $rule = $this->normalize_validation_rule(
$need[
'validation'] ??
'',
$need[
'mode'] ??
'single');
962 ? array_values(array_filter(
$value, fn($item) => trim((
string)$item) !==
''))
963 : (trim((string)
$value) !==
'' ? array(
$value) : array());
966 foreach ((array)(
$need[
'options'] ?? array()) as $option) {
967 $optionValue = is_array($option) ? (string)($option[
'value'] ??
'') : (string)$option;
968 if ($optionValue !==
'')
$allowed[] = $optionValue;
970 $onlyAllowedValues =
function(array $selected) use (
$allowed):
bool {
972 foreach ($selected as $item) {
973 if (!in_array((
string)$item,
$allowed,
true))
return false;
978 $enforceAllowed = in_array(
'select',
$actions,
true)
979 && !array_intersect(
$actions, array(
'create',
'form',
'module'));
981 if (
$rule ===
'positive_integer') {
982 return count(
$items) === 1 && filter_var(
$items[0], FILTER_VALIDATE_INT) !==
false && (int)
$items[0] > 0;
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);
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;
993 private function is_done($values,
$need) {
995 if (!array_key_exists($key, $values))
return false;
996 return $this->validate_need_value(
$need, $values[$key]);
999 private function value_matches($actual,
$expected) {
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;
1009 return trim((
string)$actual) ===
$expected;
1012 private function dependency_has_value(
string $key, array $values): bool {
1013 if ($key ===
'' || !array_key_exists($key, $values)) return false;
1015 if (is_array(
$value) && !empty(
$value[
'skipped']))
return false;
1017 return count(array_filter(
$value, fn($item) => trim((
string)$item) !==
'')) > 0;
1019 return trim((
string)
$value) !==
'';
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'] ??
'');
1029 private function next_need(
$definition, $values) {
1031 if (!$this->need_is_applicable(
$need, $values))
continue;
1032 if (!$this->is_done($values,
$need))
return $need;
1037 private function completed_count(
$definition, $values) {
1040 if (!$this->need_is_applicable(
$need, $values))
continue;
1046 private function applicable_count(
$definition, $values) {
1049 if ($this->need_is_applicable(
$need, $values))
$count++;
1059 private function progress_total_count(
$definition, $values) {
1062 $dependsOn = $this->normalize_key(
$need[
'depends_on'] ??
'');
1063 if ($dependsOn ===
'') {
1067 if (!$this->dependency_has_value($dependsOn, $values)) {
1071 if ($this->value_matches($values[$dependsOn],
$need[
'depends_value'] ??
''))
$count++;
1076 private function apply_command($instance, $cmd) {
1077 $cmd = strtolower(trim((
string)$cmd));
1078 if (!$cmd)
return $instance;
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.';
1086 $values = $this->values_from_instance($instance);
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') {
1097 $update = array(
'status' =>
'running',
'message' =>
'Workflow neu gestartet.',
'data_json' => $this->write_json($values),
'percent' => 0,
'step_percent' => 0);
1101 $ok = $this->db()->update(
1104 array(
'id' => $iid),
1105 $this->instance_write_access(),
1111 $instance = array_merge($instance, $update);
1113 $instance[
'message'] =
'Workflow-Aktion konnte nicht gespeichert werden.';
1120 private function post_value(
$need) {
1123 if (isset(
$_POST[
'workflow_skip']) && !
$need[
'required']) {
1124 return array(
'skipped' => 1,
'label' =>
'Uebersprungen');
1130 if (isset(
$_POST[
'form_value'])) {
1131 return trim((
string)
$_POST[
'form_value']);
1134 if (isset(
$_POST[
'selected_value'])) {
1137 return array_values(array_filter(array_map(
'trim',
$value),
'strlen'));
1139 return trim((
string)
$value);
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;
1149 $iid = (int)($instance[
'id'] ?? 0);
1150 $isFinish = !empty(
$_POST[
'workflow_finish']);
1151 $form = $this->runtime_form(
1153 $isFinish ?
'review' :
'step',
1154 $isFinish ?
'workflow-review' :
'workflow-step-choice'
1156 if (!
$form->submit()) {
1157 $instance[
'message'] =
'Ungueltiger oder abgelaufener Formular-Token.';
1160 if (!$this->has_instance_action_token($iid)) {
1161 $instance[
'message'] =
'Ungueltiger oder abgelaufener Aktions-Token.';
1165 $values = $this->values_from_instance($instance);
1167 if (!empty(
$_POST[
'workflow_finish'])) {
1170 $instance[
'message'] =
'Workflow unvollständig. Es fehlen: ' . implode(
', ',
$missing) .
'.';
1179 if (
$db->begin($this->ddInstance) !== 1) {
1180 $instance[
'message'] =
'Workflow-Abschluss konnte nicht atomar gestartet werden.';
1183 $claim =
$db->update(
1185 array(
'status' =>
'finishing',
'message' =>
'Workflow wird abgeschlossen.'),
1186 array(
'id' => $iid,
'status' =>
'running'),
1187 $this->instance_write_access(),
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.'));
1197 $instance[
'status'] =
'finishing';
1198 $instance[
'message'] =
'Workflow wird abgeschlossen.';
1200 $finishMessage =
'Workflow abgeschlossen.';
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.',
1209 $db->update($this->ddInstance, $errorUpdate, array(
'id' => $iid), $this->instance_write_access(), 1, 1, 1);
1210 return array_merge($instance, $errorUpdate);
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.',
1218 $db->update($this->ddInstance, $failedUpdate, array(
'id' => $iid), $this->instance_write_access(), 1, 1, 1);
1219 return array_merge($instance, $failedUpdate);
1221 $finishMessage = (string)($moduleResult[
'message'] ?? $finishMessage);
1225 'status' =>
'finished',
1227 'step_percent' => 100,
1228 'current_need' =>
'',
1229 'message' => $finishMessage,
1230 'data_json' => $this->write_json($values)
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.';
1236 return array_merge($instance, $update);
1239 $needKey = $this->normalize_key(
$_POST[
'need_key'] ??
'');
1241 foreach ((array)
$definition[
'needs'] as $candidate) {
1242 if ($candidate[
'key'] === $needKey) {
1248 if (!
$need)
return $instance;
1252 $instance[
'message'] = (string)(
$need[
'missing_message'] ?? (
'Bitte einen gültigen Wert für „' .
$need[
'label'] .
'“ eintragen.'));
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.',
1269 $total = max(1, $this->progress_total_count(
$definition, $values));
1270 $percent = (int)floor(($stepNo / $total) * 100);
1272 'data_json' => $this->write_json($values),
1273 'percent' => $percent,
1274 'step_percent' => 100,
1275 'message' =>
$need[
'label'] .
' wurde uebernommen.'
1279 if (
$db->begin($this->ddInstance) !== 1) {
1280 $instance[
'message'] =
'Workflow-Schritt konnte nicht atomar gespeichert werden.';
1286 $stepAccess = (int)
dbx()->user() > 0 ? 1 : 0;
1287 $previous =
$db->select(
1289 array(
'instance_id' => $iid,
'need_key' =>
$need[
'key'],
'status' =>
'finished'),
1298 $sameStep = is_array($previous)
1299 && isset($previous[0])
1300 && (string)($previous[0][
'value_json'] ??
'') === (string)$stepRecord[
'value_json'];
1302 if (!$sameStep &&
$db->insert($this->ddStep, $stepRecord, 1, 1, 1, 1) !== 1) {
1303 throw new \RuntimeException(
'step_insert_failed');
1308 array(
'id' => $iid,
'status' =>
'running'),
1309 $this->instance_write_access(),
1314 throw new \RuntimeException(
'instance_update_failed');
1316 if (
$db->commit($this->ddInstance) !== 1) {
1317 throw new \RuntimeException(
'commit_failed');
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.';
1326 return array_merge($instance, $update);
1329 private function apply_automations(array $instance, array
$definition): array {
1330 if (($instance[
'status'] ??
'') !==
'running') return $instance;
1332 $values = $this->values_from_instance($instance);
1334 $stepRecords = array();
1335 $limit = max(1, count((array)(
$definition[
'needs'] ?? array())));
1337 for ($i = 0; $i < $limit; $i++) {
1339 if (!
$need || (
string)(
$need[
'automation'] ??
'manual') !==
'observe')
break;
1342 if (!is_array(
$result) || !array_key_exists(
'value',
$result))
break;
1347 $stepNo = $this->completed_count(
$definition, $values);
1348 $message = trim((
string)(
$result[
'message'] ?? (
$need[
'label'] .
' automatisch geprüft.')));
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,
1364 $completed = $this->completed_count(
$definition, $values);
1365 $total = max(1, $this->progress_total_count(
$definition, $values));
1367 'data_json' => $this->write_json($values),
1368 'percent' => (
int)floor(($completed / $total) * 100),
1369 'step_percent' => 100,
1374 if (
$db->begin($this->ddInstance) !== 1) {
1375 $instance[
'message'] =
'Automatische Workflow-Schritte konnten nicht atomar gespeichert werden.';
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');
1387 array(
'id' => (
int)$instance[
'id'],
'status' =>
'running'),
1388 $this->instance_write_access(),
1393 throw new \RuntimeException(
'automation_instance_update_failed');
1395 if (
$db->commit($this->ddInstance) !== 1) {
1396 throw new \RuntimeException(
'automation_commit_failed');
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.';
1405 return array_merge($instance, $update);
1408 private function render_options(
$need, $currentValue =
null) {
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);
1416 $value = $label = (string)$opt;
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
1432 return $this->workflowModule()->renderStepContext(
$definition,
$need, $values);
1436 if (!in_array(
'form', (array)(
$need[
'actions'] ?? array()),
true)) {
1440 if (array_key_exists(
$need[
'key'], $values)) {
1441 return $this->h($values[
$need[
'key']]);
1444 return $this->workflowModule()->renderFormValue(
$definition,
$need, $values);
1447 private function workflow_value_for_token($key, array $values) {
1448 $key = $this->normalize_key($key);
1449 if ($key ===
'' || !array_key_exists($key, $values)) {
1457 return implode(
',', array_map(
'strval',
$value));
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);
1469 private function render_module_links(
$need, array $values) {
1471 foreach ((array)(
$need[
'module_links'] ?? array()) as $link) {
1472 if (!is_array($link)) {
1475 $url = trim((
string)($link[
'url'] ??
''));
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>';
1497 if (!in_array(
'module', (array)(
$need[
'actions'] ?? array()),
true)) {
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>';
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.'))
1514 private function render_step($iid,
$definition,
$need, $values, $targetId) {
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'];
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"' :
''
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')
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']),
1552 if (in_array(
'module',
$actions,
true)) {
1556 $skip = !
$need[
'required'] ? $this->tpl()->get_tpl(
'dbxWorkflow|workflow-skip-button') :
'';
1560 'action' => $this->h($this->instance_action_url(
1561 '?dbx_modul=dbxWorkflow&dbx_run1=run&iid=' . (
int)$iid,
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
1581 return $this->runtime_form(
1584 'workflow-step-choice',
1589 private function render_requirements_check($iid,
$definition, $values,
string $status): string {
1595 $key = (string)(
$need[
'key'] ??
'');
1596 if ($key ===
'')
continue;
1601 if (
$required && $done) $requiredDone++;
1604 $stateLabel =
'Wartet auf Voraussetzung';
1608 $stateLabel =
'Vollständig';
1609 $icon =
'bi-check2-circle';
1612 $stateLabel =
'Unvollständig';
1613 $icon =
'bi-exclamation-circle';
1616 $stateLabel =
'Optional';
1617 $icon =
'bi-circle';
1620 $resolver = $this->requirement_resolver(
$need);
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>';
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>'
1636 $complete = $requiredTotal > 0 && $requiredDone >= $requiredTotal;
1637 $summary = $complete
1638 ?
'Alle ' . $requiredTotal .
' Pflichtangaben sind vollständig.'
1639 : $requiredDone .
' von ' . $requiredTotal .
' Pflichtangaben sind vollständig.';
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>'
1649 if ($formatted !==
null) {
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));
1658 private function render_review_rows(
$definition, $values) {
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']),
1670 private function missing_required_labels(
$definition, $values): array {
1673 if (!$this->need_is_applicable(
$need, $values)) {
1676 if (empty(
$need[
'required'])) {
1679 if (!$this->is_done($values,
$need)) {
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));
1690 return $this->workflowModule()->renderFinalStatus(
$definition, $values, $status, $completed, $total,
$missing);
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,
1699 'target_id' => $this->h($targetId),
1700 'result_label' => $this->h(
$definition[
'result']),
1702 'rows' => $this->render_review_rows(
$definition, $values),
1703 'final_status' => $this->render_final_status(
$definition, $values,
'running')
1707 private function status_label($status) {
1709 'running'=>
'Laeuft',
1710 'finishing'=>
'Wird abgeschlossen',
1711 'paused'=>
'Angehalten',
1712 'canceled'=>
'Abgebrochen',
1713 'finished'=>
'Fertig',
1716 return $map[$status] ?? $status;
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';
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';
1734 public function render($iid,
bool $allowAutomations =
false) {
1736 $instance = $this->load_instance($iid);
1738 return $this->tpl()->get_tpl(
'dbx|alert-warning', array(
'msg' =>
'Workflow-Instanz nicht gefunden.'));
1741 $workflowKey = (string)($instance[
'workflow_key'] ??
'');
1745 $snapshot = $this->read_json($instance[
'definition_json'] ??
'', array());
1754 if (!
$baseDefinition)
return $this->unavailable_definition_message($workflowKey);
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);
1760 if ($allowAutomations || $this->has_instance_action_token($iid)) {
1761 $instance = $this->apply_automations($instance,
$definition);
1763 $values = $this->values_from_instance($instance);
1765 $status = (string)($instance[
'status'] ??
'running');
1766 $targetId =
'dbx_workflow_' . $iid;
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),
1775 '?dbx_modul=dbxWorkflow&dbx_run1=start&workflow=' . rawurlencode(
$definition[
'workflow_key']),
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)));
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));
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';
1797 $stepContent = $this->render_step($iid,
$definition,
$need, $values, $targetId);
1798 $instance[
'current_need'] =
$need[
'key'];
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);
1811 $stateUpdate = array(
1812 'current_need' => $instance[
'current_need'] ??
'',
1813 'percent' => $percent,
1814 'step_percent' => ($status ===
'finished') ? 100 : 0,
1815 'message' => $message
1817 if (isset($instance[
'_transient_message'])) {
1818 unset($stateUpdate[
'message']);
1821 if ((
string)($instance[
$field] ??
'') === (
string)
$value) {
1822 unset($stateUpdate[
$field]);
1826 $this->db()->update(
1829 array(
'id' => $iid),
1830 $this->instance_write_access(),
1835 $instance = array_merge($instance, $stateUpdate);
1838 $nextUrl =
'?dbx_modul=dbxWorkflow&dbx_run1=run&iid=' . $iid;
1839 $instanceActionUrl = $this->instance_action_url($nextUrl, $iid);
1840 $restartUrl = $instanceActionUrl .
'&proc_cmd=restart';
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 .
'"';
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'])),
1864 'target_id' => $this->h($targetId),
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' =>
'',
1899 private function requested_need(
$definition, $values) {
1900 $requested = $this->normalize_key(
dbx()->get_modul_var(
'need',
'',
'parameter'));
1909 if (!$this->need_is_applicable(
$need, $values)) {
1918 private function render_steps_nav($iid,
$definition, $values, $activeKey, $status) {
1919 $html =
'<div class="dbx-workflow-step-nav" aria-label="Workflow Schritte">';
1925 $key = (string)(
$need[
'key'] ??
'');
1934 $stateLabel =
'Gesperrt';
1936 if ($key === $activeKey) {
1938 $icon =
'bi-pencil-square';
1939 $stateLabel =
'Aktuell';
1942 $icon =
'bi-check2';
1943 $stateLabel =
'Erledigt';
1946 $icon =
'bi-circle';
1947 $stateLabel =
'Offen';
1951 $dependsOn = $this->normalize_key(
$need[
'depends_on'] ??
'');
1952 $title = (string)(
$need[
'label'] ?? $key);
1954 $title .=
' - erst nach "' . $dependsOn .
'" moeglich';
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>'
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>';
1967 $html .=
'<span class="dbx-workflow-step-nav-item is-' .
$state .
'" title="' . $this->h($title) .
'">' . $inner .
'</span>';
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>';
1980 $html .=
'<span class="dbx-workflow-step-nav-item is-' . $reviewState .
'" title="Workflow pruefen und abschliessen">' . $reviewInner .
'</span>';
1983 return $html .
'</div>';
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'),
1993 'description' => $this->h(
$definition[
'description']),
1994 'result_label' => $this->h(
$definition[
'result']),
1996 '?dbx_modul=dbxWorkflow&dbx_run1=start&workflow=' . rawurlencode(
$definition[
'workflow_key']),
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' =>
'',