@open-mercato/core 0.6.6-develop.6314.1.c7b8291aa2 → 0.6.6-develop.6330.1.a261878aa8

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 (91) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js +7 -12
  3. package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js.map +2 -2
  4. package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js +5 -2
  5. package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js.map +2 -2
  6. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +2 -1
  7. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
  8. package/dist/modules/customer_accounts/widgets/injection/company-users/widget.client.js +2 -1
  9. package/dist/modules/customer_accounts/widgets/injection/company-users/widget.client.js.map +2 -2
  10. package/dist/modules/customers/ai-tools/_shared.js +158 -0
  11. package/dist/modules/customers/ai-tools/_shared.js.map +7 -0
  12. package/dist/modules/customers/ai-tools/companies-pack.js +15 -121
  13. package/dist/modules/customers/ai-tools/companies-pack.js.map +2 -2
  14. package/dist/modules/customers/ai-tools/people-pack.js +9 -107
  15. package/dist/modules/customers/ai-tools/people-pack.js.map +2 -2
  16. package/dist/modules/customers/components/calendar/CalendarScreen.js +1 -0
  17. package/dist/modules/customers/components/calendar/CalendarScreen.js.map +2 -2
  18. package/dist/modules/customers/components/calendar/EventPeekPopover.js +12 -1
  19. package/dist/modules/customers/components/calendar/EventPeekPopover.js.map +2 -2
  20. package/dist/modules/customers/components/calendar/TimeGrid.js +3 -0
  21. package/dist/modules/customers/components/calendar/TimeGrid.js.map +2 -2
  22. package/dist/modules/customers/components/calendar/types.js.map +1 -1
  23. package/dist/modules/customers/components/detail/EntityTagsDialog.js +76 -67
  24. package/dist/modules/customers/components/detail/EntityTagsDialog.js.map +3 -3
  25. package/dist/modules/dashboards/components/WidgetVisibilityEditor.js +4 -3
  26. package/dist/modules/dashboards/components/WidgetVisibilityEditor.js.map +2 -2
  27. package/dist/modules/data_sync/api/runs.js +1 -0
  28. package/dist/modules/data_sync/api/runs.js.map +2 -2
  29. package/dist/modules/data_sync/backend/data-sync/page.js +3 -1
  30. package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
  31. package/dist/modules/data_sync/data/validators.js +1 -0
  32. package/dist/modules/data_sync/data/validators.js.map +2 -2
  33. package/dist/modules/data_sync/lib/sync-run-service.js +20 -0
  34. package/dist/modules/data_sync/lib/sync-run-service.js.map +2 -2
  35. package/dist/modules/entities/api/records.js +11 -1
  36. package/dist/modules/entities/api/records.js.map +2 -2
  37. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +17 -10
  38. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
  39. package/dist/modules/messages/api/unread-count/route.js +32 -0
  40. package/dist/modules/messages/api/unread-count/route.js.map +2 -2
  41. package/dist/modules/query_index/components/QueryIndexesTable.js +13 -5
  42. package/dist/modules/query_index/components/QueryIndexesTable.js.map +2 -2
  43. package/dist/modules/shipping_carriers/i18n/en.js +11 -1
  44. package/dist/modules/shipping_carriers/i18n/en.js.map +2 -2
  45. package/dist/modules/shipping_carriers/i18n/pl.js +11 -1
  46. package/dist/modules/shipping_carriers/i18n/pl.js.map +2 -2
  47. package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.client.js +25 -0
  48. package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.client.js.map +7 -0
  49. package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.js +3 -17
  50. package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.js.map +2 -2
  51. package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js +19 -14
  52. package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js.map +2 -2
  53. package/package.json +7 -7
  54. package/src/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.tsx +7 -10
  55. package/src/modules/customer_accounts/backend/customer_accounts/users/page.tsx +6 -7
  56. package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +3 -2
  57. package/src/modules/customer_accounts/widgets/injection/company-users/widget.client.tsx +3 -2
  58. package/src/modules/customers/ai-tools/_shared.ts +270 -0
  59. package/src/modules/customers/ai-tools/companies-pack.ts +17 -157
  60. package/src/modules/customers/ai-tools/people-pack.ts +11 -133
  61. package/src/modules/customers/components/calendar/CalendarScreen.tsx +1 -0
  62. package/src/modules/customers/components/calendar/EventPeekPopover.tsx +29 -11
  63. package/src/modules/customers/components/calendar/TimeGrid.tsx +3 -0
  64. package/src/modules/customers/components/calendar/types.ts +1 -0
  65. package/src/modules/customers/components/detail/EntityTagsDialog.tsx +89 -71
  66. package/src/modules/customers/i18n/de.json +1 -0
  67. package/src/modules/customers/i18n/en.json +1 -0
  68. package/src/modules/customers/i18n/es.json +1 -0
  69. package/src/modules/customers/i18n/pl.json +1 -0
  70. package/src/modules/dashboards/components/WidgetVisibilityEditor.tsx +6 -3
  71. package/src/modules/data_sync/api/runs.ts +1 -0
  72. package/src/modules/data_sync/backend/data-sync/page.tsx +3 -1
  73. package/src/modules/data_sync/data/validators.ts +1 -0
  74. package/src/modules/data_sync/i18n/de.json +1 -0
  75. package/src/modules/data_sync/i18n/en.json +1 -0
  76. package/src/modules/data_sync/i18n/es.json +1 -0
  77. package/src/modules/data_sync/i18n/pl.json +1 -0
  78. package/src/modules/data_sync/lib/sync-run-service.ts +23 -0
  79. package/src/modules/entities/api/records.ts +17 -1
  80. package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +25 -12
  81. package/src/modules/messages/api/unread-count/route.ts +40 -0
  82. package/src/modules/query_index/components/QueryIndexesTable.tsx +17 -15
  83. package/src/modules/shipping_carriers/i18n/de.json +11 -1
  84. package/src/modules/shipping_carriers/i18n/en.json +11 -1
  85. package/src/modules/shipping_carriers/i18n/en.ts +10 -0
  86. package/src/modules/shipping_carriers/i18n/es.json +11 -1
  87. package/src/modules/shipping_carriers/i18n/pl.json +11 -1
  88. package/src/modules/shipping_carriers/i18n/pl.ts +10 -0
  89. package/src/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.client.tsx +35 -0
  90. package/src/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.tsx +3 -22
  91. package/src/modules/translations/widgets/injection/translation-manager/widget.client.tsx +30 -13
@@ -438,53 +438,49 @@ export function EntityTagsDialog({
438
438
  const loadData = React.useCallback(async () => {
439
439
  setLoading(true)
440
440
  try {
441
- let kindSettings: KindSetting[] = []
442
441
  const scopedQuery = new URLSearchParams()
443
442
  if (entityOrganizationId) {
444
443
  scopedQuery.set('organizationId', entityOrganizationId)
445
444
  }
446
- try {
447
- const settingsCall = await apiCall<{ items?: KindSetting[] }>(
448
- `/api/customers/dictionaries/kind-settings${scopedQuery.size ? `?${scopedQuery.toString()}` : ''}`,
449
- { cache: 'no-store', headers: { 'x-om-unauthorized-redirect': '0' } },
450
- )
451
- if (settingsCall.ok && settingsCall.result?.items) {
452
- kindSettings = settingsCall.result.items
445
+
446
+ const loadKindSettings = async (): Promise<KindSetting[]> => {
447
+ try {
448
+ const settingsCall = await apiCall<{ items?: KindSetting[] }>(
449
+ `/api/customers/dictionaries/kind-settings${scopedQuery.size ? `?${scopedQuery.toString()}` : ''}`,
450
+ { cache: 'no-store', headers: { 'x-om-unauthorized-redirect': '0' } },
451
+ )
452
+ if (settingsCall.ok && settingsCall.result?.items) {
453
+ return settingsCall.result.items
454
+ }
455
+ } catch {
456
+ // Default category order works without explicit settings rows.
453
457
  }
454
- } catch {
455
- // Default category order works without explicit settings rows.
458
+ return []
456
459
  }
457
460
 
458
- const settingsMap = new Map(kindSettings.map((setting) => [setting.kind, setting]))
459
-
460
- const selectedTagEntries = Array.isArray(entityData.tags)
461
- ? entityData.tags.map((tag) => ({
462
- id: tag.id,
463
- value: tag.id,
464
- label: tag.label,
465
- color: tag.color ?? null,
466
- }))
467
- : []
468
-
469
- let assignedLabelIds: string[] = []
470
- let selectedLabelEntries: CategoryOption[] = []
471
- try {
472
- const labelsQuery = new URLSearchParams()
473
- labelsQuery.set('entityId', entityId)
474
- labelsQuery.set('pageSize', '1')
475
- if (entityOrganizationId) {
476
- labelsQuery.set('organizationId', entityOrganizationId)
477
- }
478
- const labelsCall = await apiCall<{
479
- items?: LabelItem[]
480
- assignedIds?: string[]
481
- }>(`/api/customers/labels?${labelsQuery.toString()}`, {
482
- cache: 'no-store',
483
- headers: { 'x-om-unauthorized-redirect': '0' },
484
- })
485
- const labelsData = labelsCall.ok ? labelsCall.result : null
486
- assignedLabelIds = labelsData?.assignedIds ?? []
487
- if (assignedLabelIds.length > 0) {
461
+ const loadLabelData = async (): Promise<{
462
+ assignedLabelIds: string[]
463
+ selectedLabelEntries: CategoryOption[]
464
+ }> => {
465
+ try {
466
+ const labelsQuery = new URLSearchParams()
467
+ labelsQuery.set('entityId', entityId)
468
+ labelsQuery.set('pageSize', '1')
469
+ if (entityOrganizationId) {
470
+ labelsQuery.set('organizationId', entityOrganizationId)
471
+ }
472
+ const labelsCall = await apiCall<{
473
+ items?: LabelItem[]
474
+ assignedIds?: string[]
475
+ }>(`/api/customers/labels?${labelsQuery.toString()}`, {
476
+ cache: 'no-store',
477
+ headers: { 'x-om-unauthorized-redirect': '0' },
478
+ })
479
+ const labelsData = labelsCall.ok ? labelsCall.result : null
480
+ const assignedLabelIds = labelsData?.assignedIds ?? []
481
+ if (assignedLabelIds.length === 0) {
482
+ return { assignedLabelIds, selectedLabelEntries: [] }
483
+ }
488
484
  const detailQuery = new URLSearchParams({
489
485
  ids: assignedLabelIds.join(','),
490
486
  pageSize: String(Math.min(assignedLabelIds.length, 100)),
@@ -500,44 +496,36 @@ export function EntityTagsDialog({
500
496
  },
501
497
  )
502
498
  const selectedLabels = selectedLabelsCall.ok ? selectedLabelsCall.result?.items ?? [] : []
503
- selectedLabelEntries = selectedLabels.map((label) => ({
499
+ const selectedLabelEntries = selectedLabels.map((label) => ({
504
500
  id: label.id,
505
501
  value: label.id,
506
502
  label: label.label,
507
503
  color: null,
508
504
  }))
505
+ return { assignedLabelIds, selectedLabelEntries }
506
+ } catch {
507
+ return { assignedLabelIds: [], selectedLabelEntries: [] }
509
508
  }
510
- } catch {
511
- assignedLabelIds = []
512
- selectedLabelEntries = []
513
509
  }
514
510
 
515
- const categoryDefs = buildApplicableCategories(entityType, kindSettings)
516
- const loadedCategories: CategorySection[] = []
511
+ const selectedTagEntries = Array.isArray(entityData.tags)
512
+ ? entityData.tags.map((tag) => ({
513
+ id: tag.id,
514
+ value: tag.id,
515
+ label: tag.label,
516
+ color: tag.color ?? null,
517
+ }))
518
+ : []
517
519
 
518
- for (const categoryDef of categoryDefs) {
519
- if (categoryDef.source === 'tags') {
520
- loadedCategories.push({
521
- ...categoryDef,
522
- label: t(categoryDef.labelKey, categoryDef.labelFallback),
523
- description: t(categoryDef.descriptionKey, categoryDef.descriptionFallback),
524
- entries: sortOptions(selectedTagEntries),
525
- selectionMode: categoryDef.selectionMode ?? 'multi',
526
- })
527
- continue
528
- }
520
+ // The label chain is independent of kind settings and the dictionary
521
+ // fan-out, so kick it off immediately and let it resolve in parallel.
522
+ const labelDataPromise = loadLabelData()
529
523
 
530
- if (categoryDef.source === 'labels') {
531
- loadedCategories.push({
532
- ...categoryDef,
533
- label: t(categoryDef.labelKey, categoryDef.labelFallback),
534
- description: t(categoryDef.descriptionKey, categoryDef.descriptionFallback),
535
- entries: sortOptions(selectedLabelEntries),
536
- selectionMode: categoryDef.selectionMode ?? 'multi',
537
- })
538
- continue
539
- }
524
+ const kindSettings = await loadKindSettings()
525
+ const settingsMap = new Map(kindSettings.map((setting) => [setting.kind, setting]))
526
+ const categoryDefs = buildApplicableCategories(entityType, kindSettings)
540
527
 
528
+ const loadDictionaryCategory = async (categoryDef: CategoryDef): Promise<CategorySection> => {
541
529
  try {
542
530
  const dictionaryUrl = new URL(`/api/customers/dictionaries/${categoryDef.routeKind}`, 'http://localhost')
543
531
  if (entityOrganizationId) {
@@ -568,7 +556,7 @@ export function EntityTagsDialog({
568
556
  color: null,
569
557
  })
570
558
  })
571
- loadedCategories.push({
559
+ return {
572
560
  ...categoryDef,
573
561
  label: categoryDef.labelKey
574
562
  ? t(categoryDef.labelKey, categoryDef.labelFallback)
@@ -582,7 +570,7 @@ export function EntityTagsDialog({
582
570
  : categoryDef.descriptionFallback,
583
571
  entries: sortOptions(entries),
584
572
  selectionMode,
585
- })
573
+ }
586
574
  } catch {
587
575
  const setting = categoryDef.settingKind
588
576
  ? settingsMap.get(categoryDef.settingKind)
@@ -594,7 +582,7 @@ export function EntityTagsDialog({
594
582
  label: value,
595
583
  color: null,
596
584
  }))
597
- loadedCategories.push({
585
+ return {
598
586
  ...categoryDef,
599
587
  label: categoryDef.labelKey
600
588
  ? t(categoryDef.labelKey, categoryDef.labelFallback)
@@ -608,10 +596,40 @@ export function EntityTagsDialog({
608
596
  : categoryDef.descriptionFallback,
609
597
  entries: fallbackEntries,
610
598
  selectionMode,
611
- })
599
+ }
612
600
  }
613
601
  }
614
602
 
603
+ // Independent dictionary requests fan out together instead of awaiting
604
+ // each one sequentially; Promise.all preserves categoryDefs order so the
605
+ // subsequent sort and selection initialization stay stable.
606
+ const loadedCategories = await Promise.all(
607
+ categoryDefs.map(async (categoryDef): Promise<CategorySection> => {
608
+ if (categoryDef.source === 'tags') {
609
+ return {
610
+ ...categoryDef,
611
+ label: t(categoryDef.labelKey, categoryDef.labelFallback),
612
+ description: t(categoryDef.descriptionKey, categoryDef.descriptionFallback),
613
+ entries: sortOptions(selectedTagEntries),
614
+ selectionMode: categoryDef.selectionMode ?? 'multi',
615
+ }
616
+ }
617
+ if (categoryDef.source === 'labels') {
618
+ const { selectedLabelEntries } = await labelDataPromise
619
+ return {
620
+ ...categoryDef,
621
+ label: t(categoryDef.labelKey, categoryDef.labelFallback),
622
+ description: t(categoryDef.descriptionKey, categoryDef.descriptionFallback),
623
+ entries: sortOptions(selectedLabelEntries),
624
+ selectionMode: categoryDef.selectionMode ?? 'multi',
625
+ }
626
+ }
627
+ return loadDictionaryCategory(categoryDef)
628
+ }),
629
+ )
630
+
631
+ const { assignedLabelIds, selectedLabelEntries } = await labelDataPromise
632
+
615
633
  loadedCategories.sort((left, right) => {
616
634
  const leftSortOrder =
617
635
  left.settingKind && settingsMap.has(left.settingKind)
@@ -312,6 +312,7 @@
312
312
  "customers.calendar.peek.attendee": "1 Teilnehmer",
313
313
  "customers.calendar.peek.attendees": "{count} Teilnehmer",
314
314
  "customers.calendar.peek.edit": "Bearbeiten",
315
+ "customers.calendar.peek.editForbidden": "Sie haben keine Berechtigung, Termine zu bearbeiten",
315
316
  "customers.calendar.peek.join": "Beitreten",
316
317
  "customers.calendar.platform.meet": "Meet",
317
318
  "customers.calendar.platform.slack": "Slack",
@@ -312,6 +312,7 @@
312
312
  "customers.calendar.peek.attendee": "1 attendee",
313
313
  "customers.calendar.peek.attendees": "{count} attendees",
314
314
  "customers.calendar.peek.edit": "Edit",
315
+ "customers.calendar.peek.editForbidden": "You don't have permission to edit events",
315
316
  "customers.calendar.peek.join": "Join",
316
317
  "customers.calendar.platform.meet": "Meet",
317
318
  "customers.calendar.platform.slack": "Slack",
@@ -312,6 +312,7 @@
312
312
  "customers.calendar.peek.attendee": "1 asistente",
313
313
  "customers.calendar.peek.attendees": "{count} asistentes",
314
314
  "customers.calendar.peek.edit": "Editar",
315
+ "customers.calendar.peek.editForbidden": "No tienes permiso para editar eventos",
315
316
  "customers.calendar.peek.join": "Unirse",
316
317
  "customers.calendar.platform.meet": "Meet",
317
318
  "customers.calendar.platform.slack": "Slack",
@@ -312,6 +312,7 @@
312
312
  "customers.calendar.peek.attendee": "1 uczestnik",
313
313
  "customers.calendar.peek.attendees": "{count} uczestników",
314
314
  "customers.calendar.peek.edit": "Edytuj",
315
+ "customers.calendar.peek.editForbidden": "Nie masz uprawnień do edycji wydarzeń",
315
316
  "customers.calendar.peek.join": "Dołącz",
316
317
  "customers.calendar.platform.meet": "Meet",
317
318
  "customers.calendar.platform.slack": "Slack",
@@ -193,9 +193,12 @@ export const WidgetVisibilityEditor = React.forwardRef<WidgetVisibilityEditorHan
193
193
  setLoading(true)
194
194
  setError(null)
195
195
  try {
196
- await loadCatalog()
197
- if (kind === 'role') await loadRoleData(tenantIdRef.current, organizationIdRef.current)
198
- else await loadUserData(tenantIdRef.current, organizationIdRef.current)
196
+ await Promise.all([
197
+ loadCatalog(),
198
+ kind === 'role'
199
+ ? loadRoleData(tenantIdRef.current, organizationIdRef.current)
200
+ : loadUserData(tenantIdRef.current, organizationIdRef.current),
201
+ ])
199
202
  } catch (err) {
200
203
  console.error('Failed to load widget visibility data', err)
201
204
  if (!cancelled) {
@@ -25,6 +25,7 @@ export async function GET(req: Request) {
25
25
  entityType: url.searchParams.get('entityType') ?? undefined,
26
26
  direction: url.searchParams.get('direction') ?? undefined,
27
27
  status: url.searchParams.get('status') ?? undefined,
28
+ search: url.searchParams.get('search') ?? undefined,
28
29
  page: url.searchParams.get('page') ?? undefined,
29
30
  pageSize: url.searchParams.get('pageSize') ?? undefined,
30
31
  })
@@ -167,6 +167,7 @@ export default function SyncRunsDashboardPage() {
167
167
  params.set('pageSize', '20')
168
168
  if (filterValues.status) params.set('status', filterValues.status as string)
169
169
  if (filterValues.direction) params.set('direction', filterValues.direction as string)
170
+ if (search.trim()) params.set('search', search.trim())
170
171
  const fallback: ResponsePayload = { items: [], total: 0, page, totalPages: 1 }
171
172
  const call = await apiCall<ResponsePayload>(
172
173
  `/api/data_sync/runs?${params.toString()}`,
@@ -188,7 +189,7 @@ export default function SyncRunsDashboardPage() {
188
189
  }
189
190
  load()
190
191
  return () => { cancelled = true }
191
- }, [page, filterValues, reloadToken, scopeVersion, t])
192
+ }, [page, filterValues, search, reloadToken, scopeVersion, t])
192
193
 
193
194
  React.useEffect(() => {
194
195
  let cancelled = false
@@ -965,6 +966,7 @@ export default function SyncRunsDashboardPage() {
965
966
  onFiltersClear={handleFiltersClear}
966
967
  searchValue={search}
967
968
  onSearchChange={(value) => { setSearch(value); setPage(1) }}
969
+ searchPlaceholder={t('data_sync.dashboard.searchPlaceholder')}
968
970
  perspective={{ tableId: 'data_sync.runs' }}
969
971
  onRowClick={(row) => {
970
972
  router.push(`/backend/data-sync/runs/${encodeURIComponent(row.id)}`)
@@ -28,6 +28,7 @@ export const listSyncRunsQuerySchema = z.object({
28
28
  entityType: z.string().optional(),
29
29
  direction: z.enum(['import', 'export']).optional(),
30
30
  status: z.enum(['pending', 'running', 'completed', 'failed', 'cancelled', 'paused']).optional(),
31
+ search: z.string().trim().min(1).max(200).optional(),
31
32
  page: z.coerce.number().int().min(1).default(1),
32
33
  pageSize: z.coerce.number().int().min(1).max(100).default(20),
33
34
  })
@@ -47,6 +47,7 @@
47
47
  "data_sync.dashboard.schedule.timezone": "Timezone",
48
48
  "data_sync.dashboard.schedule.title": "Recurring schedule",
49
49
  "data_sync.dashboard.schedule.type": "Schedule type",
50
+ "data_sync.dashboard.searchPlaceholder": "Nach Integration, Entitätstyp, Status oder Lauf-ID suchen",
50
51
  "data_sync.dashboard.start.batchSize": "Batch Size",
51
52
  "data_sync.dashboard.start.description": "Pick a sync target, launch an ad-hoc run, or save a recurring schedule for the same entity and direction from this page.",
52
53
  "data_sync.dashboard.start.error": "Failed to start sync run",
@@ -47,6 +47,7 @@
47
47
  "data_sync.dashboard.schedule.timezone": "Timezone",
48
48
  "data_sync.dashboard.schedule.title": "Recurring schedule",
49
49
  "data_sync.dashboard.schedule.type": "Schedule type",
50
+ "data_sync.dashboard.searchPlaceholder": "Search by integration, entity type, status, or run ID",
50
51
  "data_sync.dashboard.start.batchSize": "Batch Size",
51
52
  "data_sync.dashboard.start.description": "Pick a sync target, launch an ad-hoc run, or save a recurring schedule for the same entity and direction from this page.",
52
53
  "data_sync.dashboard.start.error": "Failed to start sync run",
@@ -47,6 +47,7 @@
47
47
  "data_sync.dashboard.schedule.timezone": "Timezone",
48
48
  "data_sync.dashboard.schedule.title": "Recurring schedule",
49
49
  "data_sync.dashboard.schedule.type": "Schedule type",
50
+ "data_sync.dashboard.searchPlaceholder": "Buscar por integración, tipo de entidad, estado o ID de ejecución",
50
51
  "data_sync.dashboard.start.batchSize": "Batch Size",
51
52
  "data_sync.dashboard.start.description": "Pick a sync target, launch an ad-hoc run, or save a recurring schedule for the same entity and direction from this page.",
52
53
  "data_sync.dashboard.start.error": "Failed to start sync run",
@@ -47,6 +47,7 @@
47
47
  "data_sync.dashboard.schedule.timezone": "Timezone",
48
48
  "data_sync.dashboard.schedule.title": "Recurring schedule",
49
49
  "data_sync.dashboard.schedule.type": "Schedule type",
50
+ "data_sync.dashboard.searchPlaceholder": "Szukaj po integracji, typie encji, statusie lub ID przebiegu",
50
51
  "data_sync.dashboard.start.batchSize": "Batch Size",
51
52
  "data_sync.dashboard.start.description": "Pick a sync target, launch an ad-hoc run, or save a recurring schedule for the same entity and direction from this page.",
52
53
  "data_sync.dashboard.start.error": "Failed to start sync run",
@@ -1,8 +1,26 @@
1
1
  import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'
2
2
  import { findAndCountWithDecryption, findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
3
3
  import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
4
+ import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
4
5
  import { SyncCursor, SyncRun } from '../data/entities'
5
6
 
7
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
8
+
9
+ function buildRunSearchFilter(search: string): FilterQuery<SyncRun>[] | null {
10
+ const trimmed = search.trim()
11
+ if (!trimmed) return null
12
+ const pattern = `%${escapeLikePattern(trimmed)}%`
13
+ const conditions: FilterQuery<SyncRun>[] = [
14
+ { integrationId: { $ilike: pattern } },
15
+ { entityType: { $ilike: pattern } },
16
+ { status: { $ilike: pattern } },
17
+ ]
18
+ if (UUID_PATTERN.test(trimmed)) {
19
+ conditions.push({ id: trimmed })
20
+ }
21
+ return conditions
22
+ }
23
+
6
24
  type SyncScope = {
7
25
  organizationId: string
8
26
  tenantId: string
@@ -89,6 +107,7 @@ export function createSyncRunService(em: EntityManager) {
89
107
  entityType?: string
90
108
  direction?: 'import' | 'export'
91
109
  status?: string
110
+ search?: string
92
111
  page: number
93
112
  pageSize: number
94
113
  }, scope: SyncScope): Promise<{ items: SyncRun[]; total: number }> {
@@ -102,6 +121,10 @@ export function createSyncRunService(em: EntityManager) {
102
121
  if (query.entityType) where.entityType = query.entityType
103
122
  if (query.direction) where.direction = query.direction
104
123
  if (query.status) where.status = query.status as SyncRun['status']
124
+ if (query.search) {
125
+ const searchConditions = buildRunSearchFilter(query.search)
126
+ if (searchConditions) where.$or = searchConditions
127
+ }
105
128
 
106
129
  const [items, total] = await findAndCountWithDecryption(
107
130
  em,
@@ -107,6 +107,8 @@ const listRecordsQuerySchema = z
107
107
  pageSize: z.coerce.number().int().min(1).max(100).optional(),
108
108
  sortField: z.string().optional(),
109
109
  sortDir: z.enum(['asc', 'desc']).optional(),
110
+ search: z.string().optional(),
111
+ searchFields: z.string().optional(),
110
112
  withDeleted: z.coerce.boolean().optional(),
111
113
  format: z.enum(['csv', 'json', 'xml', 'markdown']).optional(),
112
114
  exportScope: z.enum(['full']).optional(),
@@ -145,10 +147,15 @@ export async function GET(req: Request) {
145
147
  const sortField = url.searchParams.get('sortField') || 'id'
146
148
  const sortDir = (url.searchParams.get('sortDir') || 'asc').toLowerCase() === 'desc' ? 'desc' : 'asc'
147
149
  const withDeleted = parseBooleanWithDefault(url.searchParams.get('withDeleted'), false)
150
+ const searchTerm = (url.searchParams.get('search') || '').trim()
151
+ const searchFields = (url.searchParams.get('searchFields') || '')
152
+ .split(',')
153
+ .map((field) => field.trim())
154
+ .filter(Boolean)
148
155
 
149
156
  const qpEntries: Array<[string, string]> = []
150
157
  for (const [key, val] of url.searchParams.entries()) {
151
- if (['entityId','page','pageSize','sortField','sortDir','withDeleted','format','exportScope','export_scope','all','full'].includes(key)) continue
158
+ if (['entityId','page','pageSize','sortField','sortDir','withDeleted','format','exportScope','export_scope','all','full','search','searchFields'].includes(key)) continue
152
159
  qpEntries.push([key, val])
153
160
  }
154
161
 
@@ -246,6 +253,15 @@ export async function GET(req: Request) {
246
253
  // even before the first record exists.
247
254
  if (isCustomEntity) qopts.forceCustomEntityStorage = true
248
255
  for (const [k, v] of qpEntries) buildFilter(k, v, isCustomEntity)
256
+ // Server-side full-result search: match the term against the requested fields
257
+ // (defaults to `id`) before pagination so totals/exports stay consistent with
258
+ // the active search instead of filtering only the current client page (#3229).
259
+ if (searchTerm) {
260
+ const fields = searchFields.length ? searchFields : ['id']
261
+ const pattern = `%${searchTerm}%`
262
+ const orClauses = fields.map((field) => ({ [field]: { $ilike: pattern } }))
263
+ ;(filtersObj as any).$or = orClauses
264
+ }
249
265
  const res = await qe.query(entityId as any, qopts)
250
266
  const rawItems = res.items || []
251
267
  const viewPageItems = rawItems.map(mapRow)
@@ -97,7 +97,16 @@ function RecordsPageInner({ params }: { params: { entityId?: string } }) {
97
97
  keyExtras: [scopeVersion],
98
98
  })
99
99
 
100
- // Fetch records whenever paging/sorting/filters change (do NOT refetch on cfDefs/search changes)
100
+ // Fields searched server-side: every visible column plus the base `id`.
101
+ const searchableFields = React.useMemo(() => {
102
+ const fields = (columns || [])
103
+ .map((col) => (col as any).accessorKey)
104
+ .filter((key): key is string => typeof key === 'string' && key.length > 0)
105
+ return Array.from(new Set(['id', ...fields]))
106
+ }, [columns])
107
+
108
+ // Fetch records whenever paging/sorting/filters/search change. Search is applied
109
+ // server-side (before pagination) so totals and exports stay consistent (#3229).
101
110
  React.useEffect(() => {
102
111
  let cancelled = false
103
112
  const run = async () => {
@@ -107,6 +116,11 @@ function RecordsPageInner({ params }: { params: { entityId?: string } }) {
107
116
  params.set('entityId', entityId)
108
117
  params.set('page', String(page))
109
118
  params.set('pageSize', String(pageSize))
119
+ const trimmedSearch = search.trim()
120
+ if (trimmedSearch) {
121
+ params.set('search', trimmedSearch)
122
+ if (searchableFields.length) params.set('searchFields', searchableFields.join(','))
123
+ }
110
124
  const s = sorting?.[0]
111
125
  if (s?.id) {
112
126
  params.set('sortField', String(s.id))
@@ -154,7 +168,7 @@ function RecordsPageInner({ params }: { params: { entityId?: string } }) {
154
168
  }
155
169
  if (entityId) run()
156
170
  return () => { cancelled = true }
157
- }, [entityId, page, pageSize, sorting, filterValues, scopeVersion])
171
+ }, [entityId, page, pageSize, sorting, filterValues, scopeVersion, search, searchableFields])
158
172
 
159
173
  // Build columns from custom field definitions only (no data round-trip)
160
174
  React.useEffect(() => {
@@ -175,15 +189,9 @@ function RecordsPageInner({ params }: { params: { entityId?: string } }) {
175
189
  setColumns(cols)
176
190
  }, [cfDefs])
177
191
 
178
- // Client-side quick search filtering without triggering server refetch
179
- const data = React.useMemo(() => {
180
- if (!search.trim()) return rawData
181
- const q = search.trim().toLowerCase()
182
- return (rawData || []).filter((row: any) => {
183
- const values = Object.values(row || {})
184
- return values.some((v) => normalizeCell(v).toLowerCase().includes(q))
185
- })
186
- }, [rawData, search])
192
+ // Search is server-side (see fetch effect); render the fetched page as-is so
193
+ // pagination totals and exports stay consistent with the active search (#3229).
194
+ const data = rawData
187
195
 
188
196
  const viewExportColumns = React.useMemo(() => {
189
197
  return (columns || [])
@@ -213,8 +221,13 @@ function RecordsPageInner({ params }: { params: { entityId?: string } }) {
213
221
  qp.set('sortField', String(sort.id))
214
222
  qp.set('sortDir', sort.desc ? 'desc' : 'asc')
215
223
  }
224
+ const trimmedSearch = search.trim()
225
+ if (trimmedSearch) {
226
+ qp.set('search', trimmedSearch)
227
+ if (searchableFields.length) qp.set('searchFields', searchableFields.join(','))
228
+ }
216
229
  return `/api/entities/records?${qp.toString()}`
217
- }, [entityId, sorting])
230
+ }, [entityId, sorting, search, searchableFields])
218
231
 
219
232
  const exportConfig = React.useMemo(() => {
220
233
  const safeEntityId = entityId.replace(/[^a-z0-9_-]/gi, '_') || 'records'
@@ -1,5 +1,11 @@
1
1
  import type { EntityManager } from '@mikro-orm/postgresql'
2
2
  import { type Kysely, sql } from 'kysely'
3
+ import { runWithCacheTenant } from '@open-mercato/cache'
4
+ import {
5
+ buildCollectionTags,
6
+ isCrudCacheEnabled,
7
+ resolveCrudCache,
8
+ } from '@open-mercato/shared/lib/crud/cache'
3
9
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi/types'
4
10
  import { resolveMessageContext } from '../../lib/routeHelpers'
5
11
  import { unreadCountResponseSchema } from '../openapi'
@@ -8,6 +14,16 @@ export const metadata = {
8
14
  GET: { requireAuth: true },
9
15
  }
10
16
 
17
+ const UNREAD_COUNT_RESOURCE = 'messages.message'
18
+ const UNREAD_COUNT_TTL_MS = 10_000
19
+
20
+ function buildUnreadCountCacheKey(params: {
21
+ userId: string
22
+ orgId: string | null
23
+ }): string {
24
+ return `messages:unread-count:u=${params.userId}:org=${params.orgId ?? 'null'}`
25
+ }
26
+
11
27
  function getDb(em: EntityManager): Kysely<any> {
12
28
  return em.getKysely<any>()
13
29
  }
@@ -17,6 +33,19 @@ export async function GET(req: Request) {
17
33
  const em = ctx.container.resolve('em') as EntityManager
18
34
  const db = getDb(em) as any
19
35
 
36
+ const orgId = scope.organizationId ?? null
37
+ const cache = isCrudCacheEnabled() ? resolveCrudCache(ctx.container) : null
38
+ const cacheKey = cache
39
+ ? buildUnreadCountCacheKey({ userId: scope.userId, orgId })
40
+ : null
41
+
42
+ if (cache && cacheKey) {
43
+ const cached = await runWithCacheTenant(scope.tenantId, () => cache.get(cacheKey))
44
+ if (typeof cached === 'number') {
45
+ return Response.json({ unreadCount: cached })
46
+ }
47
+ }
48
+
20
49
  let query = db
21
50
  .selectFrom('message_recipients as r')
22
51
  .innerJoin('messages as m', 'm.id', 'r.message_id')
@@ -38,6 +67,17 @@ export async function GET(req: Request) {
38
67
  .executeTakeFirst() as { count: string | number } | undefined
39
68
  const count = Number(row?.count ?? 0)
40
69
 
70
+ if (cache && cacheKey) {
71
+ try {
72
+ await runWithCacheTenant(scope.tenantId, () =>
73
+ cache.set(cacheKey, count, {
74
+ ttl: UNREAD_COUNT_TTL_MS,
75
+ tags: buildCollectionTags(UNREAD_COUNT_RESOURCE, scope.tenantId, [orgId]),
76
+ }),
77
+ )
78
+ } catch {}
79
+ }
80
+
41
81
  return Response.json({ unreadCount: count })
42
82
  }
43
83
 
@@ -11,6 +11,7 @@ import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/u
11
11
  import { useT } from '@open-mercato/shared/lib/i18n/context'
12
12
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
13
13
  import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
14
+ import { StatusBadge, type StatusBadgeVariant } from '@open-mercato/ui/primitives/status-badge'
14
15
 
15
16
  const MUTATION_CONTEXT_ID = 'query_index.status.list:actions'
16
17
 
@@ -127,7 +128,7 @@ function createColumns(t: Translator): ColumnDef<Row>[] {
127
128
  if (!record.vectorEnabled) return <span>—</span>
128
129
  const ok = record.vectorCount != null && record.baseCount != null && record.vectorCount === record.baseCount
129
130
  const display = formatCount(record.vectorCount)
130
- const className = ok ? 'text-green-600' : 'text-orange-600'
131
+ const className = ok ? 'text-status-success-text' : 'text-status-warning-text'
131
132
  return <span className={className}>{display}</span>
132
133
  },
133
134
  meta: { priority: 2 },
@@ -141,7 +142,7 @@ function createColumns(t: Translator): ColumnDef<Row>[] {
141
142
  if (!record.fulltextEnabled) return <span>—</span>
142
143
  const ok = record.fulltextCount != null && record.baseCount != null && record.fulltextCount === record.baseCount
143
144
  const display = formatCount(record.fulltextCount)
144
- const className = ok ? 'text-green-600' : 'text-orange-600'
145
+ const className = ok ? 'text-status-success-text' : 'text-status-warning-text'
145
146
  return <span className={className}>{display}</span>
146
147
  },
147
148
  meta: { priority: 2 },
@@ -159,17 +160,14 @@ function createColumns(t: Translator): ColumnDef<Row>[] {
159
160
  const label = jobProgress
160
161
  ? t('query_index.table.status.withProgress', { status: statusText, progress: jobProgress })
161
162
  : statusText
162
- const className = job
163
- ? job.status === 'stalled'
164
- ? 'text-red-600'
165
- : job.status === 'reindexing' || job.status === 'purging'
166
- ? 'text-orange-600'
167
- : ok
168
- ? 'text-green-600'
169
- : 'text-muted-foreground'
170
- : ok
171
- ? 'text-green-600'
172
- : 'text-muted-foreground'
163
+ let variant: StatusBadgeVariant = 'neutral'
164
+ if (job) {
165
+ if (job.status === 'stalled') variant = 'error'
166
+ else if (job.status === 'reindexing' || job.status === 'purging') variant = 'warning'
167
+ else variant = ok ? 'success' : 'neutral'
168
+ } else {
169
+ variant = ok ? 'success' : 'neutral'
170
+ }
173
171
 
174
172
  const lines: string[] = []
175
173
 
@@ -203,8 +201,12 @@ function createColumns(t: Translator): ColumnDef<Row>[] {
203
201
  }
204
202
 
205
203
  return (
206
- <div className="space-y-1">
207
- <span className={className}>{label}</span>
204
+ <div className="space-y-1.5">
205
+ <div>
206
+ <StatusBadge variant={variant} dot>
207
+ {label}
208
+ </StatusBadge>
209
+ </div>
208
210
  {lines.length > 0 && (
209
211
  <div className="text-xs text-muted-foreground">
210
212
  {lines.map((line, idx) => (