@open-mercato/core 0.6.2-develop.3330.1.8ad2b3b84c → 0.6.2-develop.3366.1.4692954e44

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 (102) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/sync_excel_upload/index.js +37 -0
  3. package/dist/generated/entities/sync_excel_upload/index.js.map +7 -0
  4. package/dist/generated/entities.ids.generated.js +4 -0
  5. package/dist/generated/entities.ids.generated.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +19 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/customers/search.js +3 -2
  9. package/dist/modules/customers/search.js.map +2 -2
  10. package/dist/modules/data_sync/api/runs/[id]/cancel.js +16 -0
  11. package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +2 -2
  12. package/dist/modules/data_sync/di.js +2 -1
  13. package/dist/modules/data_sync/di.js.map +2 -2
  14. package/dist/modules/data_sync/lib/sync-engine.js +178 -54
  15. package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
  16. package/dist/modules/data_sync/lib/sync-run-service.js +23 -0
  17. package/dist/modules/data_sync/lib/sync-run-service.js.map +3 -3
  18. package/dist/modules/integrations/backend/integrations/[id]/page.js +241 -55
  19. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +3 -3
  20. package/dist/modules/sync_excel/acl.js +10 -0
  21. package/dist/modules/sync_excel/acl.js.map +7 -0
  22. package/dist/modules/sync_excel/api/import/route.js +164 -0
  23. package/dist/modules/sync_excel/api/import/route.js.map +7 -0
  24. package/dist/modules/sync_excel/api/preview/route.js +84 -0
  25. package/dist/modules/sync_excel/api/preview/route.js.map +7 -0
  26. package/dist/modules/sync_excel/api/upload/route.js +131 -0
  27. package/dist/modules/sync_excel/api/upload/route.js.map +7 -0
  28. package/dist/modules/sync_excel/data/entities.js +79 -0
  29. package/dist/modules/sync_excel/data/entities.js.map +7 -0
  30. package/dist/modules/sync_excel/data/validators.js +58 -0
  31. package/dist/modules/sync_excel/data/validators.js.map +7 -0
  32. package/dist/modules/sync_excel/di.js +11 -0
  33. package/dist/modules/sync_excel/di.js.map +7 -0
  34. package/dist/modules/sync_excel/index.js +11 -0
  35. package/dist/modules/sync_excel/index.js.map +7 -0
  36. package/dist/modules/sync_excel/integration.js +36 -0
  37. package/dist/modules/sync_excel/integration.js.map +7 -0
  38. package/dist/modules/sync_excel/lib/adapters/customers.js +852 -0
  39. package/dist/modules/sync_excel/lib/adapters/customers.js.map +7 -0
  40. package/dist/modules/sync_excel/lib/column-detector.js +77 -0
  41. package/dist/modules/sync_excel/lib/column-detector.js.map +7 -0
  42. package/dist/modules/sync_excel/lib/mapping.js +16 -0
  43. package/dist/modules/sync_excel/lib/mapping.js.map +7 -0
  44. package/dist/modules/sync_excel/lib/parser.js +125 -0
  45. package/dist/modules/sync_excel/lib/parser.js.map +7 -0
  46. package/dist/modules/sync_excel/lib/scope.js +44 -0
  47. package/dist/modules/sync_excel/lib/scope.js.map +7 -0
  48. package/dist/modules/sync_excel/lib/upload-storage.js +63 -0
  49. package/dist/modules/sync_excel/lib/upload-storage.js.map +7 -0
  50. package/dist/modules/sync_excel/migrations/Migration20260329181120_sync_excel.js +11 -0
  51. package/dist/modules/sync_excel/migrations/Migration20260329181120_sync_excel.js.map +7 -0
  52. package/dist/modules/sync_excel/setup.js +19 -0
  53. package/dist/modules/sync_excel/setup.js.map +7 -0
  54. package/dist/modules/sync_excel/widgets/injection/upload-config/target-options.js +293 -0
  55. package/dist/modules/sync_excel/widgets/injection/upload-config/target-options.js.map +7 -0
  56. package/dist/modules/sync_excel/widgets/injection/upload-config/widget.client.js +934 -0
  57. package/dist/modules/sync_excel/widgets/injection/upload-config/widget.client.js.map +7 -0
  58. package/dist/modules/sync_excel/widgets/injection/upload-config/widget.js +15 -0
  59. package/dist/modules/sync_excel/widgets/injection/upload-config/widget.js.map +7 -0
  60. package/dist/modules/sync_excel/widgets/injection-table.js +17 -0
  61. package/dist/modules/sync_excel/widgets/injection-table.js.map +7 -0
  62. package/generated/entities/sync_excel_upload/index.ts +17 -0
  63. package/generated/entities.ids.generated.ts +4 -0
  64. package/generated/entity-fields-registry.ts +19 -0
  65. package/package.json +7 -7
  66. package/src/modules/customers/search.ts +3 -2
  67. package/src/modules/data_sync/api/runs/[id]/cancel.ts +18 -0
  68. package/src/modules/data_sync/di.ts +4 -1
  69. package/src/modules/data_sync/lib/adapter.ts +15 -0
  70. package/src/modules/data_sync/lib/sync-engine.ts +205 -52
  71. package/src/modules/data_sync/lib/sync-run-service.ts +24 -0
  72. package/src/modules/integrations/backend/integrations/[id]/page.tsx +220 -15
  73. package/src/modules/integrations/i18n/de.json +9 -0
  74. package/src/modules/integrations/i18n/en.json +9 -0
  75. package/src/modules/integrations/i18n/es.json +9 -0
  76. package/src/modules/integrations/i18n/pl.json +9 -0
  77. package/src/modules/sync_excel/acl.ts +6 -0
  78. package/src/modules/sync_excel/api/import/route.ts +182 -0
  79. package/src/modules/sync_excel/api/preview/route.ts +89 -0
  80. package/src/modules/sync_excel/api/upload/route.ts +144 -0
  81. package/src/modules/sync_excel/data/entities.ts +61 -0
  82. package/src/modules/sync_excel/data/validators.ts +58 -0
  83. package/src/modules/sync_excel/di.ts +9 -0
  84. package/src/modules/sync_excel/i18n/de.json +107 -0
  85. package/src/modules/sync_excel/i18n/en.json +107 -0
  86. package/src/modules/sync_excel/i18n/es.json +107 -0
  87. package/src/modules/sync_excel/i18n/pl.json +107 -0
  88. package/src/modules/sync_excel/index.ts +7 -0
  89. package/src/modules/sync_excel/integration.ts +31 -0
  90. package/src/modules/sync_excel/lib/adapters/customers.ts +1097 -0
  91. package/src/modules/sync_excel/lib/column-detector.ts +91 -0
  92. package/src/modules/sync_excel/lib/mapping.ts +19 -0
  93. package/src/modules/sync_excel/lib/parser.ts +165 -0
  94. package/src/modules/sync_excel/lib/scope.ts +63 -0
  95. package/src/modules/sync_excel/lib/upload-storage.ts +79 -0
  96. package/src/modules/sync_excel/migrations/.snapshot-open-mercato.json +199 -0
  97. package/src/modules/sync_excel/migrations/Migration20260329181120_sync_excel.ts +10 -0
  98. package/src/modules/sync_excel/setup.ts +20 -0
  99. package/src/modules/sync_excel/widgets/injection/upload-config/target-options.ts +351 -0
  100. package/src/modules/sync_excel/widgets/injection/upload-config/widget.client.tsx +1224 -0
  101. package/src/modules/sync_excel/widgets/injection/upload-config/widget.ts +30 -0
  102. package/src/modules/sync_excel/widgets/injection-table.ts +15 -0
@@ -0,0 +1,351 @@
1
+ import type { CustomFieldDefDto } from '@open-mercato/ui/backend/utils/customFieldDefs'
2
+ import type { FieldMapping, FieldMappingDedupeRole, FieldMappingKind } from '../../../../data_sync/lib/adapter'
3
+
4
+ export const SYNC_EXCEL_PEOPLE_CUSTOM_FIELD_ENTITY_IDS = [
5
+ 'customers:customer_entity',
6
+ 'customers:customer_person_profile',
7
+ ] as const
8
+
9
+ export type SuggestedMapping = {
10
+ entityType: 'customers.person'
11
+ matchStrategy: 'externalId' | 'email' | 'custom'
12
+ matchField?: string
13
+ fields: FieldMapping[]
14
+ unmappedColumns: string[]
15
+ }
16
+
17
+ export type MappingTargetOption = {
18
+ value: string
19
+ labelKey?: string
20
+ fallback: string
21
+ mappingKind: FieldMappingKind
22
+ dedupeRole?: FieldMappingDedupeRole
23
+ matchTokens: string[]
24
+ }
25
+
26
+ const CORE_TARGET_OPTIONS: MappingTargetOption[] = [
27
+ {
28
+ value: 'person.externalId',
29
+ labelKey: 'sync_excel.mapping.targets.externalId',
30
+ fallback: 'External ID',
31
+ mappingKind: 'external_id',
32
+ dedupeRole: 'primary',
33
+ matchTokens: ['external id', 'record id', 'lead id'],
34
+ },
35
+ {
36
+ value: 'person.firstName',
37
+ labelKey: 'sync_excel.mapping.targets.firstName',
38
+ fallback: 'First name',
39
+ mappingKind: 'core',
40
+ matchTokens: ['first name', 'firstname', 'given name'],
41
+ },
42
+ {
43
+ value: 'person.lastName',
44
+ labelKey: 'sync_excel.mapping.targets.lastName',
45
+ fallback: 'Last name',
46
+ mappingKind: 'core',
47
+ matchTokens: ['last name', 'lastname', 'surname', 'family name'],
48
+ },
49
+ {
50
+ value: 'person.displayName',
51
+ labelKey: 'sync_excel.mapping.targets.displayName',
52
+ fallback: 'Display name',
53
+ mappingKind: 'core',
54
+ matchTokens: ['display name', 'lead name', 'full name', 'name'],
55
+ },
56
+ {
57
+ value: 'person.primaryEmail',
58
+ labelKey: 'sync_excel.mapping.targets.primaryEmail',
59
+ fallback: 'Primary email',
60
+ mappingKind: 'core',
61
+ dedupeRole: 'secondary',
62
+ matchTokens: ['email', 'primary email', 'email address'],
63
+ },
64
+ {
65
+ value: 'person.primaryPhone',
66
+ labelKey: 'sync_excel.mapping.targets.primaryPhone',
67
+ fallback: 'Primary phone',
68
+ mappingKind: 'core',
69
+ matchTokens: ['phone', 'primary phone', 'mobile', 'mobile phone', 'telephone'],
70
+ },
71
+ {
72
+ value: 'person.jobTitle',
73
+ labelKey: 'sync_excel.mapping.targets.jobTitle',
74
+ fallback: 'Job title',
75
+ mappingKind: 'core',
76
+ matchTokens: ['job title', 'title', 'position'],
77
+ },
78
+ {
79
+ value: 'person.status',
80
+ labelKey: 'sync_excel.mapping.targets.status',
81
+ fallback: 'Status',
82
+ mappingKind: 'core',
83
+ matchTokens: ['status', 'lead status'],
84
+ },
85
+ {
86
+ value: 'person.source',
87
+ labelKey: 'sync_excel.mapping.targets.source',
88
+ fallback: 'Source',
89
+ mappingKind: 'core',
90
+ matchTokens: ['source', 'lead source'],
91
+ },
92
+ {
93
+ value: 'person.description',
94
+ labelKey: 'sync_excel.mapping.targets.description',
95
+ fallback: 'Description',
96
+ mappingKind: 'core',
97
+ matchTokens: ['description', 'notes', 'comment'],
98
+ },
99
+ ]
100
+
101
+ const ADDRESS_TARGET_OPTIONS: MappingTargetOption[] = [
102
+ {
103
+ value: 'address.name',
104
+ labelKey: 'sync_excel.mapping.targets.addressName',
105
+ fallback: 'Address label',
106
+ mappingKind: 'core',
107
+ matchTokens: ['address label', 'address name', 'label'],
108
+ },
109
+ {
110
+ value: 'address.purpose',
111
+ labelKey: 'sync_excel.mapping.targets.addressPurpose',
112
+ fallback: 'Address purpose',
113
+ mappingKind: 'core',
114
+ matchTokens: ['address purpose', 'address type'],
115
+ },
116
+ {
117
+ value: 'address.companyName',
118
+ labelKey: 'sync_excel.mapping.targets.addressCompanyName',
119
+ fallback: 'Address company',
120
+ mappingKind: 'core',
121
+ matchTokens: ['address company', 'address company name'],
122
+ },
123
+ {
124
+ value: 'address.addressLine1',
125
+ labelKey: 'sync_excel.mapping.targets.addressLine1',
126
+ fallback: 'Address line 1',
127
+ mappingKind: 'core',
128
+ matchTokens: ['address line 1', 'street address', 'street', 'street 1'],
129
+ },
130
+ {
131
+ value: 'address.addressLine2',
132
+ labelKey: 'sync_excel.mapping.targets.addressLine2',
133
+ fallback: 'Address line 2',
134
+ mappingKind: 'core',
135
+ matchTokens: ['address line 2', 'street 2'],
136
+ },
137
+ {
138
+ value: 'address.buildingNumber',
139
+ labelKey: 'sync_excel.mapping.targets.buildingNumber',
140
+ fallback: 'Building number',
141
+ mappingKind: 'core',
142
+ matchTokens: ['building number', 'building no', 'house number'],
143
+ },
144
+ {
145
+ value: 'address.flatNumber',
146
+ labelKey: 'sync_excel.mapping.targets.flatNumber',
147
+ fallback: 'Flat number',
148
+ mappingKind: 'core',
149
+ matchTokens: ['flat number', 'apartment number', 'unit number'],
150
+ },
151
+ {
152
+ value: 'address.city',
153
+ labelKey: 'sync_excel.mapping.targets.city',
154
+ fallback: 'City',
155
+ mappingKind: 'core',
156
+ matchTokens: ['city', 'town'],
157
+ },
158
+ {
159
+ value: 'address.region',
160
+ labelKey: 'sync_excel.mapping.targets.region',
161
+ fallback: 'Region / State',
162
+ mappingKind: 'core',
163
+ matchTokens: ['region', 'state', 'province'],
164
+ },
165
+ {
166
+ value: 'address.postalCode',
167
+ labelKey: 'sync_excel.mapping.targets.postalCode',
168
+ fallback: 'Postal code',
169
+ mappingKind: 'core',
170
+ matchTokens: ['postal code', 'zip code', 'postcode'],
171
+ },
172
+ {
173
+ value: 'address.country',
174
+ labelKey: 'sync_excel.mapping.targets.country',
175
+ fallback: 'Country',
176
+ mappingKind: 'core',
177
+ matchTokens: ['country'],
178
+ },
179
+ {
180
+ value: 'address.latitude',
181
+ labelKey: 'sync_excel.mapping.targets.latitude',
182
+ fallback: 'Latitude',
183
+ mappingKind: 'core',
184
+ matchTokens: ['latitude', 'lat'],
185
+ },
186
+ {
187
+ value: 'address.longitude',
188
+ labelKey: 'sync_excel.mapping.targets.longitude',
189
+ fallback: 'Longitude',
190
+ mappingKind: 'core',
191
+ matchTokens: ['longitude', 'lng', 'lon'],
192
+ },
193
+ ]
194
+
195
+ function normalizeMatchToken(value: string): string {
196
+ return value
197
+ .trim()
198
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
199
+ .toLowerCase()
200
+ .replace(/[_-]+/g, ' ')
201
+ .replace(/\s+/g, ' ')
202
+ .trim()
203
+ }
204
+
205
+ function titleizeKey(key: string): string {
206
+ return key
207
+ .split('_')
208
+ .map((part) => (part.length > 0 ? part[0].toUpperCase() + part.slice(1) : part))
209
+ .join(' ')
210
+ }
211
+
212
+ function scoreCustomFieldDefinition(def: CustomFieldDefDto, entityIndex: number): {
213
+ base: number
214
+ penalty: number
215
+ entityIndex: number
216
+ } {
217
+ const listVisibleScore = def.listVisible === false ? 0 : 1
218
+ const formEditableScore = def.formEditable === false ? 0 : 1
219
+ const filterableScore = def.filterable ? 1 : 0
220
+ const kindScore = (() => {
221
+ switch (def.kind) {
222
+ case 'dictionary':
223
+ return 8
224
+ case 'relation':
225
+ return 6
226
+ case 'select':
227
+ return 4
228
+ case 'multiline':
229
+ return 3
230
+ case 'boolean':
231
+ case 'integer':
232
+ case 'float':
233
+ return 2
234
+ default:
235
+ return 1
236
+ }
237
+ })()
238
+ const optionsBonus = Array.isArray(def.options) && def.options.length > 0 ? 2 : 0
239
+ const dictionaryBonus = typeof def.dictionaryId === 'string' && def.dictionaryId.trim().length > 0 ? 5 : 0
240
+ return {
241
+ base: (listVisibleScore * 16) + (formEditableScore * 8) + (filterableScore * 4) + kindScore + optionsBonus + dictionaryBonus,
242
+ penalty: typeof def.priority === 'number' ? def.priority : 0,
243
+ entityIndex,
244
+ }
245
+ }
246
+
247
+ function buildCustomFieldOption(def: CustomFieldDefDto): MappingTargetOption {
248
+ const fallback = typeof def.label === 'string' && def.label.trim().length > 0
249
+ ? def.label.trim()
250
+ : titleizeKey(def.key)
251
+ const normalizedTokens = Array.from(new Set([
252
+ normalizeMatchToken(def.key),
253
+ normalizeMatchToken(fallback),
254
+ ].filter((value) => value.length > 0)))
255
+
256
+ return {
257
+ value: `cf:${def.key}`,
258
+ fallback,
259
+ mappingKind: 'custom_field',
260
+ matchTokens: normalizedTokens,
261
+ }
262
+ }
263
+
264
+ function selectPreferredCustomFieldDefs(customFieldDefs: CustomFieldDefDto[]): CustomFieldDefDto[] {
265
+ const entityOrder = new Map<string, number>()
266
+ SYNC_EXCEL_PEOPLE_CUSTOM_FIELD_ENTITY_IDS.forEach((entityId, index) => entityOrder.set(entityId, index))
267
+ const bestByKey = new Map<string, { def: CustomFieldDefDto; score: { base: number; penalty: number; entityIndex: number } }>()
268
+
269
+ for (const def of customFieldDefs) {
270
+ if (typeof def.key !== 'string' || def.key.trim().length === 0) continue
271
+ const score = scoreCustomFieldDefinition(def, entityOrder.get(def.entityId ?? '') ?? Number.MAX_SAFE_INTEGER)
272
+ const existing = bestByKey.get(def.key)
273
+ const isBetter = !existing
274
+ || score.base > existing.score.base
275
+ || (
276
+ score.base === existing.score.base
277
+ && (score.penalty < existing.score.penalty
278
+ || (score.penalty === existing.score.penalty && score.entityIndex < existing.score.entityIndex))
279
+ )
280
+ if (isBetter) {
281
+ bestByKey.set(def.key, { def, score })
282
+ }
283
+ }
284
+
285
+ return Array.from(bestByKey.values()).map((entry) => entry.def)
286
+ }
287
+
288
+ export function buildPeopleTargetOptions(customFieldDefs: CustomFieldDefDto[]): MappingTargetOption[] {
289
+ const customOptions = selectPreferredCustomFieldDefs(customFieldDefs).map(buildCustomFieldOption)
290
+ return [...CORE_TARGET_OPTIONS, ...ADDRESS_TARGET_OPTIONS, ...customOptions]
291
+ }
292
+
293
+ export function buildPeopleSuggestedMapping(
294
+ headers: string[],
295
+ suggestedMapping: SuggestedMapping,
296
+ customFieldDefs: CustomFieldDefDto[],
297
+ ): SuggestedMapping {
298
+ const fields = [...suggestedMapping.fields]
299
+ const usedExternalFields = new Set(fields.map((field) => field.externalField))
300
+ const usedTargetFields = new Set(fields.map((field) => field.localField))
301
+ const supplementalTargetOptions = buildPeopleTargetOptions(customFieldDefs).filter(
302
+ (option) => option.mappingKind === 'custom_field' || option.value.startsWith('address.'),
303
+ )
304
+
305
+ for (const header of headers) {
306
+ if (usedExternalFields.has(header)) continue
307
+ const normalizedHeader = normalizeMatchToken(header)
308
+ const matchedOption = supplementalTargetOptions.find((option) => {
309
+ if (usedTargetFields.has(option.value)) return false
310
+ return option.matchTokens.includes(normalizedHeader)
311
+ })
312
+ if (!matchedOption) continue
313
+
314
+ fields.push({
315
+ externalField: header,
316
+ localField: matchedOption.value,
317
+ mappingKind: matchedOption.mappingKind,
318
+ })
319
+ usedExternalFields.add(header)
320
+ usedTargetFields.add(matchedOption.value)
321
+ }
322
+
323
+ return {
324
+ ...suggestedMapping,
325
+ fields,
326
+ unmappedColumns: headers.filter((header) => !usedExternalFields.has(header)),
327
+ }
328
+ }
329
+
330
+ export function buildSuggestedMappingSignature(headers: string[], suggestedMapping: SuggestedMapping): string {
331
+ return JSON.stringify({
332
+ headers,
333
+ matchStrategy: suggestedMapping.matchStrategy,
334
+ matchField: suggestedMapping.matchField ?? null,
335
+ fields: suggestedMapping.fields.map((field) => ({
336
+ externalField: field.externalField,
337
+ localField: field.localField,
338
+ mappingKind: field.mappingKind ?? null,
339
+ dedupeRole: field.dedupeRole ?? null,
340
+ })),
341
+ })
342
+ }
343
+
344
+ export function findMappingTargetOption(
345
+ targetOptions: MappingTargetOption[],
346
+ targetField: string,
347
+ ): MappingTargetOption | undefined {
348
+ return targetOptions.find((option) => option.value === targetField)
349
+ }
350
+
351
+ export { normalizeMatchToken }