@hartl-services/medusa-base 2.8.0 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginSettingsForm.js +4 -5
  2. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/plugin-view.d.ts +0 -5
  3. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/plugin-view.js +0 -5
  4. package/.medusa/server/src/admin/admin-components/i18n/texts.d.ts +1 -2
  5. package/.medusa/server/src/admin/admin-components/i18n/texts.js +1 -2
  6. package/.medusa/server/src/admin/index.js +106 -69
  7. package/.medusa/server/src/admin/index.mjs +107 -70
  8. package/.medusa/server/src/api/admin/email/templates/[kind]/send-test/route.js +7 -1
  9. package/.medusa/server/src/email/application/warn-credential-expiry.js +77 -0
  10. package/.medusa/server/src/email/config/index.js +8 -2
  11. package/.medusa/server/src/email/config/notification-module.js +28 -2
  12. package/.medusa/server/src/email/domain/business-keys.js +5 -2
  13. package/.medusa/server/src/email/domain/failure-classification.js +9 -1
  14. package/.medusa/server/src/email/plugin-features.js +123 -1
  15. package/.medusa/server/src/email/rendering/default-templates.js +16 -1
  16. package/.medusa/server/src/email/rendering/render-email.js +2 -1
  17. package/.medusa/server/src/email/types/email-kinds.js +2 -1
  18. package/.medusa/server/src/email/types/template-data.js +8 -1
  19. package/.medusa/server/src/jobs/email-credential-expiry.js +27 -0
  20. package/.medusa/server/src/modules/email/domain/message-state.js +3 -1
  21. package/.medusa/server/src/modules/email/migrations/Migration20261011120000.js +20 -0
  22. package/.medusa/server/src/providers/smtp/delivery-settings.js +75 -0
  23. package/.medusa/server/src/providers/smtp/graph-transport.js +150 -0
  24. package/.medusa/server/src/providers/smtp/service.js +105 -37
  25. package/.medusa/server/src/providers/smtp/types.js +7 -1
  26. package/.medusa/server/src/shop-config/build-food-shop-config.js +3 -5
  27. package/README.md +100 -65
  28. package/dist/email/config/index.d.ts +6 -1
  29. package/dist/email/config/index.js +7 -1
  30. package/dist/email/config/notification-module.d.ts +25 -0
  31. package/dist/email/config/notification-module.js +27 -1
  32. package/dist/email/types/email-kinds.d.ts +2 -1
  33. package/dist/email/types/email-kinds.js +1 -0
  34. package/dist/email/types/template-data.d.ts +10 -0
  35. package/dist/email/types/template-data.js +7 -0
  36. package/dist/shop-config/build-food-shop-config.d.ts +1 -1
  37. package/dist/shop-config/build-food-shop-config.js +2 -4
  38. package/package.json +2 -2
@@ -4,7 +4,7 @@ import { texts } from "../../../i18n/texts";
4
4
  import { AdminSection } from "../../../shared/ui/AdminSection";
5
5
  import { InfoTooltip } from "../../../shared/ui/InfoTooltip";
6
6
  import { SwitchRow } from "../../../shared/ui/SwitchRow";
7
- import { invalidatedByLabels, invalidatesLabels, isGroupDimmed, settingInfoText, showsBoundsLine, showsSourceHint, } from "../lib/plugin-view";
7
+ import { invalidatedByLabels, isGroupDimmed, settingInfoText, showsBoundsLine, showsSourceHint, } from "../lib/plugin-view";
8
8
  import { isHexColor, normalizeColorInput } from "../lib/settings-draft";
9
9
  import { dateFromInputValue, inputValueFromDate } from "../../../shared/lib/date-input";
10
10
  import { AdminDatePicker } from "../../../shared/ui/AdminDatePicker";
@@ -36,7 +36,7 @@ const ColorSettingInput = ({ id, hintId, label, value, invalid, required, disabl
36
36
  className: `h-8 w-10 shrink-0 cursor-pointer rounded-md border border-ui-border-base bg-ui-bg-field p-1 disabled:cursor-not-allowed ${hasColor ? "" : "opacity-30"}`, value: hasColor ? value.trim() : "#000000", disabled: disabled, onChange: (event) => onChange(normalizeColorInput(event.target.value)) })] }));
37
37
  };
38
38
  /** A non-boolean setting: label (with source hint), control and hints. */
39
- const SettingField = ({ pluginKey, setting, invalidatedBy, invalidates, value, invalid, disabled, onChange, }) => {
39
+ const SettingField = ({ pluginKey, setting, invalidatedBy, value, invalid, disabled, onChange, }) => {
40
40
  const id = fieldId(pluginKey, setting.key);
41
41
  const labelId = `${id}-label`;
42
42
  const hintId = `${id}-hint`;
@@ -49,7 +49,6 @@ const SettingField = ({ pluginKey, setting, invalidatedBy, invalidates, value, i
49
49
  const hasHints = Boolean(hint) ||
50
50
  clearPending ||
51
51
  invalidatedBy.length > 0 ||
52
- invalidates.length > 0 ||
53
52
  showBounds ||
54
53
  invalid;
55
54
  const describedBy = hasHints ? hintId : undefined;
@@ -58,7 +57,7 @@ const SettingField = ({ pluginKey, setting, invalidatedBy, invalidates, value, i
58
57
  ? texts.settings.secretClearUndoLabel(setting.label)
59
58
  : texts.settings.secretClearLabel(setting.label), onClick: () => onChange(clearPending ? "" : null), children: clearPending
60
59
  ? texts.settings.secretClearUndo
61
- : texts.settings.secretClear }))] })) : setting.type === "select" ? (_jsxs(Select, { value: typeof value === "string" ? value : "", disabled: disabled, onValueChange: (next) => onChange(next), children: [_jsx(Select.Trigger, { id: id, "aria-describedby": describedBy, "aria-required": required || undefined, children: _jsx(Select.Value, { placeholder: texts.settings.selectPlaceholder }) }), _jsx(Select.Content, { children: setting.options.map((option) => (_jsx(Select.Item, { value: option.value, children: option.label }, option.value))) })] })) : setting.type === "image" ? (_jsx(ImageSettingInput, { id: id, describedBy: describedBy, label: setting.label, value: typeof value === "string" ? value : "", disabled: disabled, onChange: (url) => onChange(url) })) : setting.type === "color" ? (_jsx(ColorSettingInput, { id: id, hintId: describedBy, label: setting.label, value: typeof value === "string" ? value : "", invalid: invalid, required: required, disabled: disabled, onChange: (next) => onChange(next) })) : setting.type === "date" ? (_jsx(AdminDatePicker, { id: id, "aria-labelledby": labelId, "aria-describedby": describedBy, isDisabled: disabled, value: dateFromInputValue(typeof value === "string" ? value : ""), onChange: (date) => onChange(inputValueFromDate(date)) })) : setting.type === "number" ? (_jsx(Input, { id: id, "aria-describedby": describedBy, "aria-invalid": invalid, "aria-required": required || undefined, type: "number", step: setting.integer ? 1 : "any", min: setting.min ?? undefined, max: setting.max ?? undefined, placeholder: placeholder, value: typeof value === "string" ? value : "", disabled: disabled, onChange: (event) => onChange(event.target.value, event.target.validity.badInput) })) : (_jsx(Input, { id: id, "aria-describedby": describedBy, "aria-invalid": invalid, "aria-required": required || undefined, type: setting.type === "url" ? "url" : "text", placeholder: placeholder, value: typeof value === "string" ? value : "", disabled: disabled, onChange: (event) => onChange(event.target.value) })), hasHints && (_jsxs("div", { id: hintId, className: "flex flex-col gap-y-1", children: [hint && (_jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: hint })), clearPending && (_jsx(Hint, { variant: "info", children: texts.settings.secretClearPending })), invalidatedBy.length > 0 && (_jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: texts.settings.invalidatedBy(invalidatedBy) })), invalidates.length > 0 && (_jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: texts.settings.invalidates(invalidates.join(", ")) })), showBounds && !invalid && (_jsx(Text, { size: "small", className: "text-ui-fg-muted", children: bounds })), invalid && (_jsx(Hint, { variant: "error", children: setting.type === "color"
60
+ : texts.settings.secretClear }))] })) : setting.type === "select" ? (_jsxs(Select, { value: typeof value === "string" ? value : "", disabled: disabled, onValueChange: (next) => onChange(next), children: [_jsx(Select.Trigger, { id: id, "aria-describedby": describedBy, "aria-required": required || undefined, children: _jsx(Select.Value, { placeholder: texts.settings.selectPlaceholder }) }), _jsx(Select.Content, { children: setting.options.map((option) => (_jsx(Select.Item, { value: option.value, children: option.label }, option.value))) })] })) : setting.type === "image" ? (_jsx(ImageSettingInput, { id: id, describedBy: describedBy, label: setting.label, value: typeof value === "string" ? value : "", disabled: disabled, onChange: (url) => onChange(url) })) : setting.type === "color" ? (_jsx(ColorSettingInput, { id: id, hintId: describedBy, label: setting.label, value: typeof value === "string" ? value : "", invalid: invalid, required: required, disabled: disabled, onChange: (next) => onChange(next) })) : setting.type === "date" ? (_jsx(AdminDatePicker, { id: id, "aria-labelledby": labelId, "aria-describedby": describedBy, isDisabled: disabled, value: dateFromInputValue(typeof value === "string" ? value : ""), onChange: (date) => onChange(inputValueFromDate(date)) })) : setting.type === "number" ? (_jsx(Input, { id: id, "aria-describedby": describedBy, "aria-invalid": invalid, "aria-required": required || undefined, type: "number", step: setting.integer ? 1 : "any", min: setting.min ?? undefined, max: setting.max ?? undefined, placeholder: placeholder, value: typeof value === "string" ? value : "", disabled: disabled, onChange: (event) => onChange(event.target.value, event.target.validity.badInput) })) : (_jsx(Input, { id: id, "aria-describedby": describedBy, "aria-invalid": invalid, "aria-required": required || undefined, type: setting.type === "url" ? "url" : "text", placeholder: placeholder, value: typeof value === "string" ? value : "", disabled: disabled, onChange: (event) => onChange(event.target.value) })), hasHints && (_jsxs("div", { id: hintId, className: "flex flex-col gap-y-1", children: [hint && (_jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: hint })), clearPending && (_jsx(Hint, { variant: "info", children: texts.settings.secretClearPending })), invalidatedBy.length > 0 && (_jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: texts.settings.invalidatedBy(invalidatedBy) })), showBounds && !invalid && (_jsx(Text, { size: "small", className: "text-ui-fg-muted", children: bounds })), invalid && (_jsx(Hint, { variant: "error", children: setting.type === "color"
62
61
  ? texts.settings.invalidColor
63
62
  : (bounds ?? texts.settings.invalidNumber) }))] }))] }));
64
63
  };
@@ -73,7 +72,7 @@ export const PluginSettingsForm = ({ plugin }) => {
73
72
  setting.type === "date");
74
73
  const renderField = (setting) => (_jsx(SettingField, { pluginKey: plugin.plugin_key, setting: setting, invalidatedBy: setting.type === "secret"
75
74
  ? invalidatedByLabels(plugin.settings, setting.key)
76
- : [], invalidates: invalidatesLabels(plugin.settings, setting), value: form.draft[setting.key], invalid: form.invalid.includes(setting.key), disabled: pending, onChange: (value, badInput) => form.setValue(setting.key, value, badInput) }, setting.key));
75
+ : [], value: form.draft[setting.key], invalid: form.invalid.includes(setting.key), disabled: pending, onChange: (value, badInput) => form.setValue(setting.key, value, badInput) }, setting.key));
77
76
  // `dimmed`: the group's required feature is off; the rows stay editable.
78
77
  const renderBlock = (block, dimmed) => {
79
78
  const dim = dimmed ? "opacity-50 transition-opacity" : "transition-opacity";
@@ -27,11 +27,6 @@ export declare const disabledParent: (features: readonly AdminPluginFeature[], f
27
27
  export declare const invalidatedByLabels: (settings: readonly AdminPluginSetting[], secretKey: string) => string[];
28
28
  /** Whether a settings group is dimmed: its required feature exists and is not effective. */
29
29
  export declare const isGroupDimmed: (features: readonly AdminPluginFeature[], requiresFeature: string | null) => boolean;
30
- /**
31
- * Labels of the secrets that a change of `setting` deletes (forward
32
- * `invalidates`), in the setting's order; unknown keys fall back to the key.
33
- */
34
- export declare const invalidatesLabels: (settings: readonly AdminPluginSetting[], setting: Pick<AdminPluginSetting, "invalidates">) => string[];
35
30
  /**
36
31
  * The description a setting shows in its info tooltip: only when a `hint`
37
32
  * takes the place under the field and the description says something else.
@@ -41,11 +41,6 @@ export const invalidatedByLabels = (settings, secretKey) => settings
41
41
  export const isGroupDimmed = (features, requiresFeature) => requiresFeature !== null &&
42
42
  features.find((feature) => feature.key === requiresFeature)
43
43
  ?.effective_enabled === false;
44
- /**
45
- * Labels of the secrets that a change of `setting` deletes (forward
46
- * `invalidates`), in the setting's order; unknown keys fall back to the key.
47
- */
48
- export const invalidatesLabels = (settings, setting) => setting.invalidates.map((key) => settings.find((candidate) => candidate.key === key)?.label ?? key);
49
44
  /**
50
45
  * The description a setting shows in its info tooltip: only when a `hint`
51
46
  * takes the place under the field and the description says something else.
@@ -5,6 +5,7 @@ export declare const texts: {
5
5
  breadcrumbs: {
6
6
  hub: string;
7
7
  emailAttachments: string;
8
+ emailDelivery: string;
8
9
  };
9
10
  page: {
10
11
  title: string;
@@ -70,8 +71,6 @@ export declare const texts: {
70
71
  secretClearUndoLabel: (label: string) => string;
71
72
  /** Shown on a secret: the settings whose change deletes it ("A", "A oder B"). */
72
73
  invalidatedBy: (labels: readonly string[]) => string;
73
- /** Shown on the changing setting: the secrets its change deletes. */
74
- invalidates: (labels: string) => string;
75
74
  invalidNumber: string;
76
75
  invalidColor: string;
77
76
  colorPlaceholder: string;
@@ -5,6 +5,7 @@ export const texts = {
5
5
  breadcrumbs: {
6
6
  hub: "Plugins",
7
7
  emailAttachments: "Anhänge",
8
+ emailDelivery: "Versand",
8
9
  },
9
10
  page: {
10
11
  title: "Hartl Services Plugins",
@@ -72,8 +73,6 @@ export const texts = {
72
73
  invalidatedBy: (labels) => `Wird gelöscht, wenn ${labels.length > 1
73
74
  ? `${labels.slice(0, -1).join(", ")} oder ${labels[labels.length - 1]}`
74
75
  : labels.join("")} sich ändert.`,
75
- /** Shown on the changing setting: the secrets its change deletes. */
76
- invalidates: (labels) => `Beim Ändern muss ${labels} neu eingegeben werden.`,
77
76
  invalidNumber: "Bitte eine gültige Zahl eingeben.",
78
77
  invalidColor: "Bitte eine Hex-Farbe wie #1a2b3c eingeben.",
79
78
  colorPlaceholder: "Standard",
@@ -28,7 +28,8 @@ const texts = {
28
28
  },
29
29
  breadcrumbs: {
30
30
  hub: "Plugins",
31
- emailAttachments: "Anhänge"
31
+ emailAttachments: "Anhänge",
32
+ emailDelivery: "Versand"
32
33
  },
33
34
  page: {
34
35
  title: "Hartl Services Plugins",
@@ -94,8 +95,6 @@ const texts = {
94
95
  secretClearUndoLabel: (label) => `${label} nicht löschen`,
95
96
  /** Shown on a secret: the settings whose change deletes it ("A", "A oder B"). */
96
97
  invalidatedBy: (labels) => `Wird gelöscht, wenn ${labels.length > 1 ? `${labels.slice(0, -1).join(", ")} oder ${labels[labels.length - 1]}` : labels.join("")} sich ändert.`,
97
- /** Shown on the changing setting: the secrets its change deletes. */
98
- invalidates: (labels) => `Beim Ändern muss ${labels} neu eingegeben werden.`,
99
98
  invalidNumber: "Bitte eine gültige Zahl eingeben.",
100
99
  invalidColor: "Bitte eine Hex-Farbe wie #1a2b3c eingeben.",
101
100
  colorPlaceholder: "Standard",
@@ -396,9 +395,6 @@ const disabledParent = (features, feature) => {
396
395
  };
397
396
  const invalidatedByLabels = (settings, secretKey) => settings.filter((setting) => setting.invalidates.includes(secretKey)).map((setting) => setting.label);
398
397
  const isGroupDimmed = (features, requiresFeature) => requiresFeature !== null && features.find((feature) => feature.key === requiresFeature)?.effective_enabled === false;
399
- const invalidatesLabels = (settings, setting) => setting.invalidates.map(
400
- (key) => settings.find((candidate) => candidate.key === key)?.label ?? key
401
- );
402
398
  const settingInfoText = (setting) => setting.hint !== null && setting.description !== null && setting.description !== setting.hint ? setting.description : null;
403
399
  const headerInfoText = (plugin2) => plugin2.summary !== null && plugin2.description !== null && plugin2.description !== plugin2.summary ? plugin2.description : null;
404
400
  const showsSourceHint = (source) => source !== "default";
@@ -1343,7 +1339,6 @@ const SettingField = ({
1343
1339
  pluginKey,
1344
1340
  setting,
1345
1341
  invalidatedBy,
1346
- invalidates,
1347
1342
  value,
1348
1343
  invalid,
1349
1344
  disabled,
@@ -1358,7 +1353,7 @@ const SettingField = ({
1358
1353
  const info = settingInfoText(setting);
1359
1354
  const showBounds = Boolean(bounds) && showsBoundsLine(setting);
1360
1355
  const required = setting.required;
1361
- const hasHints = Boolean(hint) || clearPending || invalidatedBy.length > 0 || invalidates.length > 0 || showBounds || invalid;
1356
+ const hasHints = Boolean(hint) || clearPending || invalidatedBy.length > 0 || showBounds || invalid;
1362
1357
  const describedBy = hasHints ? hintId : void 0;
1363
1358
  const placeholder = setting.placeholder ?? void 0;
1364
1359
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
@@ -1482,7 +1477,6 @@ const SettingField = ({
1482
1477
  hint && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: hint }),
1483
1478
  clearPending && /* @__PURE__ */ jsxRuntime.jsx(ui.Hint, { variant: "info", children: texts.settings.secretClearPending }),
1484
1479
  invalidatedBy.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: texts.settings.invalidatedBy(invalidatedBy) }),
1485
- invalidates.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: texts.settings.invalidates(invalidates.join(", ")) }),
1486
1480
  showBounds && !invalid && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted", children: bounds }),
1487
1481
  invalid && /* @__PURE__ */ jsxRuntime.jsx(ui.Hint, { variant: "error", children: setting.type === "color" ? texts.settings.invalidColor : bounds ?? texts.settings.invalidNumber })
1488
1482
  ] })
@@ -1505,7 +1499,6 @@ const PluginSettingsForm = ({ plugin: plugin2 }) => {
1505
1499
  pluginKey: plugin2.plugin_key,
1506
1500
  setting,
1507
1501
  invalidatedBy: setting.type === "secret" ? invalidatedByLabels(plugin2.settings, setting.key) : [],
1508
- invalidates: invalidatesLabels(plugin2.settings, setting),
1509
1502
  value: form.draft[setting.key],
1510
1503
  invalid: form.invalid.includes(setting.key),
1511
1504
  disabled: pending,
@@ -3528,7 +3521,7 @@ const PluginFeaturesSettingsPage = () => {
3528
3521
  const config = adminSdk.defineRouteConfig({
3529
3522
  label: texts.menu.label
3530
3523
  });
3531
- const handle$a = {
3524
+ const handle$b = {
3532
3525
  breadcrumb: () => texts.breadcrumbs.hub
3533
3526
  };
3534
3527
  const KINDS = [
@@ -3544,7 +3537,8 @@ const KINDS = [
3544
3537
  "auth.login_code",
3545
3538
  "affiliate.consignment_report_due",
3546
3539
  "affiliate.application_submitted.admin",
3547
- "user.invite"
3540
+ "user.invite",
3541
+ "system.credential_expiry.admin"
3548
3542
  ];
3549
3543
  const KIND_LABELS = {
3550
3544
  "order.received": "Bestellbestätigung (Kunde)",
@@ -3559,10 +3553,12 @@ const KIND_LABELS = {
3559
3553
  "auth.login_code": "Anmeldecode",
3560
3554
  "affiliate.consignment_report_due": "Affiliate-Meldung",
3561
3555
  "affiliate.application_submitted.admin": "Neue Partner-Bewerbung (Shop)",
3562
- "user.invite": "Admin-Einladung"
3556
+ "user.invite": "Admin-Einladung",
3557
+ "system.credential_expiry.admin": "Secret läuft ab (Shop)"
3563
3558
  };
3564
3559
  const EMAIL_PAGE_PATH = `${PLUGIN_HUB_PATH}/email`;
3565
3560
  const EMAIL_ATTACHMENTS_PAGE_PATH = `${EMAIL_PAGE_PATH}/attachments`;
3561
+ const EMAIL_DELIVERY_PAGE_PATH = `${EMAIL_PAGE_PATH}/delivery`;
3566
3562
  const emailAttachmentPagePath = (attachmentId) => `${EMAIL_ATTACHMENTS_PAGE_PATH}/${encodeURIComponent(attachmentId)}`;
3567
3563
  const NO_MAIL_ASSIGNED_LABEL = "keiner Mail zugeordnet";
3568
3564
  const MAIL_KIND_OPTIONS = email$1.EMAIL_ATTACHABLE_KINDS.map((kind) => ({
@@ -3820,52 +3816,61 @@ const LinkRow = ({
3820
3816
  description,
3821
3817
  to,
3822
3818
  disabled
3823
- }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4 px-6 py-3", children: [
3824
- /* @__PURE__ */ jsxRuntime.jsx(ui.IconBadge, { children: icon }),
3825
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-y-0.5", children: [
3826
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", weight: "plus", children: title }),
3827
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", className: "text-ui-fg-subtle", children: description })
3828
- ] }),
3829
- disabled ? /* @__PURE__ */ jsxRuntime.jsx(
3830
- ui.Button,
3831
- {
3832
- "aria-label": texts.plugin.openLabel(title),
3833
- disabled: true,
3834
- size: "small",
3835
- variant: "secondary",
3836
- children: texts.plugin.open
3837
- }
3838
- ) : /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { asChild: true, size: "small", variant: "secondary", children: /* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Link, { "aria-label": texts.plugin.openLabel(title), to, children: texts.plugin.open }) })
3839
- ] });
3840
- const EmailManagementSection = ({ disabled }) => /* @__PURE__ */ jsxRuntime.jsxs(
3841
- AdminSection,
3819
+ }) => /* @__PURE__ */ jsxRuntime.jsxs(
3820
+ "div",
3842
3821
  {
3843
- className: `transition-opacity ${disabled ? "opacity-50" : ""}`,
3844
- title: "Verwaltung",
3822
+ className: `flex items-center gap-4 px-6 py-3 transition-opacity ${disabled ? "opacity-50" : ""}`,
3845
3823
  children: [
3846
- /* @__PURE__ */ jsxRuntime.jsx(
3847
- LinkRow,
3848
- {
3849
- description: "PDFs an Kunden-Mails, z. B. AGB.",
3850
- disabled,
3851
- icon: /* @__PURE__ */ jsxRuntime.jsx(icons.PaperClip, {}),
3852
- title: "Anhänge",
3853
- to: EMAIL_ATTACHMENTS_PAGE_PATH
3854
- }
3855
- ),
3856
- /* @__PURE__ */ jsxRuntime.jsx(
3857
- LinkRow,
3824
+ /* @__PURE__ */ jsxRuntime.jsx(ui.IconBadge, { children: icon }),
3825
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-y-0.5", children: [
3826
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", weight: "plus", children: title }),
3827
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", className: "text-ui-fg-subtle", children: description })
3828
+ ] }),
3829
+ disabled ? /* @__PURE__ */ jsxRuntime.jsx(
3830
+ ui.Button,
3858
3831
  {
3859
- description: "Status und Zustellversuche.",
3860
- disabled,
3861
- icon: /* @__PURE__ */ jsxRuntime.jsx(icons.ClockSolid, {}),
3862
- title: "E-Mail-Protokoll",
3863
- to: EMAIL_LOG_PAGE_PATH
3832
+ "aria-label": texts.plugin.openLabel(title),
3833
+ disabled: true,
3834
+ size: "small",
3835
+ variant: "secondary",
3836
+ children: texts.plugin.open
3864
3837
  }
3865
- )
3838
+ ) : /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { asChild: true, size: "small", variant: "secondary", children: /* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Link, { "aria-label": texts.plugin.openLabel(title), to, children: texts.plugin.open }) })
3866
3839
  ]
3867
3840
  }
3868
3841
  );
3842
+ const EmailManagementSection = ({ disabled }) => /* @__PURE__ */ jsxRuntime.jsxs(AdminSection, { title: "Verwaltung", children: [
3843
+ /* @__PURE__ */ jsxRuntime.jsx(
3844
+ LinkRow,
3845
+ {
3846
+ description: "SMTP oder Microsoft 365.",
3847
+ disabled: false,
3848
+ icon: /* @__PURE__ */ jsxRuntime.jsx(icons.PaperPlane, {}),
3849
+ title: texts.breadcrumbs.emailDelivery,
3850
+ to: EMAIL_DELIVERY_PAGE_PATH
3851
+ }
3852
+ ),
3853
+ /* @__PURE__ */ jsxRuntime.jsx(
3854
+ LinkRow,
3855
+ {
3856
+ description: "PDFs an Kunden-Mails, z. B. AGB.",
3857
+ disabled,
3858
+ icon: /* @__PURE__ */ jsxRuntime.jsx(icons.PaperClip, {}),
3859
+ title: "Anhänge",
3860
+ to: EMAIL_ATTACHMENTS_PAGE_PATH
3861
+ }
3862
+ ),
3863
+ /* @__PURE__ */ jsxRuntime.jsx(
3864
+ LinkRow,
3865
+ {
3866
+ description: "Status und Zustellversuche.",
3867
+ disabled,
3868
+ icon: /* @__PURE__ */ jsxRuntime.jsx(icons.ClockSolid, {}),
3869
+ title: "E-Mail-Protokoll",
3870
+ to: EMAIL_LOG_PAGE_PATH
3871
+ }
3872
+ )
3873
+ ] });
3869
3874
  const EmailOverviewSections = () => {
3870
3875
  const plugin2 = usePluginFeature("email").data?.plugin;
3871
3876
  if (!plugin2) {
@@ -3877,7 +3882,7 @@ const EmailOverviewSections = () => {
3877
3882
  ] });
3878
3883
  };
3879
3884
  const EmailOverviewPage = () => /* @__PURE__ */ jsxRuntime.jsx(PluginPageShell, { pluginKey: "email", variant: "header", children: /* @__PURE__ */ jsxRuntime.jsx(EmailOverviewSections, {}) });
3880
- const handle$9 = {
3885
+ const handle$a = {
3881
3886
  breadcrumb: (_match) => /* @__PURE__ */ jsxRuntime.jsx(PluginLabelCrumb, { pluginKey: "email" })
3882
3887
  };
3883
3888
  const EMAIL_TEMPLATES_PATH = `${EMAIL_PAGE_PATH}/templates`;
@@ -3997,11 +4002,11 @@ const EmailTemplateGroupPage = ({
3997
4002
  ) })
3998
4003
  ] });
3999
4004
  };
4000
- const handle$8 = {
4005
+ const handle$9 = {
4001
4006
  breadcrumb: () => EMAIL_TEMPLATE_GROUPS.account.label
4002
4007
  };
4003
4008
  const EmailAccountMailsRoute = () => /* @__PURE__ */ jsxRuntime.jsx(PluginFeatureRoute, { pluginKey: "email", children: /* @__PURE__ */ jsxRuntime.jsx(EmailTemplateGroupPage, { group: "account" }) });
4004
- const handle$7 = {
4009
+ const handle$8 = {
4005
4010
  breadcrumb: () => EMAIL_TEMPLATE_GROUPS.affiliate.label
4006
4011
  };
4007
4012
  const EmailAffiliateMailsRoute = () => /* @__PURE__ */ jsxRuntime.jsx(PluginFeatureRoute, { pluginKey: "email", children: /* @__PURE__ */ jsxRuntime.jsx(EmailTemplateGroupPage, { group: "affiliate" }) });
@@ -4402,10 +4407,37 @@ const EmailAttachmentsPage = () => {
4402
4407
  /* @__PURE__ */ jsxRuntime.jsx(NewAttachmentModal, { onOpenChange: setNewOpen, open: newOpen })
4403
4408
  ] });
4404
4409
  };
4405
- const handle$6 = {
4410
+ const handle$7 = {
4406
4411
  breadcrumb: () => texts.breadcrumbs.emailAttachments
4407
4412
  };
4408
4413
  const EmailAttachmentsRoute = () => /* @__PURE__ */ jsxRuntime.jsx(PluginFeatureRoute, { pluginKey: "email", children: /* @__PURE__ */ jsxRuntime.jsx(EmailAttachmentsPage, {}) });
4414
+ const EmailDeliveryPage = () => {
4415
+ const query = usePluginFeature("email");
4416
+ const plugin2 = query.data?.plugin;
4417
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-3", children: [
4418
+ /* @__PURE__ */ jsxRuntime.jsx(
4419
+ AdminPageHeader,
4420
+ {
4421
+ badges: plugin2 && /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { color: plugin2.configured ? "green" : "orange", children: plugin2.configured ? texts.chips.configured : texts.chips.notConfigured }),
4422
+ description: "SMTP oder Microsoft 365.",
4423
+ icon: /* @__PURE__ */ jsxRuntime.jsx(icons.PaperPlane, {}),
4424
+ title: texts.breadcrumbs.emailDelivery
4425
+ }
4426
+ ),
4427
+ plugin2 ? /* @__PURE__ */ jsxRuntime.jsx(PluginSettingsForm, { plugin: plugin2 }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "p-0", children: /* @__PURE__ */ jsxRuntime.jsx(
4428
+ QueryState,
4429
+ {
4430
+ error: query.error,
4431
+ errorText: texts.states.error,
4432
+ isLoading: query.isPending,
4433
+ loadingText: texts.states.loading
4434
+ }
4435
+ ) })
4436
+ ] });
4437
+ };
4438
+ const handle$6 = {
4439
+ breadcrumb: () => texts.breadcrumbs.emailDelivery
4440
+ };
4409
4441
  const LOG_PAGE_SIZE = 20;
4410
4442
  const LOG_MAX_OFFSET = 1e4;
4411
4443
  const LOG_MAX_RESOURCE_ID_LENGTH = 255;
@@ -4637,17 +4669,17 @@ const handle$5 = {
4637
4669
  breadcrumb: () => "Protokoll"
4638
4670
  };
4639
4671
  const EmailLogRoute = () => /* @__PURE__ */ jsxRuntime.jsx(PluginFeatureRoute, { pluginKey: "email", children: /* @__PURE__ */ jsxRuntime.jsx(EmailLogPage, {}) });
4672
+ const handle$4 = {
4673
+ breadcrumb: () => EMAIL_TEMPLATE_GROUPS.order.label
4674
+ };
4675
+ const EmailOrderMailsRoute = () => /* @__PURE__ */ jsxRuntime.jsx(PluginFeatureRoute, { pluginKey: "email", children: /* @__PURE__ */ jsxRuntime.jsx(EmailTemplateGroupPage, { group: "order" }) });
4640
4676
  const PluginDetailPage = () => {
4641
4677
  const { plugin_key: pluginKey = "" } = reactRouterDom.useParams();
4642
4678
  return /* @__PURE__ */ jsxRuntime.jsx(PluginPageShell, { pluginKey, variant: "header", children: /* @__PURE__ */ jsxRuntime.jsx(PluginFeatureSections, { pluginKey }) });
4643
4679
  };
4644
- const handle$4 = {
4645
- breadcrumb: (match) => /* @__PURE__ */ jsxRuntime.jsx(PluginLabelCrumb, { pluginKey: match.params.plugin_key ?? "" })
4646
- };
4647
4680
  const handle$3 = {
4648
- breadcrumb: () => EMAIL_TEMPLATE_GROUPS.order.label
4681
+ breadcrumb: (match) => /* @__PURE__ */ jsxRuntime.jsx(PluginLabelCrumb, { pluginKey: match.params.plugin_key ?? "" })
4649
4682
  };
4650
- const EmailOrderMailsRoute = () => /* @__PURE__ */ jsxRuntime.jsx(PluginFeatureRoute, { pluginKey: "email", children: /* @__PURE__ */ jsxRuntime.jsx(EmailTemplateGroupPage, { group: "order" }) });
4651
4683
  const STATUS_BADGES = {
4652
4684
  current: { label: "aktuell", color: "green" },
4653
4685
  planned: { label: "geplant", color: "blue" },
@@ -6705,26 +6737,31 @@ const routeModule = {
6705
6737
  {
6706
6738
  Component: PluginFeaturesSettingsPage,
6707
6739
  path: "/settings/hartl-services",
6708
- handle: { label: config.label, translationNs: config.translationNs, ...handle$a }
6740
+ handle: { label: config.label, translationNs: config.translationNs, ...handle$b }
6709
6741
  },
6710
6742
  {
6711
6743
  Component: EmailOverviewPage,
6712
6744
  path: "/settings/hartl-services/email",
6713
- handle: handle$9
6745
+ handle: handle$a
6714
6746
  },
6715
6747
  {
6716
6748
  Component: EmailAccountMailsRoute,
6717
6749
  path: "/settings/hartl-services/email/account",
6718
- handle: handle$8
6750
+ handle: handle$9
6719
6751
  },
6720
6752
  {
6721
6753
  Component: EmailAffiliateMailsRoute,
6722
6754
  path: "/settings/hartl-services/email/affiliate",
6723
- handle: handle$7
6755
+ handle: handle$8
6724
6756
  },
6725
6757
  {
6726
6758
  Component: EmailAttachmentsRoute,
6727
6759
  path: "/settings/hartl-services/email/attachments",
6760
+ handle: handle$7
6761
+ },
6762
+ {
6763
+ Component: EmailDeliveryPage,
6764
+ path: "/settings/hartl-services/email/delivery",
6728
6765
  handle: handle$6
6729
6766
  },
6730
6767
  {
@@ -6733,13 +6770,13 @@ const routeModule = {
6733
6770
  handle: handle$5
6734
6771
  },
6735
6772
  {
6736
- Component: PluginDetailPage,
6737
- path: "/settings/hartl-services/plugin/:plugin_key",
6773
+ Component: EmailOrderMailsRoute,
6774
+ path: "/settings/hartl-services/email/order",
6738
6775
  handle: handle$4
6739
6776
  },
6740
6777
  {
6741
- Component: EmailOrderMailsRoute,
6742
- path: "/settings/hartl-services/email/order",
6778
+ Component: PluginDetailPage,
6779
+ path: "/settings/hartl-services/plugin/:plugin_key",
6743
6780
  handle: handle$3
6744
6781
  },
6745
6782
  {