@hartl-services/medusa-base 1.0.0 → 2.0.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.
- package/.medusa/server/src/admin/index.js +117 -1
- package/.medusa/server/src/admin/index.mjs +118 -2
- package/.medusa/server/src/documents/plugin-features.js +15 -1
- package/.medusa/server/src/documents/templates/financial-document.js +69 -51
- package/.medusa/server/src/documents/templates/format.js +44 -2
- package/.medusa/server/src/documents/templates/html-factory.js +7 -5
- package/.medusa/server/src/documents/templates/html-utils.js +12 -2
- package/.medusa/server/src/documents/templates/layout.js +115 -70
- package/.medusa/server/src/documents/templates/packing-list.js +10 -8
- package/.medusa/server/src/documents/utils/document-merchant.js +5 -5
- package/.medusa/server/src/documents/utils/document-render-settings.js +59 -0
- package/.medusa/server/src/email/application/handle-invite-event.js +8 -2
- package/.medusa/server/src/email/application/handle-password-reset-event.js +16 -4
- package/.medusa/server/src/email/domain/order-summary.js +103 -0
- package/.medusa/server/src/email/domain/refund-summary.js +43 -0
- package/.medusa/server/src/email/domain/return-summary.js +118 -0
- package/.medusa/server/src/email/domain/shipment-summary.js +74 -0
- package/.medusa/server/src/email/plugin-features.js +9 -1
- package/.medusa/server/src/email/rendering/default-templates.js +693 -65
- package/.medusa/server/src/email/rendering/load-email-shop-details.js +36 -0
- package/.medusa/server/src/email/rendering/render-email.js +16 -3
- package/.medusa/server/src/email/rendering/resolve-shop-configuration.js +20 -1
- package/.medusa/server/src/email/types/template-data.js +155 -2
- package/.medusa/server/src/modules/documents/domain/merchant.js +2 -33
- package/.medusa/server/src/modules/documents/domain/options.js +1 -7
- package/.medusa/server/src/modules/plugin-features/domain/plugin-settings.js +4 -3
- package/.medusa/server/src/public/descriptor.js +3 -1
- package/.medusa/server/src/public/shop-master-data.js +7 -7
- package/.medusa/server/src/shop-config/build-food-shop-config.js +6 -6
- package/.medusa/server/src/shop-config/options.js +5 -15
- package/.medusa/server/src/workflows/documents/issue-cancellation-after-refund.js +2 -3
- package/.medusa/server/src/workflows/documents/issue-document.js +6 -5
- package/.medusa/server/src/workflows/documents/issue-packing-list.js +9 -3
- package/.medusa/server/src/workflows/email/dispatch-email-message.js +4 -2
- package/.medusa/server/src/workflows/email/prepare-order-canceled-email.js +42 -4
- package/.medusa/server/src/workflows/email/prepare-order-received-email.js +31 -5
- package/.medusa/server/src/workflows/email/prepare-refund-email.js +25 -1
- package/.medusa/server/src/workflows/email/prepare-return-email.js +64 -3
- package/.medusa/server/src/workflows/email/prepare-shipment-email.js +36 -3
- package/README.md +180 -43
- package/dist/admin-components/features/plugin-features/components/ImageSettingInput.d.ts +15 -0
- package/dist/admin-components/features/plugin-features/components/ImageSettingInput.js +42 -0
- package/dist/admin-components/features/plugin-features/components/PluginSettingsForm.js +4 -2
- package/dist/admin-components/features/plugin-features/hooks/usePluginFeatureEnabled.d.ts +1 -1
- package/dist/admin-components/features/plugin-features/hooks/usePluginFeatures.d.ts +4 -4
- package/dist/admin-components/features/plugin-features/hooks/usePluginSettingsForm.d.ts +1 -1
- package/dist/admin-components/features/plugin-features/lib/image-upload.d.ts +6 -0
- package/dist/admin-components/features/plugin-features/lib/image-upload.js +17 -0
- package/dist/admin-components/features/plugin-features/lib/plugin-features-admin-client.d.ts +4 -4
- package/dist/admin-components/i18n/texts.d.ts +6 -0
- package/dist/admin-components/i18n/texts.js +6 -0
- package/dist/documents/templates/financial-document.d.ts +3 -3
- package/dist/documents/templates/financial-document.js +68 -50
- package/dist/documents/templates/format.d.ts +14 -0
- package/dist/documents/templates/format.js +43 -1
- package/dist/documents/templates/html-factory.d.ts +6 -2
- package/dist/documents/templates/html-factory.js +6 -4
- package/dist/documents/templates/html-utils.d.ts +6 -0
- package/dist/documents/templates/html-utils.js +11 -1
- package/dist/documents/templates/layout.d.ts +21 -3
- package/dist/documents/templates/layout.js +114 -69
- package/dist/documents/templates/packing-list.js +9 -7
- package/dist/documents/types/documents.d.ts +11 -16
- package/dist/documents/types/html-factory.d.ts +1 -0
- package/dist/email/types/shop-configuration.d.ts +25 -0
- package/dist/email/types/template-data.d.ts +509 -0
- package/dist/email/types/template-data.js +154 -1
- package/dist/modules/documents/domain/options.d.ts +1 -9
- package/dist/modules/documents/domain/options.js +0 -6
- package/dist/public/descriptor.js +2 -0
- package/dist/public/shop-master-data.d.ts +5 -6
- package/dist/public/shop-master-data.js +6 -6
- package/dist/public/types.d.ts +2 -1
- package/dist/shop-config/build-food-shop-config.js +5 -5
- package/dist/shop-config/options.d.ts +13 -8
- package/dist/shop-config/options.js +4 -14
- package/package.json +2 -2
- package/dist/modules/documents/domain/merchant.d.ts +0 -22
- package/dist/modules/documents/domain/merchant.js +0 -85
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ plugins: [
|
|
|
68
68
|
// E-Mail, Documents and the /files gateway: see the sections below
|
|
69
69
|
// (or let the Shop-Config-Preset fill them in).
|
|
70
70
|
// email: { jwtSecret, shop },
|
|
71
|
-
// documents: {
|
|
71
|
+
// documents: { numbering, pdf, htmlFactory },
|
|
72
72
|
// files: { gateway },
|
|
73
73
|
},
|
|
74
74
|
},
|
|
@@ -83,7 +83,7 @@ every module of the plugin; each module reads only its own namespace:
|
|
|
83
83
|
|---|---|---|
|
|
84
84
|
| `settingsSecret` | `plugin_features` module | Key material for stored secret settings. Set it from an environment variable of the shop; unset or empty, the base uses `projectConfig.http.cookieSecret`. |
|
|
85
85
|
| `email` | `email` module | `{ jwtSecret, shop }`: branding, URLs and the JWT secret of the e-mail links (see "E-Mail"). Without `email.shop` the module boots but sends nothing. |
|
|
86
|
-
| `documents` | `documents` module | `{
|
|
86
|
+
| `documents` | `documents` module | `{ numbering?, pdf?, htmlFactory? }` (see "Dokumente"). Defaults: numbering time zone `Europe/Vienna`, PDF executable `google-chrome`. |
|
|
87
87
|
| `files` | `/files` routes | `{ gateway: { signingSecret } }`, returned by `createS3CacheStorageConfig` (see "Dateispeicher (S3)"). |
|
|
88
88
|
|
|
89
89
|
`options: {}` is valid: the base then only provides the feature toggles,
|
|
@@ -318,7 +318,10 @@ class DemoModuleService extends MedusaService({ /* models */ }) {
|
|
|
318
318
|
|
|
319
319
|
- Types: `string`, `url` (http/https), `number` (finite, optional inclusive
|
|
320
320
|
`min`/`max`), `boolean`, `secret`, `select` (`options: [{ value, label }]`,
|
|
321
|
-
non-empty, unique values; the stored value must be one of them)
|
|
321
|
+
non-empty, unique values; the stored value must be one of them), `image`
|
|
322
|
+
(the http(s) URL of a PNG/JPEG of at most 2 MiB that the Admin uploads
|
|
323
|
+
through Medusa's `/admin/uploads` into the public File Module storage and
|
|
324
|
+
previews; replacing or removing it leaves the old file in storage). Keys
|
|
322
325
|
follow the feature key format and are unique. A `secret` has no default;
|
|
323
326
|
any other default must match its type, options and bounds. `options` exist
|
|
324
327
|
only on `select`, `min`/`max` only on `number`. `required` defaults to
|
|
@@ -477,15 +480,13 @@ settings like any plugin's:
|
|
|
477
480
|
| `iban`, `bic` | string | no |
|
|
478
481
|
| `company_register_number` (Firmenbuchnummer), `company_register_court` (Firmenbuchgericht) | string | no |
|
|
479
482
|
|
|
480
|
-
Server-side code reads them
|
|
481
|
-
fallback
|
|
483
|
+
Server-side code reads them exactly as stored in the Admin; there is no
|
|
484
|
+
medusa-config fallback:
|
|
482
485
|
|
|
483
486
|
```ts
|
|
484
487
|
import { getShopMasterData } from "@hartl-services/medusa-base"
|
|
485
488
|
|
|
486
|
-
const { values, configured, missing } = await getShopMasterData(container
|
|
487
|
-
legal_name: options.merchant?.legalName,
|
|
488
|
-
})
|
|
489
|
+
const { values, configured, missing } = await getShopMasterData(container)
|
|
489
490
|
if (!configured) {
|
|
490
491
|
throw new MedusaError(
|
|
491
492
|
MedusaError.Types.UNEXPECTED_STATE,
|
|
@@ -891,6 +892,10 @@ meantime, such as a password-reset link, is not sent). A toggle read
|
|
|
891
892
|
failure fails the subscriber with its fixed sentinel (the Event Bus retries
|
|
892
893
|
it) or fails the job run.
|
|
893
894
|
|
|
895
|
+
The plugin declares one setting, edited in the Einstellungen section of its
|
|
896
|
+
Admin page: `logo` (type `image`, optional), the PNG/JPG shown on top of every
|
|
897
|
+
mail. It is not a Shop-Stammdaten field; Documents has its own `logo`.
|
|
898
|
+
|
|
894
899
|
Admin surfaces of a disabled feature are hidden. The E-Mail settings page
|
|
895
900
|
keeps `PluginFeatureSections` visible (so the plugin can be switched back on)
|
|
896
901
|
and gates the Terms PDF versions and mail log sections below it on the
|
|
@@ -1060,20 +1065,130 @@ has at most 35 characters. Email runtime-validates every contract before use.
|
|
|
1060
1065
|
- Password-reset messages cannot be retried by the generic retry job or resent
|
|
1061
1066
|
by an Admin.
|
|
1062
1067
|
- Unknown transport outcomes are never automatically resent.
|
|
1063
|
-
- Shop branding is
|
|
1064
|
-
|
|
1065
|
-
plugin.
|
|
1068
|
+
- Shop branding is the configured `displayName` plus the `logo` setting of
|
|
1069
|
+
the E-Mail plugin and the company details of the Shop-Stammdaten; a shop overrides the default layout or
|
|
1070
|
+
single renderers in its own configuration, not in the plugin.
|
|
1066
1071
|
|
|
1067
1072
|
Read this section and the central design before changing Email domain behavior.
|
|
1068
1073
|
|
|
1069
1074
|
### Shop configuration and default templates
|
|
1070
1075
|
|
|
1071
|
-
The E-Mail module ships a neutral German layout
|
|
1072
|
-
|
|
1073
|
-
|
|
1076
|
+
The E-Mail module ships a neutral German layout in grey tones (table-based,
|
|
1077
|
+
inline styles) and one renderer per accepted kind. The layout shows, from top
|
|
1078
|
+
to bottom: the `logo` centered (an `image` setting of the E-Mail plugin,
|
|
1079
|
+
edited on its Admin page; without one, the configured `displayName`), a white card with the subject as heading and the
|
|
1080
|
+
renderer's body, a contact line ("Fragen? Dann schreib uns an … oder ruf uns
|
|
1081
|
+
an unter …", only with `email`/`phone`), and a footer with the company details
|
|
1082
|
+
(`legal_name` or `displayName`, address, UID, Firmenbuchnummer and
|
|
1083
|
+
Firmenbuchgericht, website) plus "Diese E-Mail wurde automatisch versendet.".
|
|
1084
|
+
Missing master data fields are left out. Email reads the logo and the
|
|
1085
|
+
Shop-Stammdaten when it sends a mail, so a change applies to the next mail; if
|
|
1086
|
+
a read fails the mail is sent without that part (logo or company details)
|
|
1087
|
+
instead of failing. Besides
|
|
1088
|
+
the layout, `displayName` appears in the password-reset and invite subjects.
|
|
1089
|
+
A custom `layout` receives the same data as `shop` (`EmailShopDetails`, every
|
|
1090
|
+
field optional) next to `bodyHtml`, `preheader` and `subject`. A shop
|
|
1074
1091
|
therefore needs only `displayName`, `affiliatePortalUrl`, `inviteUrl`,
|
|
1075
1092
|
`passwordResetUrl` and `orderNotificationEmail`.
|
|
1076
1093
|
|
|
1094
|
+
The order confirmation (`order.received`) receives an `OrderSummary` as
|
|
1095
|
+
`summary`: the display ID, the items with quantity and gross amount, the
|
|
1096
|
+
shipping method names, subtotal, shipping, discount, included tax and total,
|
|
1097
|
+
plus the billing and shipping address. Email freezes it when the order is
|
|
1098
|
+
placed, so a resend shows the order as it was placed. `summary` is absent for
|
|
1099
|
+
messages stored before the summary existed; a custom renderer must handle
|
|
1100
|
+
that case.
|
|
1101
|
+
|
|
1102
|
+
The shop's order notification (`order.received.admin`) receives the same
|
|
1103
|
+
`summary` (with the order's `email` and `placed_at`). Its subject names the
|
|
1104
|
+
order number and the customer ("Neue Bestellung #1042 von Ada Beispiel"),
|
|
1105
|
+
an info box lists number, customer, e-mail and order time, followed by an
|
|
1106
|
+
"Im Admin öffnen" button and the items, totals and addresses. Two optional
|
|
1107
|
+
`ShopEmailConfiguration` fields drive it: `adminOrderUrl({ orderId })` (the
|
|
1108
|
+
button is left out without it) and `timeZone` (IANA; the order time is left
|
|
1109
|
+
out without it). `buildFoodShopConfig` sets both (`<MEDUSA_BACKEND_URL>/app/
|
|
1110
|
+
orders/<id>` and its `timeZone` option). The layout receives `audience`
|
|
1111
|
+
(`"shop"` for this kind and `user.invite`, `"customer"` for all others) and
|
|
1112
|
+
shows the contact line only to customers.
|
|
1113
|
+
|
|
1114
|
+
The shipping confirmation (`order.shipped`, one per fulfillment) receives a
|
|
1115
|
+
`ShipmentSummary` as `shipment`, frozen when the shipment fact arrives: the
|
|
1116
|
+
display ID, the shipping option name, the tracking numbers and URLs entered
|
|
1117
|
+
at shipping, the parcel's items (title and variant of the order line,
|
|
1118
|
+
shipped quantity), the delivery address and `more_to_follow` (some ordered
|
|
1119
|
+
quantity is not shipped yet). `invoice_attached` is `true` when Documents chose
|
|
1120
|
+
this shipment for the invoice PDF. Its subject is "Deine Bestellung bei
|
|
1121
|
+
<displayName> ist unterwegs"; a single http(s) tracking URL becomes a
|
|
1122
|
+
"Sendung verfolgen" button, several become one link each. A fulfillment that
|
|
1123
|
+
cannot be read is confirmed without `shipment`, like a message stored before
|
|
1124
|
+
the snapshot existed.
|
|
1125
|
+
|
|
1126
|
+
The cancellation confirmation (`order.canceled`) receives the same
|
|
1127
|
+
`OrderSummary` as `summary` plus `canceled_at`, frozen when the cancellation
|
|
1128
|
+
fact arrives, and `cancellation_attached` (`true` when Documents attached at
|
|
1129
|
+
least one cancellation document). Its subject is "Deine Bestellung #1042 bei
|
|
1130
|
+
<displayName> wurde storniert"; an info box lists the order number and the
|
|
1131
|
+
cancellation date (left out without `timeZone`), followed by a fixed refund
|
|
1132
|
+
note and the canceled items and totals without addresses. An order that
|
|
1133
|
+
cannot be summarized is confirmed without `summary` and without its internal
|
|
1134
|
+
ID.
|
|
1135
|
+
|
|
1136
|
+
The return confirmation (`order.return_requested`) receives a `ReturnSummary`
|
|
1137
|
+
as `summary`, frozen when the return is requested: return and order numbers,
|
|
1138
|
+
the return shipping method, the items to send back (title and variant of the
|
|
1139
|
+
order line, quantity, return reason label) and the return address. It is
|
|
1140
|
+
the stock location's address, addressed to the location's company or else
|
|
1141
|
+
the master data's `legal_name`; without a location address it falls back to
|
|
1142
|
+
the master data's company address. Its subject is "Deine Rücksendung zur
|
|
1143
|
+
Bestellung #1042 bei <displayName>"; the mail asks the customer to enclose
|
|
1144
|
+
the return number (`#R5`). A return that cannot be read is confirmed without
|
|
1145
|
+
`summary` and without internal IDs.
|
|
1146
|
+
|
|
1147
|
+
The return receipt (`order.return_received`, sent once the return is
|
|
1148
|
+
completely received) receives a `ReturnReceivedSummary` as `summary`: return
|
|
1149
|
+
and order numbers, `received_at` and the received items (title and variant
|
|
1150
|
+
of the order line, received quantity and the damaged part of it). Its subject
|
|
1151
|
+
is "Deine Rücksendung zur Bestellung #1042 bei <displayName> ist
|
|
1152
|
+
eingetroffen"; the arrival date needs `timeZone`, damaged units are noted per
|
|
1153
|
+
item, and a fixed sentence says that an eventual refund follows on the
|
|
1154
|
+
original payment method, without promising a separate mail (`order.refunded`
|
|
1155
|
+
only exists with Documents).
|
|
1156
|
+
|
|
1157
|
+
The refund confirmation (`order.refunded`, one per non-cancellation refund,
|
|
1158
|
+
Documents only) receives a `RefundSummary` as `summary`, frozen when the
|
|
1159
|
+
Documents refund fact arrives and read through the refund's payment: order
|
|
1160
|
+
number, refunded gross amount and currency, `refunded_at` and the Admin's
|
|
1161
|
+
refund reason label. `cancellation_attached` is `true` when Documents attached
|
|
1162
|
+
a cancellation document. Its subject is "Deine Erstattung zur Bestellung #1042
|
|
1163
|
+
bei <displayName>"; the info box shows number, amount, date (needs
|
|
1164
|
+
`timeZone`) and reason, followed by a fixed note on bank processing times.
|
|
1165
|
+
Without `summary` the mail keeps the intro and the attachment note.
|
|
1166
|
+
|
|
1167
|
+
The password reset (`auth.password_reset`) has the subject "Neues Passwort
|
|
1168
|
+
für dein Kundenkonto bei <displayName>", a "Neues Passwort festlegen" button,
|
|
1169
|
+
the link as copyable text below it, the link's lifetime ("Der Link ist 15
|
|
1170
|
+
Minuten gültig.", computed from the token's `exp` − `iat` and left out
|
|
1171
|
+
without `iat`) and a note that the mail can be ignored. The lifetime is
|
|
1172
|
+
passed as the ephemeral `valid_minutes` next to `reset_url` and is never
|
|
1173
|
+
persisted.
|
|
1174
|
+
|
|
1175
|
+
The affiliate reminder (`affiliate.consignment_report_due`) greets the
|
|
1176
|
+
affiliate by `affiliate_display_name`, names the report month ("September
|
|
1177
|
+
2026", Austrian month names) and the due date ("10.10.2026") as calendar
|
|
1178
|
+
values formatted in UTC so no time zone shifts them, explains that the counted
|
|
1179
|
+
stock determines the billed sales, links `affiliatePortalUrl` as a "Zum
|
|
1180
|
+
Partnerbereich" button and says the mail can be ignored once the report is
|
|
1181
|
+
submitted. The button leads to a 404 while the affiliate plugin's
|
|
1182
|
+
`partner_portal` toggle is off.
|
|
1183
|
+
|
|
1184
|
+
The Admin invite (`user.invite`) has the subject "Einladung ins Admin-Team von
|
|
1185
|
+
<displayName>", an "Einladung annehmen" button with the link as copyable text,
|
|
1186
|
+
the invite's current expiry ("Die Einladung ist gültig bis 02.10.2026, 01:40
|
|
1187
|
+
Uhr.", needs `timeZone`; read from the invite at send time, so a resend shows
|
|
1188
|
+
its new expiry) and a note that the mail can be ignored. The expiry is passed
|
|
1189
|
+
as the ephemeral `expires_at` next to `invite_url`. The invite goes to the
|
|
1190
|
+
shop's team, so its layout audience is `"shop"` (no contact line).
|
|
1191
|
+
|
|
1077
1192
|
- `layout` (optional) replaces the default layout completely.
|
|
1078
1193
|
- `renderers` (optional, `Partial<EmailRendererMap>`) replaces only the listed
|
|
1079
1194
|
kinds; every other kind keeps its default renderer.
|
|
@@ -1344,12 +1459,7 @@ export default defineConfig({
|
|
|
1344
1459
|
},
|
|
1345
1460
|
{
|
|
1346
1461
|
resolve: "@hartl-services/medusa-tax-compliance",
|
|
1347
|
-
options: {
|
|
1348
|
-
merchant: {
|
|
1349
|
-
countryCode: process.env.MERCHANT_COUNTRY_CODE!,
|
|
1350
|
-
vatNumber: process.env.MERCHANT_VAT_NUMBER!,
|
|
1351
|
-
},
|
|
1352
|
-
},
|
|
1462
|
+
options: {},
|
|
1353
1463
|
},
|
|
1354
1464
|
],
|
|
1355
1465
|
});
|
|
@@ -1370,10 +1480,9 @@ Options of the `documents` namespace (all optional; `{}` is valid):
|
|
|
1370
1480
|
rejected at startup when invalid.
|
|
1371
1481
|
- `pdf.executablePath` — Chromium/Chrome executable path or command name.
|
|
1372
1482
|
Defaults to `"google-chrome"`.
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
countryCode, postalCode }, vatNumber? }`.
|
|
1483
|
+
|
|
1484
|
+
The sender (company, address, UID, contact, bank) is not an option: it comes
|
|
1485
|
+
only from the Shop-Stammdaten (see "Merchant data" below).
|
|
1377
1486
|
|
|
1378
1487
|
The `documents` module is always registered with the base (no `documentsEnabled`
|
|
1379
1488
|
switch); the Admin switches it off at run time through the feature toggles
|
|
@@ -1384,8 +1493,9 @@ below. `medusa db:migrate` creates its tables.
|
|
|
1384
1493
|
`@hartl-services/medusa-base/config` exports, next to the environment helpers,
|
|
1385
1494
|
build-time values for `medusa-config.ts` and tests:
|
|
1386
1495
|
|
|
1387
|
-
- `createDefaultDocumentHtmlFactory({ timeZone })` — the built-in
|
|
1388
|
-
use as it is
|
|
1496
|
+
- `createDefaultDocumentHtmlFactory({ timeZone, theme? })` — the built-in
|
|
1497
|
+
factory, to use as it is, with a shop theme (see "Layout, logo, payment
|
|
1498
|
+
terms and shop theme" below) or to wrap.
|
|
1389
1499
|
- `DOCUMENTS_BUILD_PLACEHOLDERS` — `{ DOCUMENT_PDF_EXECUTABLE: "google-chrome" }`.
|
|
1390
1500
|
`medusa build` evaluates `medusa-config.ts` without the runtime-only
|
|
1391
1501
|
environment, so spread it under the real environment while building.
|
|
@@ -1393,6 +1503,44 @@ build-time values for `medusa-config.ts` and tests:
|
|
|
1393
1503
|
The document types (`DocumentMerchantOption`, `DocumentHtmlFactory`,
|
|
1394
1504
|
`DocumentRenderInput`, ...) come from `@hartl-services/medusa-base/types`.
|
|
1395
1505
|
|
|
1506
|
+
### Layout, logo, payment terms and shop theme
|
|
1507
|
+
|
|
1508
|
+
The default markup prints an A4 letterhead: logo top left, document title and
|
|
1509
|
+
key data (number, order, date, customer UID for exempt B2B supplies) on the
|
|
1510
|
+
right, the billing address on the left, the item table with batch and
|
|
1511
|
+
best-before date per line, the totals below the price columns and a
|
|
1512
|
+
three-column footer (sender with UID and company register, contact, bank) on
|
|
1513
|
+
every page. Packing lists show only the sender in the footer. Every page
|
|
1514
|
+
carries "Seite x von y" bottom right in an 8 mm bottom page margin
|
|
1515
|
+
(`@page { @bottom-right }`; the page count exists only in page margin boxes).
|
|
1516
|
+
|
|
1517
|
+
Settings under Settings → "Hartl Services Plugins" → **Dokumente**, read on
|
|
1518
|
+
every render and passed as `input.settings` (`DocumentRenderSettings`). Like
|
|
1519
|
+
the merchant data they are not part of the snapshot: a regenerated document
|
|
1520
|
+
shows the current values.
|
|
1521
|
+
|
|
1522
|
+
- **Logo** (`logo`, PNG or JPG): each render downloads it (10 s timeout, at
|
|
1523
|
+
most 2 MiB) and passes it inline as `settings.logo_src` (`data:` URL), so
|
|
1524
|
+
Chromium never waits on the network. Without a logo, or when the download
|
|
1525
|
+
fails (logged as a warning), the document shows the merchant's legal name;
|
|
1526
|
+
a logo never blocks issuance.
|
|
1527
|
+
- **Zahlungskonditionen** (`payment_terms`, text): printed below the totals
|
|
1528
|
+
of invoices only (not on cancellations or credit notes); blank means none.
|
|
1529
|
+
Line breaks in the text are kept.
|
|
1530
|
+
|
|
1531
|
+
**Theme:** `createDefaultDocumentHtmlFactory({ timeZone, theme })` keeps the
|
|
1532
|
+
content and markup and changes only the look. `theme.stylesheet` is CSS
|
|
1533
|
+
appended after the default stylesheet (later rules win; the defaults use the
|
|
1534
|
+
custom properties `--accent`, `--text`, `--muted`, `--line`, `--table-head`,
|
|
1535
|
+
`--page-x`, `--page-top`, `--footer-height`). Put a watermark or other
|
|
1536
|
+
full-page image into `@page { background: ... / 210mm 297mm }`: it covers the
|
|
1537
|
+
whole sheet on every page, whereas Chromium clips fixed elements at the page
|
|
1538
|
+
margin and sizes an inline `<svg>` in them wrongly from the second page on.
|
|
1539
|
+
Restyle the page number with `@page { @bottom-right { ... } }`. Embed web
|
|
1540
|
+
fonts as `data:` URLs; the container's Chromium has only its system fonts. A
|
|
1541
|
+
shop passes its factory through the preset option `documentHtmlFactory` (see
|
|
1542
|
+
"Shop-Config-Preset").
|
|
1543
|
+
|
|
1396
1544
|
### Merchant data
|
|
1397
1545
|
|
|
1398
1546
|
The legal sender printed on every document comes from the base plugin's shop
|
|
@@ -1401,8 +1549,8 @@ Each render reads it and passes it to the `htmlFactory` as `input.merchant`
|
|
|
1401
1549
|
(`DocumentMerchant`: `legal_name`, `address_line1`, `address_line2`,
|
|
1402
1550
|
`postal_code`, `city`, `country_code`, `vat_number`, `email`, `phone`,
|
|
1403
1551
|
`website`, `iban`, `bic`, `company_register_number`,
|
|
1404
|
-
`company_register_court`; unset optional fields are `null`).
|
|
1405
|
-
|
|
1552
|
+
`company_register_court`; unset optional fields are `null`). There is no
|
|
1553
|
+
medusa-config fallback: what is stored in the Admin is what is printed.
|
|
1406
1554
|
|
|
1407
1555
|
- `merchant` is not part of the stored snapshot: a regenerated document
|
|
1408
1556
|
prints the master data current at regeneration time.
|
|
@@ -1482,8 +1630,8 @@ a toggle.
|
|
|
1482
1630
|
### Shop integration
|
|
1483
1631
|
|
|
1484
1632
|
The demo shop (`apps/backend`) gets everything from the preset (see
|
|
1485
|
-
"Shop-Config-Preset"): `documents.
|
|
1486
|
-
`documents.
|
|
1633
|
+
"Shop-Config-Preset"): `documents.numbering.timeZone` is its `timeZone`
|
|
1634
|
+
option, `documents.htmlFactory` its optional `documentHtmlFactory` and
|
|
1487
1635
|
`documents.pdf.executablePath` the `DOCUMENT_PDF_EXECUTABLE` variable. The
|
|
1488
1636
|
development UID `ATU00000000` is a placeholder: Documents never prints it,
|
|
1489
1637
|
and Tax Compliance does not transmit it to FinanzOnline. Enter the real
|
|
@@ -2151,17 +2299,6 @@ pnpm add @hartl-services/medusa-base \
|
|
|
2151
2299
|
import { defineFoodShopConfig } from "@hartl-services/medusa-base/shop-config"
|
|
2152
2300
|
|
|
2153
2301
|
module.exports = defineFoodShopConfig({
|
|
2154
|
-
merchant: {
|
|
2155
|
-
countryCode: "at",
|
|
2156
|
-
legalName: "Example Shop GmbH",
|
|
2157
|
-
registeredAddress: {
|
|
2158
|
-
address1: "Musterstrasse 1",
|
|
2159
|
-
city: "Wien",
|
|
2160
|
-
countryCode: "at",
|
|
2161
|
-
postalCode: "1010",
|
|
2162
|
-
},
|
|
2163
|
-
vatNumber: "ATU12345678",
|
|
2164
|
-
},
|
|
2165
2302
|
displayName: "EXAMPLE SHOP",
|
|
2166
2303
|
orderNotificationEmail: "orders@example.com",
|
|
2167
2304
|
timeZone: "Europe/Vienna",
|
|
@@ -2202,11 +2339,11 @@ validate the environment; a missing variable fails with the variable's name.
|
|
|
2202
2339
|
|
|
2203
2340
|
| Option | Meaning |
|
|
2204
2341
|
| --- | --- |
|
|
2205
|
-
| `
|
|
2206
|
-
| `displayName` | Shop name in the default e-mail templates. |
|
|
2342
|
+
| `displayName` | Shop name in the default e-mail templates (a shop's initial seed may use it as store name). Company name, address and UID are no options: documents and tax compliance read them only from the Shop-Stammdaten (Admin, "Hartl Services Plugins"). |
|
|
2207
2343
|
| `orderNotificationEmail` | Receives order notification e-mails; must look like an e-mail address. |
|
|
2208
2344
|
| `timeZone` | IANA time zone (validated with `Intl`). Chooses the annual document-number period, the printed dates on documents and the affiliate consignment reminder time zone. |
|
|
2209
2345
|
| `storefrontCountryCode` | 2-letter lowercase country code of the storefront route. Links: `<STOREFRONT_URL>/<cc>/partnerportal` (affiliate portal), `<STOREFRONT_URL>/<cc>/reset-password` (the e-mail plugin appends `#token=`), referral base `<STOREFRONT_URL>/<cc>`. The Admin invite link is `<MEDUSA_BACKEND_URL>/app/invite?token=`. |
|
|
2346
|
+
| `documentHtmlFactory` | Optional `DocumentHtmlFactory` that replaces the default document markup, typically `createDefaultDocumentHtmlFactory({ timeZone, theme })` from `/config` with the shop's CSS and watermark (see "Layout, logo, payment terms and shop theme"). Must be a function. |
|
|
2210
2347
|
| `loadEnvFile` | Default `true`: calls `loadEnv(NODE_ENV \|\| "development", process.cwd())`. Pass `false` when the environment is already complete (tests, isolated child processes). |
|
|
2211
2348
|
|
|
2212
2349
|
### Environment variables
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type ImageSettingInputProps = {
|
|
2
|
+
id: string;
|
|
3
|
+
hintId: string;
|
|
4
|
+
label: string;
|
|
5
|
+
/** Public URL of the current image; `""` = none. */
|
|
6
|
+
value: string;
|
|
7
|
+
disabled: boolean;
|
|
8
|
+
onChange: (url: string) => void;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Uploads an image to the public File Module storage and hands its URL to
|
|
12
|
+
* the settings draft; the form stores it on save like any other value.
|
|
13
|
+
*/
|
|
14
|
+
export declare const ImageSettingInput: ({ id, hintId, label, value, disabled, onChange, }: ImageSettingInputProps) => import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button, Hint, Text } from "@medusajs/ui";
|
|
3
|
+
import { useMutation } from "@tanstack/react-query";
|
|
4
|
+
import { useRef, useState } from "react";
|
|
5
|
+
import { texts } from "../../../i18n/texts";
|
|
6
|
+
import { sdk } from "../../../lib/sdk";
|
|
7
|
+
import { validateSettingImage } from "../lib/image-upload";
|
|
8
|
+
/**
|
|
9
|
+
* Uploads an image to the public File Module storage and hands its URL to
|
|
10
|
+
* the settings draft; the form stores it on save like any other value.
|
|
11
|
+
*/
|
|
12
|
+
export const ImageSettingInput = ({ id, hintId, label, value, disabled, onChange, }) => {
|
|
13
|
+
const fileInput = useRef(null);
|
|
14
|
+
const [error, setError] = useState(null);
|
|
15
|
+
const upload = useMutation({
|
|
16
|
+
mutationFn: async (file) => {
|
|
17
|
+
const { files } = await sdk.admin.upload.create({ files: [file] });
|
|
18
|
+
const url = files[0]?.url;
|
|
19
|
+
if (!url) {
|
|
20
|
+
throw new Error("upload returned no file");
|
|
21
|
+
}
|
|
22
|
+
return url;
|
|
23
|
+
},
|
|
24
|
+
onSuccess: (url) => onChange(url),
|
|
25
|
+
onError: () => setError(texts.settings.imageUploadFailed),
|
|
26
|
+
});
|
|
27
|
+
const busy = disabled || upload.isPending;
|
|
28
|
+
const selectFile = (file) => {
|
|
29
|
+
if (!file) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const invalid = validateSettingImage(file);
|
|
33
|
+
setError(invalid);
|
|
34
|
+
if (!invalid) {
|
|
35
|
+
upload.mutate(file);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
return (_jsxs("div", { className: "grid gap-2", children: [value && (_jsx("div", { className: "bg-ui-bg-subtle flex items-center justify-center rounded-md border p-4", children: _jsx("img", { src: value, alt: texts.settings.imagePreviewAlt(label), className: "max-h-24 max-w-full object-contain" }) })), _jsx("input", { ref: fileInput, id: id, "aria-describedby": hintId, type: "file", accept: "image/png,image/jpeg", className: "hidden", onChange: (event) => {
|
|
39
|
+
selectFile(event.target.files?.[0]);
|
|
40
|
+
event.target.value = "";
|
|
41
|
+
} }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { type: "button", size: "small", variant: "secondary", isLoading: upload.isPending, disabled: busy, onClick: () => fileInput.current?.click(), children: value ? texts.settings.imageReplace : texts.settings.imageUpload }), value && (_jsx(Button, { type: "button", size: "small", variant: "transparent", disabled: busy, onClick: () => onChange(""), children: texts.settings.imageRemove })), !value && (_jsx(Text, { size: "small", className: "text-ui-fg-muted", children: texts.settings.imageNone }))] }), error && _jsx(Hint, { variant: "error", children: error })] }));
|
|
42
|
+
};
|
|
@@ -2,6 +2,7 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { Alert, Button, Container, Heading, Hint, Input, Label, Select, Switch, Text, } from "@medusajs/ui";
|
|
3
3
|
import { texts } from "../../../i18n/texts";
|
|
4
4
|
import { usePluginSettingsForm } from "../hooks/usePluginSettingsForm";
|
|
5
|
+
import { ImageSettingInput } from "./ImageSettingInput";
|
|
5
6
|
const fieldId = (pluginKey, key) => `plugin-setting-${pluginKey}-${key}`;
|
|
6
7
|
export const missingLabels = (plugin) => plugin.missing
|
|
7
8
|
.map((key) => plugin.settings.find((setting) => setting.key === key)?.label ?? key)
|
|
@@ -27,13 +28,14 @@ const SettingField = ({ pluginKey, setting, invalidatesLabels, value, invalid, d
|
|
|
27
28
|
? texts.settings.secretClearUndoLabel(setting.label)
|
|
28
29
|
: texts.settings.secretClearLabel(setting.label), onClick: () => onChange(clearPending ? "" : null), children: clearPending
|
|
29
30
|
? texts.settings.secretClearUndo
|
|
30
|
-
: 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": hintId, 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 === "number" ? (_jsx(Input, { id: id, "aria-describedby": hintId, "aria-invalid": invalid, type: "number", step: "any", min: setting.min ?? undefined, max: setting.max ?? undefined, value: typeof value === "string" ? value : "", disabled: disabled, onChange: (event) => onChange(event.target.value, event.target.validity.badInput) })) : (_jsx(Input, { id: id, "aria-describedby": hintId, "aria-invalid": invalid, type: setting.type === "url" ? "url" : "text", value: typeof value === "string" ? value : "", disabled: disabled, onChange: (event) => onChange(event.target.value) })), _jsxs("div", { id: hintId, className: "grid gap-1", children: [setting.description && (_jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: setting.description })), clearPending && (_jsx(Hint, { variant: "info", children: texts.settings.secretClearPending })), invalidatesLabels && (_jsx(Text, { size: "small", className: "text-ui-fg-muted", children: texts.settings.invalidates(invalidatesLabels) })), bounds && !invalid && (_jsx(Text, { size: "small", className: "text-ui-fg-muted", children: bounds })), invalid && (_jsx(Hint, { variant: "error", children: bounds ?? texts.settings.invalidNumber }))] })] }));
|
|
31
|
+
: 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": hintId, 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, hintId: hintId, label: setting.label, value: typeof value === "string" ? value : "", disabled: disabled, onChange: (url) => onChange(url) })) : setting.type === "number" ? (_jsx(Input, { id: id, "aria-describedby": hintId, "aria-invalid": invalid, type: "number", step: "any", min: setting.min ?? undefined, max: setting.max ?? undefined, value: typeof value === "string" ? value : "", disabled: disabled, onChange: (event) => onChange(event.target.value, event.target.validity.badInput) })) : (_jsx(Input, { id: id, "aria-describedby": hintId, "aria-invalid": invalid, type: setting.type === "url" ? "url" : "text", value: typeof value === "string" ? value : "", disabled: disabled, onChange: (event) => onChange(event.target.value) })), _jsxs("div", { id: hintId, className: "grid gap-1", children: [setting.description && (_jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: setting.description })), clearPending && (_jsx(Hint, { variant: "info", children: texts.settings.secretClearPending })), invalidatesLabels && (_jsx(Text, { size: "small", className: "text-ui-fg-muted", children: texts.settings.invalidates(invalidatesLabels) })), bounds && !invalid && (_jsx(Text, { size: "small", className: "text-ui-fg-muted", children: bounds })), invalid && (_jsx(Hint, { variant: "error", children: bounds ?? texts.settings.invalidNumber }))] })] }));
|
|
31
32
|
};
|
|
32
33
|
export const PluginSettingsForm = ({ plugin }) => {
|
|
33
34
|
const form = usePluginSettingsForm(plugin);
|
|
34
35
|
const pending = form.mutation.isPending;
|
|
35
36
|
const hasTextFields = plugin.settings.some((setting) => setting.type !== "boolean" &&
|
|
36
37
|
setting.type !== "secret" &&
|
|
37
|
-
setting.type !== "select"
|
|
38
|
+
setting.type !== "select" &&
|
|
39
|
+
setting.type !== "image");
|
|
38
40
|
return (_jsxs(Container, { className: "divide-y p-0", "aria-busy": pending, children: [_jsxs("div", { className: "px-6 py-4", children: [_jsx(Heading, { level: "h2", children: texts.sections.settings }), hasTextFields && (_jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: texts.settings.clearHint }))] }), _jsxs("form", { onSubmit: form.submit, className: "grid gap-5 px-6 py-5", children: [!plugin.configured && (_jsx(Alert, { variant: "warning", children: texts.settings.missing(missingLabels(plugin)) })), plugin.settings.map((setting) => (_jsx(SettingField, { pluginKey: plugin.plugin_key, setting: setting, invalidatesLabels: invalidatesLabels(plugin, 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))), _jsx("div", { className: "flex justify-end", children: _jsx(Button, { type: "submit", size: "small", isLoading: pending, disabled: !form.dirty || form.invalid.length > 0 || pending, children: texts.settings.save }) })] })] }));
|
|
39
41
|
};
|
|
@@ -74,7 +74,7 @@ export declare const pluginFeatureEnabledQueryOptions: (pluginKey: string, featu
|
|
|
74
74
|
source: "default" | "stored" | "config" | "none";
|
|
75
75
|
invalidates: string[];
|
|
76
76
|
} | {
|
|
77
|
-
type: "string" | "boolean" | "url";
|
|
77
|
+
type: "string" | "boolean" | "url" | "image";
|
|
78
78
|
default: string | number | boolean | null;
|
|
79
79
|
value: string | number | boolean | null;
|
|
80
80
|
key: string;
|
|
@@ -54,7 +54,7 @@ export declare function usePluginFeatures(): import("@tanstack/react-query").Use
|
|
|
54
54
|
source: "default" | "stored" | "config" | "none";
|
|
55
55
|
invalidates: string[];
|
|
56
56
|
} | {
|
|
57
|
-
type: "string" | "boolean" | "url";
|
|
57
|
+
type: "string" | "boolean" | "url" | "image";
|
|
58
58
|
default: string | number | boolean | null;
|
|
59
59
|
value: string | number | boolean | null;
|
|
60
60
|
key: string;
|
|
@@ -141,7 +141,7 @@ export declare function usePluginFeature(pluginKey: string | undefined): import(
|
|
|
141
141
|
source: "default" | "stored" | "config" | "none";
|
|
142
142
|
invalidates: string[];
|
|
143
143
|
} | {
|
|
144
|
-
type: "string" | "boolean" | "url";
|
|
144
|
+
type: "string" | "boolean" | "url" | "image";
|
|
145
145
|
default: string | number | boolean | null;
|
|
146
146
|
value: string | number | boolean | null;
|
|
147
147
|
key: string;
|
|
@@ -222,7 +222,7 @@ export declare function useUpdatePluginFeatures(): import("@tanstack/react-query
|
|
|
222
222
|
source: "default" | "stored" | "config" | "none";
|
|
223
223
|
invalidates: string[];
|
|
224
224
|
} | {
|
|
225
|
-
type: "string" | "boolean" | "url";
|
|
225
|
+
type: "string" | "boolean" | "url" | "image";
|
|
226
226
|
default: string | number | boolean | null;
|
|
227
227
|
value: string | number | boolean | null;
|
|
228
228
|
key: string;
|
|
@@ -307,7 +307,7 @@ export declare function useUpdatePluginSettings(pluginKey: string): import("@tan
|
|
|
307
307
|
source: "default" | "stored" | "config" | "none";
|
|
308
308
|
invalidates: string[];
|
|
309
309
|
} | {
|
|
310
|
-
type: "string" | "boolean" | "url";
|
|
310
|
+
type: "string" | "boolean" | "url" | "image";
|
|
311
311
|
default: string | number | boolean | null;
|
|
312
312
|
value: string | number | boolean | null;
|
|
313
313
|
key: string;
|
|
@@ -63,7 +63,7 @@ export declare function usePluginSettingsForm(plugin: AdminPluginFeatures): {
|
|
|
63
63
|
source: "default" | "stored" | "config" | "none";
|
|
64
64
|
invalidates: string[];
|
|
65
65
|
} | {
|
|
66
|
-
type: "string" | "boolean" | "url";
|
|
66
|
+
type: "string" | "boolean" | "url" | "image";
|
|
67
67
|
default: string | number | boolean | null;
|
|
68
68
|
value: string | number | boolean | null;
|
|
69
69
|
key: string;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Largest image an `image` setting accepts in the Admin. */
|
|
2
|
+
export declare const SETTING_IMAGE_MAX_BYTES: number;
|
|
3
|
+
type ImageSelection = Pick<File, "size" | "type">;
|
|
4
|
+
/** Why `file` cannot be uploaded for an `image` setting; `null` = valid. */
|
|
5
|
+
export declare const validateSettingImage: (file: ImageSelection) => string | null;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Largest image an `image` setting accepts in the Admin. */
|
|
2
|
+
export const SETTING_IMAGE_MAX_BYTES = 2 * 1024 * 1024;
|
|
3
|
+
// Gmail and Outlook do not display SVG or WebP in e-mails.
|
|
4
|
+
const IMAGE_TYPES = new Set(["image/png", "image/jpeg"]);
|
|
5
|
+
/** Why `file` cannot be uploaded for an `image` setting; `null` = valid. */
|
|
6
|
+
export const validateSettingImage = (file) => {
|
|
7
|
+
if (!IMAGE_TYPES.has(file.type)) {
|
|
8
|
+
return "Bitte eine PNG- oder JPG-Datei auswählen.";
|
|
9
|
+
}
|
|
10
|
+
if (file.size === 0) {
|
|
11
|
+
return "Die Bilddatei ist leer.";
|
|
12
|
+
}
|
|
13
|
+
if (file.size > SETTING_IMAGE_MAX_BYTES) {
|
|
14
|
+
return "Die Bilddatei darf maximal 2 MiB groß sein.";
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
};
|
package/dist/admin-components/features/plugin-features/lib/plugin-features-admin-client.d.ts
CHANGED
|
@@ -55,7 +55,7 @@ export declare const listPluginFeatures: () => Promise<{
|
|
|
55
55
|
source: "default" | "stored" | "config" | "none";
|
|
56
56
|
invalidates: string[];
|
|
57
57
|
} | {
|
|
58
|
-
type: "string" | "boolean" | "url";
|
|
58
|
+
type: "string" | "boolean" | "url" | "image";
|
|
59
59
|
default: string | number | boolean | null;
|
|
60
60
|
value: string | number | boolean | null;
|
|
61
61
|
key: string;
|
|
@@ -135,7 +135,7 @@ export declare const retrievePluginFeatures: (pluginKey: string) => Promise<{
|
|
|
135
135
|
source: "default" | "stored" | "config" | "none";
|
|
136
136
|
invalidates: string[];
|
|
137
137
|
} | {
|
|
138
|
-
type: "string" | "boolean" | "url";
|
|
138
|
+
type: "string" | "boolean" | "url" | "image";
|
|
139
139
|
default: string | number | boolean | null;
|
|
140
140
|
value: string | number | boolean | null;
|
|
141
141
|
key: string;
|
|
@@ -215,7 +215,7 @@ export declare const updatePluginFeatures: (payload: UpdatePluginFeaturesPayload
|
|
|
215
215
|
source: "default" | "stored" | "config" | "none";
|
|
216
216
|
invalidates: string[];
|
|
217
217
|
} | {
|
|
218
|
-
type: "string" | "boolean" | "url";
|
|
218
|
+
type: "string" | "boolean" | "url" | "image";
|
|
219
219
|
default: string | number | boolean | null;
|
|
220
220
|
value: string | number | boolean | null;
|
|
221
221
|
key: string;
|
|
@@ -295,7 +295,7 @@ export declare const updatePluginSettings: (pluginKey: string, payload: UpdatePl
|
|
|
295
295
|
source: "default" | "stored" | "config" | "none";
|
|
296
296
|
invalidates: string[];
|
|
297
297
|
} | {
|
|
298
|
-
type: "string" | "boolean" | "url";
|
|
298
|
+
type: "string" | "boolean" | "url" | "image";
|
|
299
299
|
default: string | number | boolean | null;
|
|
300
300
|
value: string | number | boolean | null;
|
|
301
301
|
key: string;
|
|
@@ -59,6 +59,12 @@ export declare const texts: {
|
|
|
59
59
|
numberMax: (max: number) => string;
|
|
60
60
|
numberRange: (min: number, max: number) => string;
|
|
61
61
|
selectPlaceholder: string;
|
|
62
|
+
imageUpload: string;
|
|
63
|
+
imageReplace: string;
|
|
64
|
+
imageRemove: string;
|
|
65
|
+
imageNone: string;
|
|
66
|
+
imageUploadFailed: string;
|
|
67
|
+
imagePreviewAlt: (label: string) => string;
|
|
62
68
|
clearHint: string;
|
|
63
69
|
source: {
|
|
64
70
|
stored: string;
|
|
@@ -59,6 +59,12 @@ export const texts = {
|
|
|
59
59
|
numberMax: (max) => `Höchstens ${max}.`,
|
|
60
60
|
numberRange: (min, max) => `Zwischen ${min} und ${max}.`,
|
|
61
61
|
selectPlaceholder: "Bitte auswählen",
|
|
62
|
+
imageUpload: "Bild hochladen",
|
|
63
|
+
imageReplace: "Anderes Bild hochladen",
|
|
64
|
+
imageRemove: "Entfernen",
|
|
65
|
+
imageNone: "Kein Bild hinterlegt",
|
|
66
|
+
imageUploadFailed: "Das Bild konnte nicht hochgeladen werden.",
|
|
67
|
+
imagePreviewAlt: (label) => `Vorschau ${label}`,
|
|
62
68
|
clearHint: "Ein leeres Feld speichern entfernt den gespeicherten Wert (dann gilt Konfiguration oder Standard).",
|
|
63
69
|
source: {
|
|
64
70
|
stored: "gespeichert",
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type
|
|
3
|
-
export declare const renderFinancialDocument: (input:
|
|
1
|
+
import type { FinancialDocumentRenderInput } from "../types";
|
|
2
|
+
import { type DocumentLayout } from "./layout";
|
|
3
|
+
export declare const renderFinancialDocument: (input: FinancialDocumentRenderInput, layout: DocumentLayout, timeZone: string) => string;
|