@open-mercato/core 0.6.1-develop.3256.1.fe3dec2464 → 0.6.1-develop.3260.1.dc719cac84

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 (120) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/helpers/integration/authFixtures.js.map +1 -1
  3. package/dist/helpers/integration/authUi.js +10 -2
  4. package/dist/helpers/integration/authUi.js.map +2 -2
  5. package/dist/helpers/integration/salesUi.js +3 -1
  6. package/dist/helpers/integration/salesUi.js.map +2 -2
  7. package/dist/modules/api_docs/frontend/docs/api/Explorer.js +10 -1
  8. package/dist/modules/api_docs/frontend/docs/api/Explorer.js.map +2 -2
  9. package/dist/modules/audit_logs/lib/display-helpers.js +28 -27
  10. package/dist/modules/audit_logs/lib/display-helpers.js.map +2 -2
  11. package/dist/modules/auth/api/users/route.js +21 -3
  12. package/dist/modules/auth/api/users/route.js.map +2 -2
  13. package/dist/modules/auth/backend/users/[id]/edit/page.js +6 -1
  14. package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
  15. package/dist/modules/auth/backend/users/create/page.js +4 -1
  16. package/dist/modules/auth/backend/users/create/page.js.map +2 -2
  17. package/dist/modules/auth/backend/users/page.js +56 -2
  18. package/dist/modules/auth/backend/users/page.js.map +3 -3
  19. package/dist/modules/auth/commands/users.js +14 -0
  20. package/dist/modules/auth/commands/users.js.map +2 -2
  21. package/dist/modules/auth/data/validators.js +8 -0
  22. package/dist/modules/auth/data/validators.js.map +2 -2
  23. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +1 -1
  24. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  25. package/dist/modules/customers/backend/customers/deals/[id]/page.js +15 -9
  26. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  27. package/dist/modules/customers/backend/customers/deals/pipeline/page.js +26 -3
  28. package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
  29. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +1 -1
  30. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  31. package/dist/modules/customers/components/PipelineSettings.js +108 -99
  32. package/dist/modules/customers/components/PipelineSettings.js.map +2 -2
  33. package/dist/modules/customers/components/detail/AssignRoleDialog.js +13 -5
  34. package/dist/modules/customers/components/detail/AssignRoleDialog.js.map +2 -2
  35. package/dist/modules/customers/components/detail/EntityTagsDialog.js +10 -2
  36. package/dist/modules/customers/components/detail/EntityTagsDialog.js.map +2 -2
  37. package/dist/modules/customers/components/detail/PersonCompaniesSection.js +15 -8
  38. package/dist/modules/customers/components/detail/PersonCompaniesSection.js.map +2 -2
  39. package/dist/modules/customers/components/detail/RolesSection.js +35 -22
  40. package/dist/modules/customers/components/detail/RolesSection.js.map +2 -2
  41. package/dist/modules/customers/components/detail/TasksSection.js +2 -2
  42. package/dist/modules/customers/components/detail/TasksSection.js.map +2 -2
  43. package/dist/modules/customers/components/detail/schedule/DateTimeFields.js +73 -64
  44. package/dist/modules/customers/components/detail/schedule/DateTimeFields.js.map +2 -2
  45. package/dist/modules/customers/components/linking/LinkEntityDialog.js +11 -2
  46. package/dist/modules/customers/components/linking/LinkEntityDialog.js.map +2 -2
  47. package/dist/modules/data_sync/backend/data-sync/page.js +2 -9
  48. package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
  49. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +14 -29
  50. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +2 -2
  51. package/dist/modules/dictionaries/components/DictionariesManager.js +10 -2
  52. package/dist/modules/dictionaries/components/DictionariesManager.js.map +2 -2
  53. package/dist/modules/integrations/backend/integrations/[id]/page.js +79 -95
  54. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
  55. package/dist/modules/payment_gateways/backend/payment-gateways/page.js +49 -70
  56. package/dist/modules/payment_gateways/backend/payment-gateways/page.js.map +2 -2
  57. package/dist/modules/planner/components/AvailabilityRuleSetForm.js +1 -1
  58. package/dist/modules/planner/components/AvailabilityRuleSetForm.js.map +1 -1
  59. package/dist/modules/planner/components/AvailabilityRulesEditor.js +19 -19
  60. package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
  61. package/dist/modules/resources/components/ResourceCrudForm.js +1 -1
  62. package/dist/modules/resources/components/ResourceCrudForm.js.map +1 -1
  63. package/dist/modules/resources/components/ResourceTypeCrudForm.js +1 -1
  64. package/dist/modules/resources/components/ResourceTypeCrudForm.js.map +2 -2
  65. package/dist/modules/sales/components/documents/ReturnDialog.js +11 -8
  66. package/dist/modules/sales/components/documents/ReturnDialog.js.map +2 -2
  67. package/dist/modules/staff/backend/staff/profile/create/page.js +1 -1
  68. package/dist/modules/staff/backend/staff/profile/create/page.js.map +2 -2
  69. package/dist/modules/staff/components/TeamForm.js +1 -1
  70. package/dist/modules/staff/components/TeamForm.js.map +2 -2
  71. package/dist/modules/staff/components/TeamMemberForm.js +1 -1
  72. package/dist/modules/staff/components/TeamMemberForm.js.map +1 -1
  73. package/dist/modules/staff/components/TeamRoleForm.js +1 -1
  74. package/dist/modules/staff/components/TeamRoleForm.js.map +2 -2
  75. package/package.json +5 -5
  76. package/src/helpers/integration/authFixtures.ts +1 -1
  77. package/src/helpers/integration/authUi.ts +11 -2
  78. package/src/helpers/integration/salesUi.ts +10 -1
  79. package/src/modules/api_docs/frontend/docs/api/Explorer.tsx +7 -3
  80. package/src/modules/audit_logs/lib/display-helpers.tsx +22 -14
  81. package/src/modules/auth/api/users/route.ts +22 -3
  82. package/src/modules/auth/backend/users/[id]/edit/page.tsx +9 -1
  83. package/src/modules/auth/backend/users/create/page.tsx +6 -2
  84. package/src/modules/auth/backend/users/page.tsx +60 -3
  85. package/src/modules/auth/commands/users.ts +15 -0
  86. package/src/modules/auth/data/validators.ts +8 -0
  87. package/src/modules/auth/i18n/de.json +4 -0
  88. package/src/modules/auth/i18n/en.json +4 -0
  89. package/src/modules/auth/i18n/es.json +4 -0
  90. package/src/modules/auth/i18n/pl.json +4 -0
  91. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +1 -1
  92. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +12 -13
  93. package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +17 -13
  94. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +1 -1
  95. package/src/modules/customers/components/PipelineSettings.tsx +95 -95
  96. package/src/modules/customers/components/detail/AssignRoleDialog.tsx +7 -4
  97. package/src/modules/customers/components/detail/EntityTagsDialog.tsx +13 -8
  98. package/src/modules/customers/components/detail/PersonCompaniesSection.tsx +16 -12
  99. package/src/modules/customers/components/detail/RolesSection.tsx +34 -31
  100. package/src/modules/customers/components/detail/TasksSection.tsx +16 -8
  101. package/src/modules/customers/components/detail/schedule/DateTimeFields.tsx +77 -51
  102. package/src/modules/customers/components/linking/LinkEntityDialog.tsx +7 -3
  103. package/src/modules/data_sync/backend/data-sync/page.tsx +4 -7
  104. package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +19 -54
  105. package/src/modules/dictionaries/components/DictionariesManager.tsx +7 -4
  106. package/src/modules/integrations/backend/integrations/[id]/page.tsx +101 -146
  107. package/src/modules/payment_gateways/backend/payment-gateways/page.tsx +60 -103
  108. package/src/modules/planner/components/AvailabilityRuleSetForm.tsx +1 -1
  109. package/src/modules/planner/components/AvailabilityRulesEditor.tsx +17 -17
  110. package/src/modules/resources/components/ResourceCrudForm.tsx +1 -1
  111. package/src/modules/resources/components/ResourceTypeCrudForm.tsx +1 -1
  112. package/src/modules/sales/components/documents/ReturnDialog.tsx +10 -7
  113. package/src/modules/sales/i18n/de.json +2 -0
  114. package/src/modules/sales/i18n/en.json +2 -0
  115. package/src/modules/sales/i18n/es.json +2 -0
  116. package/src/modules/sales/i18n/pl.json +2 -0
  117. package/src/modules/staff/backend/staff/profile/create/page.tsx +1 -1
  118. package/src/modules/staff/components/TeamForm.tsx +1 -1
  119. package/src/modules/staff/components/TeamMemberForm.tsx +1 -1
  120. package/src/modules/staff/components/TeamRoleForm.tsx +1 -1
@@ -431,7 +431,13 @@ export function TasksSection({
431
431
  ) : null}
432
432
 
433
433
  {!isInitialLoading && !hasTasks ? (
434
- <TabEmptyState title={emptyState.title} />
434
+ <TabEmptyState title={emptyState.title}>
435
+ <Button asChild variant="outline" size="sm" className="mt-2">
436
+ <Link href="/backend/customer-tasks">
437
+ {t('customers.people.detail.tasks.viewAll', 'View all tasks')}
438
+ </Link>
439
+ </Button>
440
+ </TabEmptyState>
435
441
  ) : null}
436
442
 
437
443
  {!isInitialLoading && hasTasks ? (
@@ -565,13 +571,15 @@ export function TasksSection({
565
571
  ) : null}
566
572
  </div>
567
573
  ) : null}
568
- <div className="flex justify-center">
569
- <Button asChild variant="outline" size="sm">
570
- <Link href="/backend/customer-tasks">
571
- {t('customers.people.detail.tasks.viewAll', 'View all tasks')}
572
- </Link>
573
- </Button>
574
- </div>
574
+ {!isInitialLoading && hasTasks ? (
575
+ <div className="flex justify-center">
576
+ <Button asChild variant="outline" size="sm">
577
+ <Link href="/backend/customer-tasks">
578
+ {t('customers.people.detail.tasks.viewAll', 'View all tasks')}
579
+ </Link>
580
+ </Button>
581
+ </div>
582
+ ) : null}
575
583
  </div>
576
584
 
577
585
  <TaskDialog
@@ -1,14 +1,48 @@
1
1
  'use client'
2
2
 
3
3
  import * as React from 'react'
4
- import { Calendar, Clock, ChevronDown, Globe, Repeat } from 'lucide-react'
4
+ import { Globe, Repeat } from 'lucide-react'
5
5
  import { cn } from '@open-mercato/shared/lib/utils'
6
6
  import { useT } from '@open-mercato/shared/lib/i18n/context'
7
7
  import { Button } from '@open-mercato/ui/primitives/button'
8
+ import { DatePicker } from '@open-mercato/ui/primitives/date-picker'
9
+ import { TimePicker } from '@open-mercato/ui/backend/inputs/TimePicker'
10
+ import {
11
+ Select,
12
+ SelectContent,
13
+ SelectItem,
14
+ SelectTrigger,
15
+ SelectValue,
16
+ } from '@open-mercato/ui/primitives/select'
8
17
  import type { ActivityType, ScheduleFieldId } from './fieldConfig'
9
18
  import { isVisible, getFieldLabel } from './fieldConfig'
10
19
 
11
- const DURATION_OPTIONS = [15, 30, 45, 60, 90, 120]
20
+ function parseIsoDate(value: string): Date | null {
21
+ if (!value) return null
22
+ const parts = value.split('-')
23
+ if (parts.length !== 3) return null
24
+ const [y, m, d] = parts.map((p) => parseInt(p, 10))
25
+ if (!Number.isFinite(y) || !Number.isFinite(m) || !Number.isFinite(d)) return null
26
+ const date = new Date(y, m - 1, d)
27
+ return Number.isNaN(date.getTime()) ? null : date
28
+ }
29
+
30
+ function formatIsoDate(date: Date | null): string {
31
+ if (!date) return ''
32
+ const y = date.getFullYear()
33
+ const m = String(date.getMonth() + 1).padStart(2, '0')
34
+ const d = String(date.getDate()).padStart(2, '0')
35
+ return `${y}-${m}-${d}`
36
+ }
37
+
38
+ const DURATION_OPTIONS: Array<{ value: number; key: string; fallback: string }> = [
39
+ { value: 15, key: 'customers.schedule.duration.option.15min', fallback: '15 min' },
40
+ { value: 30, key: 'customers.schedule.duration.option.30min', fallback: '30 min' },
41
+ { value: 45, key: 'customers.schedule.duration.option.45min', fallback: '45 min' },
42
+ { value: 60, key: 'customers.schedule.duration.option.1hour', fallback: '1 hour' },
43
+ { value: 90, key: 'customers.schedule.duration.option.1h30m', fallback: '1h 30m' },
44
+ { value: 120, key: 'customers.schedule.duration.option.2hours', fallback: '2 hours' },
45
+ ]
12
46
  const DAYS_OF_WEEK = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
13
47
 
14
48
  interface DateTimeFieldsProps {
@@ -73,30 +107,23 @@ export function DateTimeFields({
73
107
  return (
74
108
  <>
75
109
  {/* Date / Time / Duration */}
76
- <div className="flex gap-3">
77
- <div className="flex flex-[2] flex-col gap-1.5">
110
+ <div className="flex flex-wrap gap-3">
111
+ <div className="flex min-w-0 flex-[1.5] flex-col gap-1.5">
78
112
  <label className="text-overline font-semibold text-muted-foreground tracking-wider">
79
113
  {getFieldLabel(activityType, 'date', t, 'customers.schedule.date', 'Date')}
80
114
  <span aria-hidden="true" className="ml-1 text-status-error-foreground">*</span>
81
115
  </label>
82
- <div
116
+ <DatePicker
117
+ value={parseIsoDate(date)}
118
+ onChange={(next) => setDate(formatIsoDate(next))}
119
+ placeholder={t('customers.schedule.date.placeholder', 'Pick a date')}
120
+ required
121
+ aria-describedby={dateMissing ? dateErrorId : undefined}
83
122
  className={cn(
84
- 'flex items-center gap-2 rounded-md border bg-background px-3 py-2.5',
85
- dateMissing ? 'border-status-error-border' : 'border-border',
123
+ 'h-10',
124
+ dateMissing && 'border-status-error-border',
86
125
  )}
87
- >
88
- <Calendar className="size-3.5 text-muted-foreground" />
89
- <input
90
- type="date"
91
- value={date}
92
- onChange={(e) => setDate(e.target.value)}
93
- required
94
- aria-required="true"
95
- aria-invalid={dateMissing ? true : undefined}
96
- aria-describedby={dateMissing ? dateErrorId : undefined}
97
- className="flex-1 bg-transparent text-sm text-foreground focus:outline-none"
98
- />
99
- </div>
126
+ />
100
127
  {dateMissing ? (
101
128
  <p id={dateErrorId} className="text-xs text-status-error-foreground">
102
129
  {t('customers.activities.errors.dateRequired', 'Date is required')}
@@ -104,30 +131,23 @@ export function DateTimeFields({
104
131
  ) : null}
105
132
  </div>
106
133
  {showStartTime && (
107
- <div className="flex flex-1 flex-col gap-1.5">
134
+ <div className="flex min-w-0 flex-1 flex-col gap-1.5">
108
135
  <label className="text-overline font-semibold text-muted-foreground tracking-wider">
109
136
  {getFieldLabel(activityType, 'startTime', t, 'customers.schedule.start', 'Start')}
110
137
  <span aria-hidden="true" className="ml-1 text-status-error-foreground">*</span>
111
138
  </label>
112
- <div
139
+ <TimePicker
140
+ value={startTime || null}
141
+ onChange={(next) => setStartTime(next ?? '')}
142
+ disabled={allDay}
143
+ placeholder={t('customers.schedule.start.placeholder', 'Pick a time')}
113
144
  className={cn(
114
- 'flex items-center gap-2 rounded-md border bg-background px-3 py-2.5',
115
- timeMissing ? 'border-status-error-border' : 'border-border',
145
+ 'py-2.5',
146
+ timeMissing ? 'border-status-error-border' : undefined,
116
147
  )}
117
- >
118
- <Clock className="size-3.5 text-muted-foreground" />
119
- <input
120
- type="time"
121
- value={startTime}
122
- onChange={(e) => setStartTime(e.target.value)}
123
- disabled={allDay}
124
- required={!allDay}
125
- aria-required={!allDay}
126
- aria-invalid={timeMissing ? true : undefined}
127
- aria-describedby={timeMissing ? timeErrorId : undefined}
128
- className="flex-1 bg-transparent text-sm text-foreground focus:outline-none disabled:opacity-50"
129
- />
130
- </div>
148
+ showNowButton
149
+ showClearButton={false}
150
+ />
131
151
  {timeMissing ? (
132
152
  <p id={timeErrorId} className="text-xs text-status-error-foreground">
133
153
  {t('customers.activities.errors.timeRequired', 'Time is required')}
@@ -136,23 +156,29 @@ export function DateTimeFields({
136
156
  </div>
137
157
  )}
138
158
  {showDuration && (
139
- <div className="flex flex-1 flex-col gap-1.5">
159
+ <div className="flex min-w-0 flex-1 flex-col gap-1.5">
140
160
  <label className="text-overline font-semibold text-muted-foreground tracking-wider">
141
161
  {getFieldLabel(activityType, 'duration', t, 'customers.schedule.duration', 'Duration')}
142
162
  </label>
143
- <div className="flex items-center gap-2 rounded-md border border-border bg-background px-3 py-2.5">
144
- <select
145
- value={duration}
146
- onChange={(e) => setDuration(Number(e.target.value))}
147
- disabled={allDay}
148
- className="flex-1 appearance-none bg-transparent text-sm text-foreground focus:outline-none disabled:opacity-50"
149
- >
150
- {DURATION_OPTIONS.map((m) => (
151
- <option key={m} value={m}>{m} min</option>
163
+ <Select
164
+ value={String(duration)}
165
+ onValueChange={(next) => {
166
+ const parsed = Number(next)
167
+ if (Number.isFinite(parsed)) setDuration(parsed)
168
+ }}
169
+ disabled={allDay}
170
+ >
171
+ <SelectTrigger className="h-10">
172
+ <SelectValue placeholder={t('customers.schedule.duration.placeholder', 'Pick duration')} />
173
+ </SelectTrigger>
174
+ <SelectContent>
175
+ {DURATION_OPTIONS.map((option) => (
176
+ <SelectItem key={option.value} value={String(option.value)}>
177
+ {t(option.key, option.fallback)}
178
+ </SelectItem>
152
179
  ))}
153
- </select>
154
- <ChevronDown className="size-3.5 text-muted-foreground" />
155
- </div>
180
+ </SelectContent>
181
+ </Select>
156
182
  </div>
157
183
  )}
158
184
  </div>
@@ -7,8 +7,10 @@ import {
7
7
  Loader2,
8
8
  Plus,
9
9
  Search,
10
+ SearchX,
10
11
  X,
11
12
  } from 'lucide-react'
13
+ import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
12
14
  import { cn } from '@open-mercato/shared/lib/utils'
13
15
  import { useT } from '@open-mercato/shared/lib/i18n/context'
14
16
  import { Avatar } from '@open-mercato/ui/primitives/avatar'
@@ -631,9 +633,11 @@ export function LinkEntityDialog<TDetails = unknown, TLinkSettings = Record<stri
631
633
  {t('customers.linking.searching', 'Searching…')}
632
634
  </div>
633
635
  ) : displayedResults.length === 0 ? (
634
- <div className="rounded-lg border border-dashed border-border/70 px-4 py-6 text-sm text-muted-foreground">
635
- {adapter.searchEmptyHint}
636
- </div>
636
+ <EmptyState
637
+ size="sm"
638
+ icon={<SearchX className="h-8 w-8" aria-hidden="true" />}
639
+ title={adapter.searchEmptyHint}
640
+ />
637
641
  ) : (
638
642
  displayedResults.map((result) => {
639
643
  const checked = draftSet.has(result.id)
@@ -31,7 +31,6 @@ import {
31
31
  ArrowRightLeft,
32
32
  Boxes,
33
33
  CalendarClock,
34
- CircleAlert,
35
34
  Clock3,
36
35
  Gauge,
37
36
  Play,
@@ -964,17 +963,15 @@ export default function SyncRunsDashboardPage() {
964
963
 
965
964
  {selectedIntegration && !selectedIntegration.isEnabled ? (
966
965
  <Alert variant="warning">
967
- <AlertDescription className="inline-flex items-center gap-2">
968
- <CircleAlert className="size-4" />
969
- <span>{t('integrations.detail.state.disabled', 'This integration is disabled. Enable it on the integration settings page before starting a sync.')}</span>
966
+ <AlertDescription>
967
+ {t('integrations.detail.state.disabled', 'This integration is disabled. Enable it on the integration settings page before starting a sync.')}
970
968
  </AlertDescription>
971
969
  </Alert>
972
970
  ) : null}
973
971
  {selectedIntegration && !selectedIntegration.hasCredentials ? (
974
972
  <Alert variant="warning">
975
- <AlertDescription className="inline-flex items-center gap-2">
976
- <CircleAlert className="size-4" />
977
- <span>{t('integrations.detail.credentials.notConfigured', 'Credentials are not configured yet. Save the integration credentials before starting a sync.')}</span>
973
+ <AlertDescription>
974
+ {t('integrations.detail.credentials.notConfigured', 'Credentials are not configured yet. Save the integration credentials before starting a sync.')}
978
975
  </AlertDescription>
979
976
  </Alert>
980
977
  ) : null}
@@ -6,6 +6,7 @@ import { FormHeader } from '@open-mercato/ui/backend/forms'
6
6
  import { Card, CardHeader, CardTitle, CardContent } from '@open-mercato/ui/primitives/card'
7
7
  import { Badge } from '@open-mercato/ui/primitives/badge'
8
8
  import { Button } from '@open-mercato/ui/primitives/button'
9
+ import { LogList, type LogListEntry } from '@open-mercato/ui/backend/LogList'
9
10
  import { Progress } from '@open-mercato/ui/primitives/progress'
10
11
  import { Spinner } from '@open-mercato/ui/primitives/spinner'
11
12
  import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
@@ -78,12 +79,6 @@ const STATUS_STYLES: Record<string, string> = {
78
79
  paused: 'bg-orange-100 text-orange-800',
79
80
  }
80
81
 
81
- const LOG_LEVEL_STYLES: Record<string, string> = {
82
- info: 'bg-blue-100 text-blue-800',
83
- warn: 'bg-yellow-100 text-yellow-800',
84
- error: 'bg-red-100 text-red-800',
85
- }
86
-
87
82
  type SyncRunDetailPageProps = {
88
83
  params?: {
89
84
  id?: string | string[]
@@ -113,7 +108,6 @@ export default function SyncRunDetailPage({ params }: SyncRunDetailPageProps) {
113
108
  const [error, setError] = React.useState<string | null>(null)
114
109
  const [logs, setLogs] = React.useState<LogEntry[]>([])
115
110
  const [isLoadingLogs, setIsLoadingLogs] = React.useState(false)
116
- const [expandedLogId, setExpandedLogId] = React.useState<string | null>(null)
117
111
 
118
112
  const resolveCurrentRunId = React.useCallback(() => {
119
113
  return runId ?? (
@@ -385,54 +379,25 @@ export default function SyncRunDetailPage({ params }: SyncRunDetailPageProps) {
385
379
  <CardContent>
386
380
  {isLoadingLogs ? (
387
381
  <div className="flex justify-center py-4"><Spinner /></div>
388
- ) : logs.length === 0 ? (
389
- <p className="text-muted-foreground text-sm">{t('data_sync.runs.detail.noLogs')}</p>
390
382
  ) : (
391
- <div className="rounded-lg border">
392
- <table className="w-full text-sm">
393
- <thead>
394
- <tr className="border-b bg-muted/50">
395
- <th className="px-4 py-2 text-left font-medium">{t('data_sync.runs.detail.logs.time')}</th>
396
- <th className="px-4 py-2 text-left font-medium">{t('data_sync.runs.detail.logs.level')}</th>
397
- <th className="px-4 py-2 text-left font-medium">{t('data_sync.runs.detail.logs.message')}</th>
398
- </tr>
399
- </thead>
400
- <tbody>
401
- {logs.map((log) => (
402
- <React.Fragment key={log.id}>
403
- <tr className="border-b last:border-0">
404
- <td className="px-4 py-2 text-muted-foreground whitespace-nowrap">
405
- {new Date(log.createdAt).toLocaleString()}
406
- </td>
407
- <td className="px-4 py-2">
408
- <Badge variant="secondary" className={LOG_LEVEL_STYLES[log.level] ?? ''}>
409
- {log.level}
410
- </Badge>
411
- </td>
412
- <td className="px-4 py-2">
413
- <button
414
- type="button"
415
- className="w-full text-left"
416
- onClick={() => setExpandedLogId((current) => current === log.id ? null : log.id)}
417
- >
418
- {log.message}
419
- </button>
420
- </td>
421
- </tr>
422
- {expandedLogId === log.id && log.payload ? (
423
- <tr className="border-b bg-muted/30 last:border-0">
424
- <td colSpan={3} className="px-4 py-4">
425
- <pre className="overflow-x-auto whitespace-pre-wrap rounded-md border bg-card p-3 text-xs">
426
- {JSON.stringify(log.payload, null, 2)}
427
- </pre>
428
- </td>
429
- </tr>
430
- ) : null}
431
- </React.Fragment>
432
- ))}
433
- </tbody>
434
- </table>
435
- </div>
383
+ <LogList
384
+ entries={logs.map<LogListEntry>((log) => ({
385
+ id: log.id,
386
+ time: new Date(log.createdAt).toLocaleString(),
387
+ level: log.level,
388
+ message: log.message,
389
+ body: log.payload ? (
390
+ <pre className="overflow-x-auto whitespace-pre-wrap rounded-md border bg-card p-3 text-xs">
391
+ {JSON.stringify(log.payload, null, 2)}
392
+ </pre>
393
+ ) : (
394
+ <p className="text-sm text-muted-foreground">
395
+ {t('data_sync.runs.detail.logs.noPayload', 'No payload recorded for this log entry.')}
396
+ </p>
397
+ ),
398
+ }))}
399
+ emptyMessage={t('data_sync.runs.detail.noLogs')}
400
+ />
436
401
  )}
437
402
  </CardContent>
438
403
  </Card>
@@ -2,7 +2,8 @@
2
2
 
3
3
  import * as React from 'react'
4
4
  import { useSearchParams } from 'next/navigation'
5
- import { Plus, Pencil, Trash2 } from 'lucide-react'
5
+ import { Book, Plus, Pencil, Trash2 } from 'lucide-react'
6
+ import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
6
7
  import { Button } from '@open-mercato/ui/primitives/button'
7
8
  import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'
8
9
  import { Spinner } from '@open-mercato/ui/primitives/spinner'
@@ -324,9 +325,11 @@ export function DictionariesManager() {
324
325
  readOnly={selectedDictionary.isInherited}
325
326
  />
326
327
  ) : (
327
- <div className="flex h-full flex-col items-center justify-center rounded border border-dashed p-10 text-center text-sm text-muted-foreground">
328
- {t('dictionaries.config.entries.placeholder', 'Select a dictionary to manage its entries.')}
329
- </div>
328
+ <EmptyState
329
+ icon={<Book className="h-8 w-8" aria-hidden="true" />}
330
+ title={t('dictionaries.config.entries.placeholder', 'Select a dictionary to manage its entries.')}
331
+ className="h-full"
332
+ />
330
333
  )}
331
334
  </div>
332
335