@dev.smartpricing/message-composer-layer 4.5.1-next.2 → 5.0.0-automations.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/README.md +132 -3
- package/app/api/automations.ts +74 -0
- package/app/api/index.ts +1 -0
- package/app/api/messageGroups.ts +5 -1
- package/app/app.config.ts +10 -0
- package/app/components/Automations/ActionCard.vue +163 -0
- package/app/components/Automations/ActionEditor.vue +376 -0
- package/app/components/Automations/Actions.vue +141 -0
- package/app/components/Automations/CloneModal.vue +74 -0
- package/app/components/Automations/Conditions.vue +146 -0
- package/app/components/Automations/Create.vue +106 -0
- package/app/components/Automations/Data.vue +46 -0
- package/app/components/Automations/DeleteModal.vue +68 -0
- package/app/components/Automations/Edit.vue +135 -0
- package/app/components/Automations/Form.vue +279 -0
- package/app/components/Automations/List.vue +343 -0
- package/app/components/Automations/PageGate.vue +50 -0
- package/app/components/Automations/SaveProblems.vue +52 -0
- package/app/components/Automations/Unavailable.vue +71 -0
- package/app/components/Automations/UnavailableBadge.vue +35 -0
- package/app/components/Common/MessageGroupSelect.vue +30 -1
- package/app/components/Composer/EmailComposer.vue +5 -1
- package/app/components/Composer/WhatsappComposer.vue +5 -1
- package/app/components/Filters/DateInput.vue +57 -0
- package/app/components/Filters/Editor.vue +163 -0
- package/app/components/Filters/Row.vue +119 -0
- package/app/components/Filters/Summary.vue +215 -0
- package/app/components/Filters/UnavailableRow.vue +74 -0
- package/app/components/Filters/ValueInput.vue +215 -0
- package/app/components/Layout/BasePage.vue +2 -0
- package/app/components/TemplateList/BaseTable.vue +0 -11
- package/app/composables/automationsContext.ts +42 -0
- package/app/composables/composerContext.ts +0 -6
- package/app/composables/useAutomationClone.ts +33 -0
- package/app/composables/useAutomationEditorTrip.ts +77 -0
- package/app/composables/useAutomationSaveProblems.ts +82 -0
- package/app/composables/useAutomationUnavailable.ts +98 -0
- package/app/composables/useAutomationsGate.ts +68 -0
- package/app/composables/useBuiltInOptionLists.ts +44 -0
- package/app/composables/useEditorTripBack.ts +15 -0
- package/app/composables/useMessageGroupWaba.ts +4 -7
- package/app/composables/useTemplateOperations.ts +8 -3
- package/app/middleware/automations-enabled.ts +12 -0
- package/app/pages/automations/[id].vue +85 -0
- package/app/pages/automations/index.vue +27 -0
- package/app/pages/automations/new.vue +51 -0
- package/app/pages/template/email/[id].vue +5 -0
- package/app/pages/template/email/create.vue +5 -0
- package/app/pages/template/whatsapp/[id].vue +5 -0
- package/app/pages/template/whatsapp/create.vue +5 -0
- package/app/queries/automations.ts +206 -0
- package/app/queries/index.ts +1 -0
- package/app/queries/messageGroups.ts +1 -3
- package/app/stores/appContext.ts +0 -3
- package/app/stores/editorTrip.ts +67 -0
- package/app/types/app-config.d.ts +3 -0
- package/app/types/tracking.ts +33 -0
- package/app/utils/automations/form.ts +368 -0
- package/app/utils/automations/metadata.ts +71 -0
- package/app/utils/automations/unavailable.ts +90 -0
- package/app/utils/filters/data/countries.json +998 -0
- package/app/utils/filters/data/languages.json +754 -0
- package/app/utils/filters/rows.ts +367 -0
- package/app/utils/testIds.const.ts +0 -1
- package/i18n/locales/de.ts +268 -1
- package/i18n/locales/en.ts +254 -1
- package/i18n/locales/es.ts +257 -1
- package/i18n/locales/fr.ts +263 -1
- package/i18n/locales/it.ts +259 -1
- package/package.json +4 -4
- package/app.config.ts +0 -5
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { CONDITION_OPERATORS_BY_FIELD_TYPE } from '@dev.smartpricing/message-composer-utils/types'
|
|
2
|
+
import type {
|
|
3
|
+
Condition,
|
|
4
|
+
ConditionFieldType,
|
|
5
|
+
ConditionOperator,
|
|
6
|
+
FilterField,
|
|
7
|
+
FilterOptionValue,
|
|
8
|
+
FilterUnavailableReason,
|
|
9
|
+
FiltersReport,
|
|
10
|
+
} from '@dev.smartpricing/message-composer-utils/types'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The filter components' own model (ADR 0018): a list of Conditions seen as
|
|
14
|
+
* rows, one per field. Internal: "between" as one row, prices in units and
|
|
15
|
+
* the rows themselves never leave the components.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The operators of a row: the engine's, plus "between", a pair of Conditions.
|
|
20
|
+
* There is no "not between": the engine holds all the Conditions of a list
|
|
21
|
+
* together, and no pair of them says "outside this range".
|
|
22
|
+
*/
|
|
23
|
+
export type FilterRowOperator = ConditionOperator | 'between'
|
|
24
|
+
|
|
25
|
+
/** Select: one option. Multiselect: a list. Number, price: a number. Date: `YYYY-MM-DD`. Text: a string. */
|
|
26
|
+
export type FilterRowValue = FilterOptionValue | FilterOptionValue[] | undefined
|
|
27
|
+
|
|
28
|
+
/** A row with no field picked yet. Never emitted, and not half-filled either. */
|
|
29
|
+
export interface FilterBlankRow {
|
|
30
|
+
kind: 'blank'
|
|
31
|
+
id: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface FilterEditableRow {
|
|
35
|
+
kind: 'editable'
|
|
36
|
+
id: number
|
|
37
|
+
field: string
|
|
38
|
+
type: ConditionFieldType
|
|
39
|
+
operator: FilterRowOperator
|
|
40
|
+
/** The value, or the lower bound of "between". A price is in units, not cents. */
|
|
41
|
+
value: FilterRowValue
|
|
42
|
+
/** The upper bound of "between". */
|
|
43
|
+
to: FilterRowValue
|
|
44
|
+
/**
|
|
45
|
+
* The Conditions the row was read from, until the user edits it: an
|
|
46
|
+
* untouched row is emitted exactly as it came in.
|
|
47
|
+
*/
|
|
48
|
+
original?: Condition[]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A Condition the editor cannot edit: shown locked, emitted as it is. */
|
|
52
|
+
export interface FilterUnavailableRow {
|
|
53
|
+
kind: 'unavailable'
|
|
54
|
+
id: number
|
|
55
|
+
reason: FilterUnavailableReason
|
|
56
|
+
condition: Condition
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type FilterRow = FilterBlankRow | FilterEditableRow | FilterUnavailableRow
|
|
60
|
+
|
|
61
|
+
const ORDERED_TYPES: readonly ConditionFieldType[] = ['number', 'price', 'date']
|
|
62
|
+
|
|
63
|
+
function isOrdered(type: ConditionFieldType): boolean {
|
|
64
|
+
return ORDERED_TYPES.includes(type)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The operators a row on a field of this type offers, in display order. */
|
|
68
|
+
export function filterRowOperators(type: ConditionFieldType): FilterRowOperator[] {
|
|
69
|
+
const operators: FilterRowOperator[] = [...CONDITION_OPERATORS_BY_FIELD_TYPE[type]]
|
|
70
|
+
if (!isOrdered(type)) return operators
|
|
71
|
+
// "Is set" stays last.
|
|
72
|
+
operators.splice(operators.indexOf('exists'), 0, 'between')
|
|
73
|
+
return operators
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function isFilterRange(operator: FilterRowOperator): boolean {
|
|
77
|
+
return operator === 'between'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isOptionValue(value: unknown): value is FilterOptionValue {
|
|
81
|
+
return typeof value === 'string' || (typeof value === 'number' && Number.isFinite(value))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isIsoDate(value: unknown): value is string {
|
|
85
|
+
if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return false
|
|
86
|
+
const date = new Date(`${value}T00:00:00Z`)
|
|
87
|
+
// "2026-02-31" parses, as the 3rd of March.
|
|
88
|
+
return !Number.isNaN(date.getTime()) && date.toISOString().startsWith(value)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The row value of a stored one, or `undefined` when it does not fit the type. */
|
|
92
|
+
function readValue(type: ConditionFieldType, value: unknown): FilterRowValue {
|
|
93
|
+
switch (type) {
|
|
94
|
+
case 'select':
|
|
95
|
+
return isOptionValue(value) ? value : undefined
|
|
96
|
+
case 'multiselect':
|
|
97
|
+
return Array.isArray(value) && value.every(isOptionValue) ? [...value] : undefined
|
|
98
|
+
case 'number':
|
|
99
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined
|
|
100
|
+
case 'price':
|
|
101
|
+
return typeof value === 'number' && Number.isFinite(value) ? value / 100 : undefined
|
|
102
|
+
case 'date':
|
|
103
|
+
return isIsoDate(value) ? value : undefined
|
|
104
|
+
case 'text':
|
|
105
|
+
return typeof value === 'string' ? value : undefined
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function writeValue(type: ConditionFieldType, value: FilterRowValue): unknown {
|
|
110
|
+
return type === 'price' ? Math.round((value as number) * 100) : value
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isComplete(type: ConditionFieldType, value: FilterRowValue): boolean {
|
|
114
|
+
switch (type) {
|
|
115
|
+
case 'select':
|
|
116
|
+
return isOptionValue(value) && value !== ''
|
|
117
|
+
case 'multiselect':
|
|
118
|
+
return Array.isArray(value) && value.length > 0
|
|
119
|
+
case 'number':
|
|
120
|
+
case 'price':
|
|
121
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
122
|
+
case 'date':
|
|
123
|
+
return isIsoDate(value)
|
|
124
|
+
case 'text':
|
|
125
|
+
return typeof value === 'string' && value !== ''
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Numbers compare as numbers, ISO dates as strings. */
|
|
130
|
+
function isAtMost(a: FilterRowValue, b: FilterRowValue): boolean {
|
|
131
|
+
return (a as number) <= (b as number)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
type RowContent = Pick<FilterEditableRow, 'operator' | 'value' | 'to'>
|
|
135
|
+
|
|
136
|
+
function readSingle(field: FilterField, condition: Condition): RowContent | undefined {
|
|
137
|
+
const operators: readonly ConditionOperator[] = CONDITION_OPERATORS_BY_FIELD_TYPE[field.type]
|
|
138
|
+
if (!operators.includes(condition.operator)) return undefined
|
|
139
|
+
if (condition.operator === 'exists') {
|
|
140
|
+
return { operator: 'exists', value: undefined, to: undefined }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const value = readValue(field.type, condition.value)
|
|
144
|
+
if (value === undefined) return undefined
|
|
145
|
+
return { operator: condition.operator, value, to: undefined }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* "Between" is `gte` the lower bound and `lte` the upper one. A `gte` above
|
|
150
|
+
* its `lte` is no range: the two are read one by one.
|
|
151
|
+
*/
|
|
152
|
+
function readPair(
|
|
153
|
+
field: FilterField,
|
|
154
|
+
first: Condition | undefined,
|
|
155
|
+
second: Condition | undefined,
|
|
156
|
+
): RowContent | undefined {
|
|
157
|
+
if (!first || !second || !isOrdered(field.type)) return undefined
|
|
158
|
+
|
|
159
|
+
const gte = [first, second].find((condition) => condition.operator === 'gte')
|
|
160
|
+
const lte = [first, second].find((condition) => condition.operator === 'lte')
|
|
161
|
+
if (!gte || !lte) return undefined
|
|
162
|
+
|
|
163
|
+
const from = readValue(field.type, gte.value)
|
|
164
|
+
const until = readValue(field.type, lte.value)
|
|
165
|
+
if (from === undefined || until === undefined) return undefined
|
|
166
|
+
|
|
167
|
+
return isAtMost(from, until) ? { operator: 'between', value: from, to: until } : undefined
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The rows of a list of Conditions, in the order the Conditions are in. No
|
|
172
|
+
* Condition is left out: one the editor cannot edit is an unavailable row.
|
|
173
|
+
* A field gets one editable row, from its first two Conditions when they are
|
|
174
|
+
* a "between", else from the first one that can be edited.
|
|
175
|
+
*/
|
|
176
|
+
export function conditionsToFilterRows(
|
|
177
|
+
conditions: readonly Condition[],
|
|
178
|
+
fields: readonly FilterField[],
|
|
179
|
+
nextId: () => number,
|
|
180
|
+
): FilterRow[] {
|
|
181
|
+
const fieldsByKey = new Map(fields.map((field) => [field.key, field]))
|
|
182
|
+
|
|
183
|
+
const indexesByField = new Map<string, number[]>()
|
|
184
|
+
conditions.forEach((condition, index) => {
|
|
185
|
+
const indexes = indexesByField.get(condition.field) ?? []
|
|
186
|
+
indexes.push(index)
|
|
187
|
+
indexesByField.set(condition.field, indexes)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
// By the index of the Condition a row starts at; `null` for the second of a pair.
|
|
191
|
+
const editable = new Map<number, (RowContent & { original: Condition[] }) | null>()
|
|
192
|
+
for (const [key, indexes] of indexesByField) {
|
|
193
|
+
const field = fieldsByKey.get(key)
|
|
194
|
+
if (!field) continue
|
|
195
|
+
|
|
196
|
+
const [first, second] = indexes as [number, number | undefined]
|
|
197
|
+
const pair = readPair(
|
|
198
|
+
field,
|
|
199
|
+
conditions[first],
|
|
200
|
+
second === undefined ? undefined : conditions[second],
|
|
201
|
+
)
|
|
202
|
+
if (pair && second !== undefined) {
|
|
203
|
+
editable.set(first, { ...pair, original: [conditions[first]!, conditions[second]!] })
|
|
204
|
+
editable.set(second, null)
|
|
205
|
+
continue
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for (const index of indexes) {
|
|
209
|
+
const single = readSingle(field, conditions[index]!)
|
|
210
|
+
if (!single) continue
|
|
211
|
+
editable.set(index, { ...single, original: [conditions[index]!] })
|
|
212
|
+
break
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const rows: FilterRow[] = []
|
|
217
|
+
conditions.forEach((condition, index) => {
|
|
218
|
+
const content = editable.get(index)
|
|
219
|
+
if (content === null) return
|
|
220
|
+
|
|
221
|
+
const field = fieldsByKey.get(condition.field)
|
|
222
|
+
if (content && field) {
|
|
223
|
+
rows.push({ kind: 'editable', id: nextId(), field: field.key, type: field.type, ...content })
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
rows.push({
|
|
227
|
+
kind: 'unavailable',
|
|
228
|
+
id: nextId(),
|
|
229
|
+
reason: field ? 'not_editable' : 'unknown_field',
|
|
230
|
+
condition,
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
return rows
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** The Conditions of an editable row; `null` while it is half-filled. */
|
|
237
|
+
export function filterRowToConditions(row: FilterEditableRow): Condition[] | null {
|
|
238
|
+
if (row.original) return row.original
|
|
239
|
+
|
|
240
|
+
const { field, type, operator } = row
|
|
241
|
+
if (operator === 'exists') return [{ field, operator }]
|
|
242
|
+
|
|
243
|
+
if (!isFilterRange(operator)) {
|
|
244
|
+
if (!isComplete(type, row.value)) return null
|
|
245
|
+
return [{ field, operator: operator as ConditionOperator, value: writeValue(type, row.value) }]
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!isComplete(type, row.value) || !isComplete(type, row.to)) return null
|
|
249
|
+
// Whichever way round the user typed them.
|
|
250
|
+
const [lower, upper] = isAtMost(row.value, row.to) ? [row.value, row.to] : [row.to, row.value]
|
|
251
|
+
return [
|
|
252
|
+
{ field, operator: 'gte', value: writeValue(type, lower) },
|
|
253
|
+
{ field, operator: 'lte', value: writeValue(type, upper) },
|
|
254
|
+
]
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** What the rows are worth as Conditions. Blank and half-filled rows are left out. */
|
|
258
|
+
export function filterRowsToConditions(rows: readonly FilterRow[]): Condition[] {
|
|
259
|
+
return rows.flatMap((row) => {
|
|
260
|
+
if (row.kind === 'unavailable') return [row.condition]
|
|
261
|
+
if (row.kind === 'editable') return filterRowToConditions(row) ?? []
|
|
262
|
+
return []
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** The option values a row holds that its field's `options` do not list. */
|
|
267
|
+
export function missingFilterOptions(
|
|
268
|
+
row: FilterEditableRow,
|
|
269
|
+
field: FilterField | undefined,
|
|
270
|
+
): FilterOptionValue[] {
|
|
271
|
+
if (row.operator === 'exists') return []
|
|
272
|
+
if (row.type !== 'select' && row.type !== 'multiselect') return []
|
|
273
|
+
|
|
274
|
+
const known = new Set((field?.options ?? []).map((option) => option.value))
|
|
275
|
+
const selected = Array.isArray(row.value) ? row.value : [row.value]
|
|
276
|
+
return selected.filter(
|
|
277
|
+
(value): value is FilterOptionValue => value !== undefined && value !== '' && !known.has(value),
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function reportFilterRows(
|
|
282
|
+
rows: readonly FilterRow[],
|
|
283
|
+
fields: readonly FilterField[],
|
|
284
|
+
): FiltersReport {
|
|
285
|
+
const fieldsByKey = new Map(fields.map((field) => [field.key, field]))
|
|
286
|
+
const report: FiltersReport = {
|
|
287
|
+
unknownFields: [],
|
|
288
|
+
unavailable: [],
|
|
289
|
+
unavailableOptions: [],
|
|
290
|
+
halfFilled: 0,
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
for (const row of rows) {
|
|
294
|
+
if (row.kind === 'unavailable') {
|
|
295
|
+
report.unavailable.push({ condition: row.condition, reason: row.reason })
|
|
296
|
+
const { field } = row.condition
|
|
297
|
+
if (row.reason === 'unknown_field' && !report.unknownFields.includes(field)) {
|
|
298
|
+
report.unknownFields.push(field)
|
|
299
|
+
}
|
|
300
|
+
} else if (row.kind === 'editable') {
|
|
301
|
+
if (filterRowToConditions(row) === null) report.halfFilled += 1
|
|
302
|
+
for (const value of missingFilterOptions(row, fieldsByKey.get(row.field))) {
|
|
303
|
+
report.unavailableOptions.push({ field: row.field, value })
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return report
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** A row on a field the user just picked: its first operator, no value. */
|
|
311
|
+
export function newFilterRow(id: number, field: FilterField): FilterEditableRow {
|
|
312
|
+
return {
|
|
313
|
+
kind: 'editable',
|
|
314
|
+
id,
|
|
315
|
+
field: field.key,
|
|
316
|
+
type: field.type,
|
|
317
|
+
operator: filterRowOperators(field.type)[0]!,
|
|
318
|
+
value: field.type === 'multiselect' ? [] : undefined,
|
|
319
|
+
to: undefined,
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** The row under another operator: a value is kept wherever it still means something. */
|
|
324
|
+
export function withFilterOperator(
|
|
325
|
+
row: FilterEditableRow,
|
|
326
|
+
operator: FilterRowOperator,
|
|
327
|
+
): FilterEditableRow {
|
|
328
|
+
return {
|
|
329
|
+
...row,
|
|
330
|
+
operator,
|
|
331
|
+
to: isFilterRange(operator) ? row.to : undefined,
|
|
332
|
+
original: undefined,
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** The locale key of an operator's label, under `filters.operators`. */
|
|
337
|
+
export const FILTER_OPERATOR_LABEL_KEYS: Record<FilterRowOperator, string> = {
|
|
338
|
+
eq: 'is',
|
|
339
|
+
neq: 'is_not',
|
|
340
|
+
gt: 'is_more',
|
|
341
|
+
gte: 'is_at_least',
|
|
342
|
+
lt: 'is_less',
|
|
343
|
+
lte: 'is_at_most',
|
|
344
|
+
between: 'is_between',
|
|
345
|
+
in: 'in',
|
|
346
|
+
not_in: 'not_in',
|
|
347
|
+
contains: 'contains',
|
|
348
|
+
exclude: 'does_not_contain',
|
|
349
|
+
exists: 'is_set',
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** A stored value the components cannot label, as it is. */
|
|
353
|
+
export function rawConditionValue(value: unknown): string {
|
|
354
|
+
if (value === undefined) return ''
|
|
355
|
+
return typeof value === 'string' ? value : JSON.stringify(value)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** The currency to show prices in: the caller's, or EUR when `Intl` does not take it. */
|
|
359
|
+
export function filterCurrency(currency: string | undefined): string {
|
|
360
|
+
if (!currency) return 'EUR'
|
|
361
|
+
try {
|
|
362
|
+
new Intl.NumberFormat('en', { style: 'currency', currency })
|
|
363
|
+
return currency
|
|
364
|
+
} catch {
|
|
365
|
+
return 'EUR'
|
|
366
|
+
}
|
|
367
|
+
}
|
|
@@ -519,7 +519,6 @@ export const templateListTableTestIds = {
|
|
|
519
519
|
paginationSummary: 'template-list-table-pagination-summary',
|
|
520
520
|
card: 'template-list-table-card',
|
|
521
521
|
table: 'template-list-table-table',
|
|
522
|
-
loading: 'template-list-table-loading',
|
|
523
522
|
rowName: 'template-list-table-row-name',
|
|
524
523
|
rowInternalBadge: 'template-list-table-row-internal-badge',
|
|
525
524
|
rowCategoryBadge: 'template-list-table-row-category-badge',
|
package/i18n/locales/de.ts
CHANGED
|
@@ -1,4 +1,226 @@
|
|
|
1
1
|
export default {
|
|
2
|
+
automations: {
|
|
3
|
+
action: {
|
|
4
|
+
at: 'Um',
|
|
5
|
+
itIsSent: 'Wird gesendet',
|
|
6
|
+
},
|
|
7
|
+
actions: {
|
|
8
|
+
addChannel: 'Nachricht hinzufügen',
|
|
9
|
+
},
|
|
10
|
+
advanced: 'Erweitert',
|
|
11
|
+
audienceAll: 'Alle Gäste',
|
|
12
|
+
audienceSpecific: 'Spezifisch',
|
|
13
|
+
channel: {
|
|
14
|
+
addTitle: 'Neue Nachricht',
|
|
15
|
+
cannotFindMessage: 'Brauchen Sie eine andere Nachricht?',
|
|
16
|
+
category: 'Nachrichten',
|
|
17
|
+
channelType: 'Nachrichtentyp',
|
|
18
|
+
chooseDays: 'Tage',
|
|
19
|
+
createNew: 'Neu erstellen',
|
|
20
|
+
editTitle: 'Nachricht bearbeiten',
|
|
21
|
+
no_message: {
|
|
22
|
+
description:
|
|
23
|
+
'Wählen Sie eine Nachricht aus oder erstellen Sie eine neue, um hier die Vorschau zu sehen',
|
|
24
|
+
title: 'Keine Vorschau',
|
|
25
|
+
},
|
|
26
|
+
preview: 'Vorschau',
|
|
27
|
+
searchMessagePlaceholder: 'Nach einer Nachricht suchen',
|
|
28
|
+
selectMessage: 'Nachricht',
|
|
29
|
+
selectMessageHint:
|
|
30
|
+
'Nachrichten im Status „Entwurf“ sind in der Liste nicht sichtbar und können nicht ausgewählt werden.',
|
|
31
|
+
selectMessagePlaceholder: 'Wählen Sie eine Nachricht',
|
|
32
|
+
sendingTime: 'Sendezeit',
|
|
33
|
+
when: 'Geplantes Senden',
|
|
34
|
+
whenHint:
|
|
35
|
+
'Diese Planung ist nicht an den Check-out-Aktion in Ihrem Property Management System (PMS) gebunden.',
|
|
36
|
+
},
|
|
37
|
+
channels: {
|
|
38
|
+
email: 'E-Mail',
|
|
39
|
+
whatsapp: 'WhatsApp',
|
|
40
|
+
},
|
|
41
|
+
clone: {
|
|
42
|
+
namePlaceholder: 'Name der Automatisierung',
|
|
43
|
+
title: 'Automatisierung duplizieren',
|
|
44
|
+
},
|
|
45
|
+
delete: {
|
|
46
|
+
confirm:
|
|
47
|
+
'Möchtest du diese Automatisierung löschen? Nach dem Löschen kann sie nicht wiederhergestellt werden.',
|
|
48
|
+
messages: 'Auch ihre Nachrichten löschen',
|
|
49
|
+
title: 'Automatisierung löschen',
|
|
50
|
+
warning:
|
|
51
|
+
'Eine Nachricht kann von anderen Automatisierungen verwendet werden: Beim Löschen wird sie auch dort entfernt.',
|
|
52
|
+
},
|
|
53
|
+
editTitle: 'Automatisierung bearbeiten',
|
|
54
|
+
form: {
|
|
55
|
+
actions: {
|
|
56
|
+
deleteConfirm: 'Nachricht löschen',
|
|
57
|
+
deleteConfirmDescription:
|
|
58
|
+
'Sind Sie sicher, dass Sie die Nachricht löschen möchten? Sie können sie jederzeit wieder hinzufügen.',
|
|
59
|
+
empty:
|
|
60
|
+
'Es wurde noch keine Nachricht hinzugefügt. Klicken Sie auf die Schaltfläche, um eine zu erstellen.',
|
|
61
|
+
emptyHint:
|
|
62
|
+
'Wählen Sie, wie Sie mit Ihrer Zielgruppe kommunizieren, indem Sie die richtigen Nachrichten hinzufügen.',
|
|
63
|
+
},
|
|
64
|
+
addFilters: 'Filter hinzufügen',
|
|
65
|
+
changeTrigger: {
|
|
66
|
+
confirm: 'Ändern',
|
|
67
|
+
message:
|
|
68
|
+
'Wenn Sie den Auslöser ändern, werden Ihre Nachrichten und Zielgruppe zurückgesetzt.',
|
|
69
|
+
title: 'Auslöser ändern?',
|
|
70
|
+
},
|
|
71
|
+
create: 'Erstellen',
|
|
72
|
+
create_specific_audience: 'Eine bestimmte Zielgruppe erstellen',
|
|
73
|
+
create_specific_audience_hint:
|
|
74
|
+
'Nutzen Sie Filter, um die Nachricht an eine zielgerichtete Zielgruppe zu senden.',
|
|
75
|
+
data: {
|
|
76
|
+
requiredBadge: 'Pflichtdaten',
|
|
77
|
+
title: 'Daten',
|
|
78
|
+
},
|
|
79
|
+
error_banner: 'Bitte füllen Sie alle Pflichtfelder aus',
|
|
80
|
+
includeIf: 'Gäste einschließen, wenn...',
|
|
81
|
+
messages: 'Nachrichten',
|
|
82
|
+
name: 'Name',
|
|
83
|
+
namePlaceholder: 'Marketplace bewerben',
|
|
84
|
+
required: 'Erforderlich',
|
|
85
|
+
sendToHint:
|
|
86
|
+
'Nutzen Sie Filter, um die Nachricht an eine zielgerichtete Zielgruppe zu senden.',
|
|
87
|
+
sendTo_all: 'Alle Gäste',
|
|
88
|
+
sendTotitle: 'Zielgruppe',
|
|
89
|
+
title: 'Details',
|
|
90
|
+
trigger: 'Auslöser',
|
|
91
|
+
},
|
|
92
|
+
gate: {
|
|
93
|
+
catalog_failed_description:
|
|
94
|
+
'Beim Laden der für diese Seite benötigten Daten ist ein Fehler aufgetreten. Versuche es erneut.',
|
|
95
|
+
catalog_failed_title: 'Die Automatisierungen konnten nicht geladen werden',
|
|
96
|
+
load_failed_description:
|
|
97
|
+
'Beim Laden dieser Automatisierung ist ein Fehler aufgetreten. Versuche es erneut.',
|
|
98
|
+
load_failed_title: 'Die Automatisierung konnte nicht geladen werden',
|
|
99
|
+
no_catalog_description:
|
|
100
|
+
'In diesem Produkt gibt es noch nichts, woraus eine Automatisierung erstellt werden kann. Wende dich an den Support, wenn du sie hier erwartet hast.',
|
|
101
|
+
no_catalog_title: 'Automatisierungen sind hier nicht verfügbar',
|
|
102
|
+
no_context_description:
|
|
103
|
+
'Diese Seite hat nicht erhalten, was sie zum Funktionieren braucht. Lade die Seite neu; wenn es erneut passiert, wende dich an den Support.',
|
|
104
|
+
no_context_title: 'Automatisierungen können nicht angezeigt werden',
|
|
105
|
+
no_organization_description:
|
|
106
|
+
'Automatisierungen gehören zu einer Organisation, und es ist keine ausgewählt.',
|
|
107
|
+
no_organization_title: 'Keine Organisation zum Arbeiten',
|
|
108
|
+
not_found_description:
|
|
109
|
+
'Sie wurde möglicherweise gelöscht oder gehört zu einer anderen Organisation oder einem anderen Produkt.',
|
|
110
|
+
not_found_title: 'Automatisierung nicht gefunden',
|
|
111
|
+
},
|
|
112
|
+
immediately: 'Sofort {moment}',
|
|
113
|
+
list: {
|
|
114
|
+
active: 'Aktiv',
|
|
115
|
+
allChannels: 'Alle Nachrichten',
|
|
116
|
+
allStatuses: 'Alle Status',
|
|
117
|
+
allTriggers: 'Alle Auslöser',
|
|
118
|
+
empty: 'Keine Automatisierungen gefunden',
|
|
119
|
+
headers: {
|
|
120
|
+
audience: 'Zielgruppe',
|
|
121
|
+
channels: 'Nachrichten',
|
|
122
|
+
createdAt: 'Erstellt am',
|
|
123
|
+
name: 'Name',
|
|
124
|
+
status: 'Status',
|
|
125
|
+
trigger: 'Auslöser',
|
|
126
|
+
},
|
|
127
|
+
inactive: 'Inaktiv',
|
|
128
|
+
search: 'Nach Namen suchen',
|
|
129
|
+
},
|
|
130
|
+
nDays: '{count} Tage {moment}',
|
|
131
|
+
new: 'Neue Automatisierung',
|
|
132
|
+
newTitle: 'Automatisierung erstellen',
|
|
133
|
+
oneDay: '1 Tag {moment}',
|
|
134
|
+
preventDuplicates: {
|
|
135
|
+
hint: 'Senden überspringen, wenn diese Nachricht bereits an denselben Empfänger zur selben Zeit gesendet oder geplant wurde',
|
|
136
|
+
label: 'Doppelte Nachrichten verhindern',
|
|
137
|
+
},
|
|
138
|
+
problems: {
|
|
139
|
+
action_channel_not_in_catalog:
|
|
140
|
+
'"{name}": Dieser Nachrichtentyp kann mit diesem Auslöser nicht verwendet werden.',
|
|
141
|
+
action_date_reference_not_in_catalog:
|
|
142
|
+
'"{name}": Der Zeitpunkt, zu dem diese Nachricht geplant ist, ist nicht mehr verfügbar. Wählen Sie einen anderen.',
|
|
143
|
+
action_invalid:
|
|
144
|
+
'"{name}": Diese Nachricht ist aktiv, aber nicht in jeder Sprache genehmigt. Deaktivieren Sie sie oder wählen Sie eine genehmigte Nachricht, um zu speichern.',
|
|
145
|
+
action_not_found: '"{name}": Diese Nachricht existiert nicht mehr. Wählen Sie eine andere.',
|
|
146
|
+
action_other_organization:
|
|
147
|
+
'"{name}": Diese Nachricht gehört zu einer anderen Organisation. Wählen Sie eine andere.',
|
|
148
|
+
condition_field_not_in_catalog:
|
|
149
|
+
'Ein Filter bezieht sich auf "{field}", das nicht mehr verfügbar ist. Entfernen Sie ihn, um zu speichern.',
|
|
150
|
+
condition_operator_not_allowed:
|
|
151
|
+
'Der Filter auf "{field}" verwendet einen Vergleich, der hier nicht möglich ist. Ändern oder entfernen Sie ihn, um zu speichern.',
|
|
152
|
+
show: 'Anzeigen',
|
|
153
|
+
title: 'Diese Automatisierung kann noch nicht gespeichert werden',
|
|
154
|
+
trigger_not_in_catalog: 'Der Auslöser ist nicht mehr verfügbar. Wählen Sie einen anderen.',
|
|
155
|
+
unknown:
|
|
156
|
+
'Etwas in dieser Automatisierung kann nicht gespeichert werden. Bitte prüfen und erneut versuchen.',
|
|
157
|
+
},
|
|
158
|
+
recurrence: {
|
|
159
|
+
daily: 'Täglich',
|
|
160
|
+
label: 'Wiederholung',
|
|
161
|
+
monthly: 'Monatlich',
|
|
162
|
+
none: 'Keine',
|
|
163
|
+
yearly: 'Jährlich',
|
|
164
|
+
},
|
|
165
|
+
strictDelay: {
|
|
166
|
+
hint: 'Nachrichten überspringen, wenn die geplante Zeit bereits verstrichen ist',
|
|
167
|
+
label: 'Strikte Verzögerung',
|
|
168
|
+
},
|
|
169
|
+
system: {
|
|
170
|
+
alert:
|
|
171
|
+
'Dies ist eine Systemautomatisierung. Sie können nur ihre Nachrichten aktivieren oder deaktivieren und die angeforderten Daten ausfüllen. Duplizieren Sie sie, um eine vollständig bearbeitbare Kopie zu erstellen.',
|
|
172
|
+
badge: 'Systemautomatisierung',
|
|
173
|
+
tooltip: 'Nur die Nachrichtenaktivierung und die angeforderten Daten können geändert werden',
|
|
174
|
+
},
|
|
175
|
+
title: 'Automatisierungen',
|
|
176
|
+
toast: {
|
|
177
|
+
cloned: 'Automatisierung erfolgreich dupliziert',
|
|
178
|
+
created: 'Automatisierung erfolgreich erstellt',
|
|
179
|
+
deleted: 'Automatisierung erfolgreich gelöscht',
|
|
180
|
+
deleted_messages: 'Mit ihr gelöschte Nachrichten: {names}',
|
|
181
|
+
kept_messages: 'Nachrichten, die nicht gelöscht werden konnten und erhalten bleiben: {names}',
|
|
182
|
+
partly_saved: 'Die Automatisierung wurde nur teilweise gespeichert',
|
|
183
|
+
partly_saved_description:
|
|
184
|
+
'Deine Änderungen sind noch da. Speichere erneut, um sie abzuschließen.',
|
|
185
|
+
updated: 'Automatisierung erfolgreich aktualisiert',
|
|
186
|
+
},
|
|
187
|
+
trip: {
|
|
188
|
+
back: 'Zurück zur Automatisierung',
|
|
189
|
+
kept: 'Deine nicht gespeicherten Änderungen wurden beibehalten',
|
|
190
|
+
},
|
|
191
|
+
unavailable: {
|
|
192
|
+
badges: {
|
|
193
|
+
not_approved: 'Nicht genehmigt',
|
|
194
|
+
not_set_up: 'Nicht eingerichtet',
|
|
195
|
+
unavailable: 'Nicht verfügbar',
|
|
196
|
+
},
|
|
197
|
+
channel:
|
|
198
|
+
'{channel} ist für diese Organisation nicht eingerichtet. Diese Nachricht bleibt erhalten, wird aber nicht gesendet.',
|
|
199
|
+
channel_option: '{channel} (nicht eingerichtet)',
|
|
200
|
+
condition_not_editable:
|
|
201
|
+
'Dieser Filter kann hier nicht bearbeitet werden. Er gilt weiterhin: Behalte ihn oder entferne ihn.',
|
|
202
|
+
condition_unknown_field:
|
|
203
|
+
'Dieser Filter ist nicht mehr verfügbar. Entferne ihn, um zu speichern.',
|
|
204
|
+
date_reference:
|
|
205
|
+
'Das Datum, auf das diese Nachricht geplant ist, ist nicht mehr verfügbar. Wähle ein anderes, um zu speichern.',
|
|
206
|
+
kept: 'Bleiben unverändert erhalten',
|
|
207
|
+
message_group_active:
|
|
208
|
+
'Diese Nachricht ist aktiv, aber nicht in jeder Sprache genehmigt. Deaktiviere sie oder wähle eine genehmigte Nachricht, um zu speichern.',
|
|
209
|
+
message_group_inactive:
|
|
210
|
+
'Diese Nachricht ist noch nicht in jeder Sprache genehmigt. Sie kann aktiviert werden, sobald sie es ist.',
|
|
211
|
+
option:
|
|
212
|
+
'Eine ausgewählte Option ist nicht mehr verfügbar. Sie gilt weiterhin und bleibt erhalten, bis du sie entfernst.',
|
|
213
|
+
title_blocked: 'Diese Automatisierung kann noch nicht gespeichert werden',
|
|
214
|
+
title_kept: 'Einige Teile dieser Automatisierung sind nicht verfügbar',
|
|
215
|
+
to_fix: 'Vor dem Speichern zu beheben',
|
|
216
|
+
trigger:
|
|
217
|
+
'Dieser Auslöser ist nicht mehr verfügbar. Wähle einen anderen, um zu speichern; deine Nachrichten und deine Zielgruppe werden zurückgesetzt.',
|
|
218
|
+
where: {
|
|
219
|
+
filter: 'Filter "{field}"',
|
|
220
|
+
message: 'Nachricht "{name}"',
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
2
224
|
categories: {
|
|
3
225
|
marketing: 'Marketing',
|
|
4
226
|
utility: 'Service',
|
|
@@ -642,6 +864,52 @@ export default {
|
|
|
642
864
|
no_preview: 'Keine Vorschau verfügbar',
|
|
643
865
|
subject: 'Betreff',
|
|
644
866
|
},
|
|
867
|
+
filters: {
|
|
868
|
+
actions: {
|
|
869
|
+
add_filter: 'Filter hinzufügen',
|
|
870
|
+
open_calendar: 'Kalender öffnen',
|
|
871
|
+
remove_filter: 'Filter entfernen',
|
|
872
|
+
remove_option: 'Option entfernen',
|
|
873
|
+
},
|
|
874
|
+
and: 'und',
|
|
875
|
+
applied: '{count} Filter angewendet',
|
|
876
|
+
field_placeholder: 'Feld auswählen',
|
|
877
|
+
operators: {
|
|
878
|
+
contains: 'Enthält',
|
|
879
|
+
does_not_contain: 'Enthält nicht',
|
|
880
|
+
in: 'In',
|
|
881
|
+
is: 'Ist',
|
|
882
|
+
is_at_least: 'Ist mindestens',
|
|
883
|
+
is_at_most: 'Ist höchstens',
|
|
884
|
+
is_between: 'Ist zwischen',
|
|
885
|
+
is_empty: 'Ist leer',
|
|
886
|
+
is_less: 'Ist kleiner als',
|
|
887
|
+
is_more: 'Ist größer als',
|
|
888
|
+
is_not: 'Ist nicht',
|
|
889
|
+
is_not_between: 'Ist nicht zwischen',
|
|
890
|
+
is_not_empty: 'Ist nicht leer',
|
|
891
|
+
is_set: 'Ist gesetzt',
|
|
892
|
+
not_in: 'Nicht in',
|
|
893
|
+
},
|
|
894
|
+
selected: '{count} ausgewählt',
|
|
895
|
+
types: {
|
|
896
|
+
date: 'Datum',
|
|
897
|
+
multiselect: 'Mehrfachauswahl',
|
|
898
|
+
number: 'Zahl',
|
|
899
|
+
price: 'Preis',
|
|
900
|
+
select: 'Einfachauswahl',
|
|
901
|
+
text: 'Text',
|
|
902
|
+
},
|
|
903
|
+
unavailable: {
|
|
904
|
+
badge: 'Nicht verfügbar',
|
|
905
|
+
not_editable:
|
|
906
|
+
'Dieser Filter kann hier nicht bearbeitet werden. Er gilt weiterhin: Behalte ihn oder entferne ihn.',
|
|
907
|
+
option: 'Option nicht verfügbar',
|
|
908
|
+
option_description:
|
|
909
|
+
'Eine ausgewählte Option ist nicht mehr verfügbar. Sie gilt weiterhin und bleibt erhalten, bis du sie entfernst.',
|
|
910
|
+
unknown_field: 'Dieser Filter ist nicht mehr verfügbar.',
|
|
911
|
+
},
|
|
912
|
+
},
|
|
645
913
|
language: {
|
|
646
914
|
de: 'Deutsch',
|
|
647
915
|
en: 'Englisch',
|
|
@@ -825,7 +1093,6 @@ export default {
|
|
|
825
1093
|
status: 'Status',
|
|
826
1094
|
updated_at: 'Aktualisiert am',
|
|
827
1095
|
},
|
|
828
|
-
loading: 'Nachrichten werden geladen...',
|
|
829
1096
|
pagination_summary: '{from}–{to} von {total}',
|
|
830
1097
|
product_placeholder: 'Produkt auswählen',
|
|
831
1098
|
render_type_placeholder: 'Render-Typ auswählen',
|