diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 3fcb16b5..3d3f315b 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -228,12 +228,36 @@ }, "editor": { "namePlaceholder": "Workflow-Name", - "config": "Konfiguration (JSON)", - "applyConfig": "Konfiguration übernehmen", - "configApplied": "Konfiguration übernommen (zum Speichern nicht vergessen)", - "badJson": "Konfiguration ist kein gültiges JSON", + "when": "Wenn", "saved": "Workflow gespeichert", - "saveFailed": "Speichern fehlgeschlagen" + "saveFailed": "Speichern fehlgeschlagen", + "badJson": "Konfiguration ist kein gültiges JSON", + "showAdvanced": "Erweitert (JSON)", + "hideAdvanced": "Erweitert ausblenden (JSON)", + "triggerHint": "Der Auslöser wird oben in der Leiste gesetzt (Wenn …).", + "actionLabel": "Aktion", + "recipient": "Empfänger", + "recipientCustomer": "Kunde (berücksichtigt Geschäftszeiten)", + "recipientAdmin": "Admin (sofort gesendet)", + "emailTemplate": "E-Mail-Vorlagenschlüssel", + "webhookUrl": "Webhook-URL", + "condition": "Bedingung", + "exprField": "Feld", + "exprOp": "Operator", + "exprValue": "Wert", + "conditionHint": "Führt bei „wahr“ zur „yes“-Kante, sonst zur „no“-Kante.", + "waitType": "Warten", + "waitUntil": "Bis zu einem Datum", + "waitDelay": "Feste Verzögerung", + "waitAnchor": "Warten bis", + "days": "Tage", + "hours": "Stunden", + "minutes": "Min", + "maxIterations": "Höchstens wiederholen (mal)", + "gatePrompt": "Frage an den Admin", + "gatePromptPh": "z. B. Keine Zahlung erhalten – Mahnung senden?", + "gateTimeout": "Automatisch ablaufen nach (Tagen, optional)", + "gateHint": "Sendet dem Admin einen Bestätigen/Ablehnen-Link; führt zur „confirm“- oder „deny“-Kante." } }, "eventTypes": { diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 4eb6f332..006884e8 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -228,12 +228,36 @@ }, "editor": { "namePlaceholder": "Workflow name", - "config": "Config (JSON)", - "applyConfig": "Apply config", - "configApplied": "Config applied (remember to Save)", - "badJson": "Config is not valid JSON", + "when": "When", "saved": "Workflow saved", - "saveFailed": "Could not save" + "saveFailed": "Could not save", + "badJson": "Config is not valid JSON", + "showAdvanced": "Advanced (JSON)", + "hideAdvanced": "Hide advanced (JSON)", + "triggerHint": "The trigger is set in the toolbar above (When …).", + "actionLabel": "Action", + "recipient": "Recipient", + "recipientCustomer": "Customer (respects business hours)", + "recipientAdmin": "Admin (sent immediately)", + "emailTemplate": "Email template key", + "webhookUrl": "Webhook URL", + "condition": "Condition", + "exprField": "Field", + "exprOp": "Operator", + "exprValue": "Value", + "conditionHint": "Routes to the “yes” edge when true, “no” when false.", + "waitType": "Wait", + "waitUntil": "Until a date", + "waitDelay": "A fixed delay", + "waitAnchor": "Wait until", + "days": "Days", + "hours": "Hours", + "minutes": "Min", + "maxIterations": "Repeat at most (times)", + "gatePrompt": "Question for the admin", + "gatePromptPh": "e.g. No payment received — send a reminder?", + "gateTimeout": "Auto-expire after (days, optional)", + "gateHint": "Emails the admin a confirm/deny link; routes to the “confirm” or “deny” edge." } }, "archives": { diff --git a/frontend/src/pages/admin/workflows/NodeConfigPanel.tsx b/frontend/src/pages/admin/workflows/NodeConfigPanel.tsx new file mode 100644 index 00000000..ea0406af --- /dev/null +++ b/frontend/src/pages/admin/workflows/NodeConfigPanel.tsx @@ -0,0 +1,201 @@ +/** + * Structured config editor for a workflow node — dropdowns + typed fields per + * node type, so admins don't hand-edit JSON. An "Advanced (JSON)" expander is + * kept for power users / config the form doesn't cover. Changes are applied + * live to the node (the global Save persists them). + */ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +type Cfg = Record; + +interface Props { + nodeType: string; + config: Cfg; + onChange: (next: Cfg) => void; +} + +const field = 'w-full px-2 py-1.5 rounded border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-900 text-neutral-900 dark:text-neutral-100 text-sm'; +const lbl = 'block text-xs text-neutral-500 dark:text-neutral-400 mb-1'; + +const ACTIONS = [ + ['queue_payment_check', 'Send payment-check email (dunning gate)'], + ['send_email', 'Send email'], + ['reserve_date', 'Reserve the event date'], + ['prepare_quote', 'Prepare a quote (draft)'], + ['prepare_contract', 'Prepare a contract (draft)'], + ['prepare_invoice', 'Prepare an invoice (draft)'], + ['prepare_event', 'Create an event (draft)'], + ['prepare_gallery', 'Create a gallery (draft)'], + ['send_document', 'Send the document'], + ['webhook', 'Call a webhook'], + ['noop', 'Do nothing'], +]; +const CONDITIONS = [ + ['invoice_paid', 'Invoice is paid'], + ['expr', 'Compare a field'], + ['always', 'Always → yes'], + ['never', 'Never → no'], +]; +const WAIT_ANCHORS = [ + ['dueDate', 'the invoice due date'], + ['issueDate', 'the invoice date'], + ['eventDate', 'the event date'], +]; +const OPS = ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'truthy', 'falsy']; + +const Row: React.FC<{ label: string; children: React.ReactNode }> = ({ label, children }) => ( +
{children}
+); + +export const NodeConfigPanel: React.FC = ({ nodeType, config, onChange }) => { + const { t } = useTranslation(); + const [showJson, setShowJson] = useState(false); + const [jsonText, setJsonText] = useState(JSON.stringify(config || {}, null, 2)); + const [jsonErr, setJsonErr] = useState(null); + + const set = (patch: Cfg) => onChange({ ...config, ...patch }); + const num = (v: string) => (v === '' ? undefined : Number(v)); + + const applyJson = (text: string) => { + setJsonText(text); + try { onChange(JSON.parse(text || '{}')); setJsonErr(null); } + catch (e) { setJsonErr(t('workflows.editor.badJson', 'Config is not valid JSON') as string); } + }; + + const waitMode = config.untilVar ? 'until' : 'delay'; + + return ( +
+ {nodeType === 'trigger' && ( +

+ {t('workflows.editor.triggerHint', 'The trigger is set in the toolbar above (When …).')} +

+ )} + + {(nodeType === 'action' || nodeType === 'webhook') && ( + + + + )} + + {nodeType === 'action' && config.action === 'send_email' && ( + <> + + + + + set({ emailType: e.target.value })} placeholder="invoice_reminder" /> + + + )} + + {(nodeType === 'action' || nodeType === 'webhook') && (config.action === 'webhook' || nodeType === 'webhook') && ( + + set({ url: e.target.value })} placeholder="https://…" /> + + )} + + {(nodeType === 'condition' || nodeType === 'branch') && ( + <> + + + + {config.condition === 'expr' && ( + <> + + set({ field: e.target.value })} /> + + + + + {!['truthy', 'falsy'].includes(config.op) && ( + + set({ value: e.target.value })} /> + + )} + + )} +

+ {t('workflows.editor.conditionHint', 'Routes to the “yes” edge when true, “no” when false.')} +

+ + )} + + {nodeType === 'wait' && ( + <> + + + + {waitMode === 'until' ? ( + + + + ) : ( +
+ + set({ delayDays: num(e.target.value) })} /> + + + set({ delayHours: num(e.target.value) })} /> + + + set({ delayMinutes: num(e.target.value) })} /> + +
+ )} + + )} + + {nodeType === 'loop' && ( + + set({ maxIterations: num(e.target.value) })} /> + + )} + + {nodeType === 'gate' && ( + <> + +