@open-mercato/core 0.4.5-develop-610fbb24ec → 0.4.5-develop-811deeb983

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 (141) hide show
  1. package/dist/modules/catalog/backend/catalog/categories/[id]/edit/page.js +17 -2
  2. package/dist/modules/catalog/backend/catalog/categories/[id]/edit/page.js.map +2 -2
  3. package/dist/modules/catalog/backend/catalog/products/[id]/page.js +15 -0
  4. package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
  5. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js +30 -0
  6. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js.map +2 -2
  7. package/dist/modules/catalog/data/validators.js +4 -3
  8. package/dist/modules/catalog/data/validators.js.map +2 -2
  9. package/dist/modules/catalog/lib/messageObjectPreviews.js +146 -0
  10. package/dist/modules/catalog/lib/messageObjectPreviews.js.map +7 -0
  11. package/dist/modules/catalog/message-objects.js +95 -0
  12. package/dist/modules/catalog/message-objects.js.map +7 -0
  13. package/dist/modules/currencies/backend/currencies/[id]/page.js +21 -0
  14. package/dist/modules/currencies/backend/currencies/[id]/page.js.map +2 -2
  15. package/dist/modules/currencies/lib/messageObjectPreviews.js +51 -0
  16. package/dist/modules/currencies/lib/messageObjectPreviews.js.map +7 -0
  17. package/dist/modules/currencies/message-objects.js +41 -0
  18. package/dist/modules/currencies/message-objects.js.map +7 -0
  19. package/dist/modules/customers/backend/customers/companies/[id]/page.js +20 -0
  20. package/dist/modules/customers/backend/customers/companies/[id]/page.js.map +2 -2
  21. package/dist/modules/customers/backend/customers/deals/[id]/page.js +12 -1
  22. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  23. package/dist/modules/customers/backend/customers/people/[id]/page.js +20 -0
  24. package/dist/modules/customers/backend/customers/people/[id]/page.js.map +2 -2
  25. package/dist/modules/customers/components/detail/CompanyHighlights.js +18 -14
  26. package/dist/modules/customers/components/detail/CompanyHighlights.js.map +2 -2
  27. package/dist/modules/customers/components/detail/PersonHighlights.js +18 -14
  28. package/dist/modules/customers/components/detail/PersonHighlights.js.map +2 -2
  29. package/dist/modules/customers/lib/messageObjectPreviews.js +41 -5
  30. package/dist/modules/customers/lib/messageObjectPreviews.js.map +2 -2
  31. package/dist/modules/customers/message-objects.js +31 -11
  32. package/dist/modules/customers/message-objects.js.map +2 -2
  33. package/dist/modules/messages/commands/messages.js +3 -0
  34. package/dist/modules/messages/commands/messages.js.map +2 -2
  35. package/dist/modules/messages/components/message-detail/panels/objects-panel.js +6 -1
  36. package/dist/modules/messages/components/message-detail/panels/objects-panel.js.map +2 -2
  37. package/dist/modules/messages/components/message-detail/panels/thread-panel.js +4 -1
  38. package/dist/modules/messages/components/message-detail/panels/thread-panel.js.map +2 -2
  39. package/dist/modules/messages/frontend/messages/view/[token]/page.js +1 -0
  40. package/dist/modules/messages/frontend/messages/view/[token]/page.js.map +2 -2
  41. package/dist/modules/resources/backend/resources/resources/[id]/page.js +24 -7
  42. package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
  43. package/dist/modules/resources/lib/messageObjectPreviews.js +43 -0
  44. package/dist/modules/resources/lib/messageObjectPreviews.js.map +7 -0
  45. package/dist/modules/resources/message-objects.js +37 -0
  46. package/dist/modules/resources/message-objects.js.map +7 -0
  47. package/dist/modules/sales/backend/sales/channels/[channelId]/edit/page.js +19 -0
  48. package/dist/modules/sales/backend/sales/channels/[channelId]/edit/page.js.map +2 -2
  49. package/dist/modules/sales/backend/sales/documents/[id]/page.js +23 -2
  50. package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
  51. package/dist/modules/sales/backend/sales/quotes/[id]/page.js +1 -1
  52. package/dist/modules/sales/backend/sales/quotes/[id]/page.js.map +2 -2
  53. package/dist/modules/sales/lib/messageObjectPreviews.js +49 -4
  54. package/dist/modules/sales/lib/messageObjectPreviews.js.map +2 -2
  55. package/dist/modules/sales/message-objects.js +44 -2
  56. package/dist/modules/sales/message-objects.js.map +2 -2
  57. package/dist/modules/sales/widgets/messages/SalesDocumentMessageDetail.js +59 -30
  58. package/dist/modules/sales/widgets/messages/SalesDocumentMessageDetail.js.map +2 -2
  59. package/dist/modules/sales/widgets/messages/SalesDocumentMessagePreview.js +1 -1
  60. package/dist/modules/sales/widgets/messages/SalesDocumentMessagePreview.js.map +1 -1
  61. package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js +8 -30
  62. package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js.map +2 -2
  63. package/dist/modules/staff/backend/staff/my-availability/page.js +13 -0
  64. package/dist/modules/staff/backend/staff/my-availability/page.js.map +2 -2
  65. package/dist/modules/staff/backend/staff/my-leave-requests/[id]/page.js +8 -31
  66. package/dist/modules/staff/backend/staff/my-leave-requests/[id]/page.js.map +2 -2
  67. package/dist/modules/staff/backend/staff/team-members/[id]/page.js +32 -10
  68. package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
  69. package/dist/modules/staff/backend/staff/team-roles/[id]/edit/page.js +14 -1
  70. package/dist/modules/staff/backend/staff/team-roles/[id]/edit/page.js.map +2 -2
  71. package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js +14 -1
  72. package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js.map +2 -2
  73. package/dist/modules/staff/components/TeamForm.js +4 -2
  74. package/dist/modules/staff/components/TeamForm.js.map +2 -2
  75. package/dist/modules/staff/components/TeamRoleForm.js +4 -2
  76. package/dist/modules/staff/components/TeamRoleForm.js.map +2 -2
  77. package/dist/modules/staff/lib/messageObjectPreviews.js +111 -2
  78. package/dist/modules/staff/lib/messageObjectPreviews.js.map +2 -2
  79. package/dist/modules/staff/message-objects.js +79 -8
  80. package/dist/modules/staff/message-objects.js.map +2 -2
  81. package/package.json +3 -3
  82. package/src/modules/catalog/backend/catalog/categories/[id]/edit/page.tsx +19 -5
  83. package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +14 -0
  84. package/src/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.tsx +40 -0
  85. package/src/modules/catalog/data/validators.ts +47 -45
  86. package/src/modules/catalog/lib/messageObjectPreviews.ts +176 -0
  87. package/src/modules/catalog/message-objects.ts +102 -0
  88. package/src/modules/currencies/backend/currencies/[id]/page.tsx +20 -0
  89. package/src/modules/currencies/lib/messageObjectPreviews.ts +65 -0
  90. package/src/modules/currencies/message-objects.ts +40 -0
  91. package/src/modules/customers/backend/customers/companies/[id]/page.tsx +19 -0
  92. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +13 -0
  93. package/src/modules/customers/backend/customers/people/[id]/page.tsx +19 -0
  94. package/src/modules/customers/components/detail/CompanyHighlights.tsx +14 -9
  95. package/src/modules/customers/components/detail/PersonHighlights.tsx +14 -9
  96. package/src/modules/customers/lib/messageObjectPreviews.ts +43 -3
  97. package/src/modules/customers/message-objects.ts +31 -11
  98. package/src/modules/customers/migrations/.snapshot-open-mercato.json +236 -0
  99. package/src/modules/customers/migrations/.snapshot-openmercato.json +236 -0
  100. package/src/modules/messages/commands/messages.ts +4 -0
  101. package/src/modules/messages/components/message-detail/panels/objects-panel.tsx +8 -1
  102. package/src/modules/messages/components/message-detail/panels/thread-panel.tsx +3 -0
  103. package/src/modules/messages/frontend/messages/view/[token]/page.tsx +1 -0
  104. package/src/modules/resources/backend/resources/resources/[id]/page.tsx +20 -4
  105. package/src/modules/resources/lib/messageObjectPreviews.ts +55 -0
  106. package/src/modules/resources/message-objects.ts +36 -0
  107. package/src/modules/sales/backend/sales/channels/[channelId]/edit/page.tsx +18 -0
  108. package/src/modules/sales/backend/sales/documents/[id]/page.tsx +23 -0
  109. package/src/modules/sales/backend/sales/quotes/[id]/page.tsx +1 -1
  110. package/src/modules/sales/lib/messageObjectPreviews.ts +54 -4
  111. package/src/modules/sales/message-objects.ts +44 -2
  112. package/src/modules/sales/widgets/messages/SalesDocumentMessageDetail.tsx +72 -34
  113. package/src/modules/sales/widgets/messages/SalesDocumentMessagePreview.tsx +1 -1
  114. package/src/modules/staff/backend/staff/leave-requests/[id]/page.tsx +7 -29
  115. package/src/modules/staff/backend/staff/my-availability/page.tsx +14 -0
  116. package/src/modules/staff/backend/staff/my-leave-requests/[id]/page.tsx +8 -30
  117. package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +28 -7
  118. package/src/modules/staff/backend/staff/team-roles/[id]/edit/page.tsx +12 -0
  119. package/src/modules/staff/backend/staff/teams/[id]/edit/page.tsx +12 -0
  120. package/src/modules/staff/components/TeamForm.tsx +3 -0
  121. package/src/modules/staff/components/TeamRoleForm.tsx +3 -0
  122. package/src/modules/staff/lib/messageObjectPreviews.ts +133 -2
  123. package/src/modules/staff/message-objects.ts +79 -8
  124. package/dist/modules/customers/widgets/messages/CustomerMessageObjectDetail.js +0 -51
  125. package/dist/modules/customers/widgets/messages/CustomerMessageObjectDetail.js.map +0 -7
  126. package/dist/modules/customers/widgets/messages/CustomerMessageObjectPreview.js +0 -35
  127. package/dist/modules/customers/widgets/messages/CustomerMessageObjectPreview.js.map +0 -7
  128. package/dist/modules/customers/widgets/messages/index.js +0 -7
  129. package/dist/modules/customers/widgets/messages/index.js.map +0 -7
  130. package/dist/modules/staff/widgets/messages/StaffMessageObjectDetail.js +0 -51
  131. package/dist/modules/staff/widgets/messages/StaffMessageObjectDetail.js.map +0 -7
  132. package/dist/modules/staff/widgets/messages/StaffMessageObjectPreview.js +0 -34
  133. package/dist/modules/staff/widgets/messages/StaffMessageObjectPreview.js.map +0 -7
  134. package/dist/modules/staff/widgets/messages/index.js +0 -7
  135. package/dist/modules/staff/widgets/messages/index.js.map +0 -7
  136. package/src/modules/customers/widgets/messages/CustomerMessageObjectDetail.tsx +0 -57
  137. package/src/modules/customers/widgets/messages/CustomerMessageObjectPreview.tsx +0 -49
  138. package/src/modules/customers/widgets/messages/index.ts +0 -2
  139. package/src/modules/staff/widgets/messages/StaffMessageObjectDetail.tsx +0 -57
  140. package/src/modules/staff/widgets/messages/StaffMessageObjectPreview.tsx +0 -44
  141. package/src/modules/staff/widgets/messages/index.ts +0 -2
@@ -0,0 +1,36 @@
1
+ import type { MessageObjectTypeDefinition } from '@open-mercato/shared/modules/messages/types'
2
+ import { MessageObjectDetail, MessageObjectPreview } from '@open-mercato/ui/backend/messages'
3
+
4
+ const objectMessageTypes = ['default', 'messages.defaultWithObjects']
5
+
6
+ export const messageObjectTypes: MessageObjectTypeDefinition[] = [
7
+ {
8
+ module: 'resources',
9
+ entityType: 'resource',
10
+ messageTypes: objectMessageTypes,
11
+ entityId: 'resources:resources_resource',
12
+ optionLabelField: 'name',
13
+ optionSubtitleField: 'description',
14
+ labelKey: 'resources.messageObjects.resource.title',
15
+ icon: 'package',
16
+ PreviewComponent: MessageObjectPreview,
17
+ DetailComponent: MessageObjectDetail,
18
+ actions: [
19
+ {
20
+ id: 'view',
21
+ labelKey: 'common.view',
22
+ variant: 'outline',
23
+ href: '/backend/resources/resources/{entityId}',
24
+ },
25
+ ],
26
+ loadPreview: async (entityId, ctx) => {
27
+ if (typeof window !== 'undefined') {
28
+ return { title: 'Resource', subtitle: entityId }
29
+ }
30
+ const { loadResourcePreview } = await import('./lib/messageObjectPreviews')
31
+ return loadResourcePreview(entityId, ctx)
32
+ },
33
+ },
34
+ ]
35
+
36
+ export default messageObjectTypes
@@ -12,6 +12,7 @@ import { useT } from '@open-mercato/shared/lib/i18n/context'
12
12
  import { useChannelFields, buildChannelPayload, type ChannelFormValues } from '@open-mercato/core/modules/sales/components/channels/channelFormFields'
13
13
  import { E } from '#generated/entities.ids.generated'
14
14
  import { SalesChannelOffersPanel } from '@open-mercato/core/modules/sales/components/channels/SalesChannelOffersPanel'
15
+ import { SendObjectMessageDialog } from '@open-mercato/ui/backend/messages'
15
16
 
16
17
  type ChannelApiResponse = {
17
18
  items?: Array<Record<string, unknown>>
@@ -126,6 +127,23 @@ export default function EditChannelPage({ params }: { params?: { channelId?: str
126
127
  <CrudForm<ChannelFormValues>
127
128
  title={t('sales.channels.form.editTitle', 'Edit channel')}
128
129
  versionHistory={{ resourceKind: 'sales.channel', resourceId: channelId ? String(channelId) : '' }}
130
+ extraActions={channelId ? (
131
+ <SendObjectMessageDialog
132
+ object={{
133
+ entityModule: 'sales',
134
+ entityType: 'channel',
135
+ entityId: channelId,
136
+ previewData: {
137
+ title: initialValues?.name ?? '',
138
+ metadata: {
139
+ [t('sales.channels.form.contactEmail')]: initialValues?.contactEmail ?? '-',
140
+ [t('sales.channels.form.websiteUrl')]: initialValues?.websiteUrl ?? '-',
141
+ },
142
+ },
143
+ }}
144
+ viewHref={`/backend/sales/channels/${channelId}/edit`}
145
+ />
146
+ ) : undefined}
129
147
  entityId={E.sales.sales_channel}
130
148
  fields={fields}
131
149
  groups={[
@@ -63,6 +63,16 @@ import { readMarkdownPreferenceCookie, writeMarkdownPreferenceCookie } from '@op
63
63
  import { InjectionSpot, useInjectionWidgets } from '@open-mercato/ui/backend/injection/InjectionSpot'
64
64
  import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
65
65
 
66
+ function formatMessageAmount(amount: number | null | undefined, currency: string | null | undefined): string | null {
67
+ if (typeof amount !== 'number' || !Number.isFinite(amount)) return null
68
+ if (!currency) return amount.toLocaleString()
69
+ try {
70
+ return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(amount)
71
+ } catch {
72
+ return `${amount.toLocaleString()} ${currency}`
73
+ }
74
+ }
75
+
66
76
  function CurrencyInlineEditor({
67
77
  label,
68
78
  value,
@@ -1844,9 +1854,11 @@ function StatusInlineEditor({
1844
1854
  export default function SalesDocumentDetailPage({
1845
1855
  params,
1846
1856
  initialKind,
1857
+ includeAmountInMessageMetadata,
1847
1858
  }: {
1848
1859
  params: { id: string }
1849
1860
  initialKind?: 'order' | 'quote'
1861
+ includeAmountInMessageMetadata?: boolean
1850
1862
  }) {
1851
1863
  const t = useT()
1852
1864
  const router = useRouter()
@@ -2729,6 +2741,11 @@ export default function SalesDocumentDetailPage({
2729
2741
  : null
2730
2742
  const contactEmail = resolveCustomerEmail(customerSnapshot) ?? metadataEmail ?? record?.contactEmail ?? null
2731
2743
  const statusDisplay = record?.status ? statusDictionaryMap[record.status] ?? null : null
2744
+ const previewAmount = formatMessageAmount(record?.grandTotalGrossAmount ?? null, record?.currencyCode ?? null)
2745
+ const messagePreviewMetadata: Record<string, string> = {}
2746
+ if (includeAmountInMessageMetadata && previewAmount) {
2747
+ messagePreviewMetadata[t('sales.documents.detail.totals.grandTotalGross')] = previewAmount
2748
+ }
2732
2749
  const contactRecordId = customerSnapshot?.contact?.id ?? customerSnapshot?.customer?.id ?? record?.customerEntityId ?? null
2733
2750
  const resolveAdjustmentLabel = React.useCallback(
2734
2751
  (row: AdjustmentRowData) => {
@@ -4434,7 +4451,13 @@ export default function SalesDocumentDetailPage({
4434
4451
  entityId: record.id,
4435
4452
  sourceEntityType: kind === 'order' ? 'sales.order' : 'sales.quote',
4436
4453
  sourceEntityId: record.id,
4454
+ previewData: {
4455
+ title: number,
4456
+ status: statusDisplay?.label ?? record?.status ?? undefined,
4457
+ metadata: Object.keys(messagePreviewMetadata).length > 0 ? messagePreviewMetadata : undefined,
4458
+ },
4437
4459
  }}
4460
+ viewHref={`/backend/sales/${kind === 'order' ? 'orders' : 'quotes'}/${record.id}`}
4438
4461
  defaultValues={{
4439
4462
  sourceEntityType: kind === 'order' ? 'sales.order' : 'sales.quote',
4440
4463
  sourceEntityId: record.id,
@@ -3,5 +3,5 @@
3
3
  import SalesDocumentDetailPage from '../../documents/[id]/page'
4
4
 
5
5
  export default function SalesQuoteDetailPage(props: { params: { id: string } }) {
6
- return <SalesDocumentDetailPage {...props} initialKind="quote" />
6
+ return <SalesDocumentDetailPage {...props} initialKind="quote" includeAmountInMessageMetadata />
7
7
  }
@@ -3,7 +3,7 @@ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
3
3
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
4
4
  import type { ObjectPreviewData } from '@open-mercato/shared/modules/messages/types'
5
5
  import type { EntityManager } from '@mikro-orm/postgresql'
6
- import { SalesOrder, SalesQuote } from '../data/entities'
6
+ import { SalesChannel, SalesOrder, SalesQuote } from '../data/entities'
7
7
 
8
8
  type PreviewContext = {
9
9
  tenantId: string
@@ -84,9 +84,12 @@ async function buildPreview(kind: DocumentKind, entityId: string, record: SalesD
84
84
  const subtitle = subtitleParts.length > 0 ? subtitleParts.join(' • ') : entityId
85
85
 
86
86
  const metadata: Record<string, string> = {}
87
- if (number) metadata.Number = number
88
- if (customerName) metadata.Customer = customerName
89
- if (total) metadata.Total = total
87
+ const numberLabel = t('sales.documents.detail.number')
88
+ const customerLabel = t('sales.documents.detail.customer')
89
+ const totalLabel = t('sales.documents.detail.totals.grandTotalGross')
90
+ if (number) metadata[numberLabel] = number
91
+ if (customerName) metadata[customerLabel] = customerName
92
+ if (total) metadata[totalLabel] = total
90
93
 
91
94
  return {
92
95
  title: number && number.trim().length > 0 ? number : defaultTitle,
@@ -147,4 +150,51 @@ export async function loadSalesOrderPreview(entityId: string, ctx: PreviewContex
147
150
  return await buildPreview('order', entityId, record)
148
151
  }
149
152
 
153
+ export async function loadSalesChannelPreview(entityId: string, ctx: PreviewContext): Promise<ObjectPreviewData> {
154
+ const { t } = await resolveTranslations()
155
+ const defaultTitle = t('sales.messageObjects.channel.title')
156
+
157
+ if (!ctx.organizationId) {
158
+ return { title: defaultTitle, subtitle: entityId }
159
+ }
160
+
161
+ const { resolve } = await createRequestContainer()
162
+ const em = resolve('em') as EntityManager
163
+ const entity = await findOneWithDecryption(
164
+ em,
165
+ SalesChannel,
166
+ {
167
+ id: entityId,
168
+ tenantId: ctx.tenantId,
169
+ organizationId: ctx.organizationId,
170
+ deletedAt: null,
171
+ },
172
+ undefined,
173
+ { tenantId: ctx.tenantId, organizationId: ctx.organizationId },
174
+ )
175
+
176
+ if (!entity) {
177
+ return {
178
+ title: defaultTitle,
179
+ subtitle: entityId,
180
+ status: t('sales.messageObjects.notFound'),
181
+ statusColor: 'gray',
182
+ }
183
+ }
184
+
185
+ const metadata: Record<string, string> = {}
186
+ const contactEmailLabel = t('sales.channels.form.contactEmail')
187
+ const websiteUrlLabel = t('sales.channels.form.websiteUrl')
188
+ if (entity.contactEmail && entity.contactEmail.trim().length > 0) metadata[contactEmailLabel] = entity.contactEmail
189
+ if (entity.websiteUrl && entity.websiteUrl.trim().length > 0) metadata[websiteUrlLabel] = entity.websiteUrl
190
+
191
+ return {
192
+ title: entity.name,
193
+ subtitle: entity.status ?? undefined,
194
+ status: entity.status ?? undefined,
195
+ statusColor: statusColor(entity.status),
196
+ metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
197
+ }
198
+ }
199
+
150
200
 
@@ -1,4 +1,5 @@
1
1
  import type { MessageObjectTypeDefinition } from '@open-mercato/shared/modules/messages/types'
2
+ import { MessageObjectDetail, MessageObjectPreview } from '@open-mercato/ui/backend/messages'
2
3
  import { SalesDocumentMessageDetail } from './widgets/messages/SalesDocumentMessageDetail'
3
4
  import { SalesDocumentMessagePreview } from './widgets/messages/SalesDocumentMessagePreview'
4
5
 
@@ -16,7 +17,14 @@ export const messageObjectTypes: MessageObjectTypeDefinition[] = [
16
17
  icon: 'receipt-text',
17
18
  PreviewComponent: SalesDocumentMessagePreview,
18
19
  DetailComponent: SalesDocumentMessageDetail,
19
- actions: [],
20
+ actions: [
21
+ {
22
+ id: 'view',
23
+ labelKey: 'common.view',
24
+ variant: 'outline',
25
+ href: '/backend/sales/orders/{entityId}',
26
+ },
27
+ ],
20
28
  loadPreview: async (entityId, ctx) => {
21
29
  if (typeof window !== 'undefined') {
22
30
  return {
@@ -39,7 +47,14 @@ export const messageObjectTypes: MessageObjectTypeDefinition[] = [
39
47
  icon: 'file-text',
40
48
  PreviewComponent: SalesDocumentMessagePreview,
41
49
  DetailComponent: SalesDocumentMessageDetail,
42
- actions: [],
50
+ actions: [
51
+ {
52
+ id: 'view',
53
+ labelKey: 'common.view',
54
+ variant: 'outline',
55
+ href: '/backend/sales/quotes/{entityId}',
56
+ },
57
+ ],
43
58
  loadPreview: async (entityId, ctx) => {
44
59
  if (typeof window !== 'undefined') {
45
60
  return {
@@ -51,6 +66,33 @@ export const messageObjectTypes: MessageObjectTypeDefinition[] = [
51
66
  return loadSalesQuotePreview(entityId, ctx)
52
67
  },
53
68
  },
69
+ {
70
+ module: 'sales',
71
+ entityType: 'channel',
72
+ messageTypes: objectMessageTypes,
73
+ entityId: 'sales:sales_channel',
74
+ optionLabelField: 'name',
75
+ optionSubtitleField: 'status',
76
+ labelKey: 'sales.messageObjects.channel.title',
77
+ icon: 'store',
78
+ PreviewComponent: MessageObjectPreview,
79
+ DetailComponent: MessageObjectDetail,
80
+ actions: [
81
+ {
82
+ id: 'view',
83
+ labelKey: 'common.view',
84
+ variant: 'outline',
85
+ href: '/backend/sales/channels/{entityId}/edit',
86
+ },
87
+ ],
88
+ loadPreview: async (entityId, ctx) => {
89
+ if (typeof window !== 'undefined') {
90
+ return { title: 'Sales channel', subtitle: entityId }
91
+ }
92
+ const { loadSalesChannelPreview } = await import('./lib/messageObjectPreviews')
93
+ return loadSalesChannelPreview(entityId, ctx)
94
+ },
95
+ },
54
96
  ]
55
97
 
56
98
  export default messageObjectTypes
@@ -1,52 +1,90 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from 'react'
4
+ import Link from 'next/link'
4
5
  import { useT } from '@open-mercato/shared/lib/i18n/context'
5
6
  import type { ObjectDetailProps } from '@open-mercato/shared/modules/messages/types'
6
7
  import { Button } from '@open-mercato/ui/primitives/button'
7
8
  import { SalesDocumentMessagePreview } from './SalesDocumentMessagePreview'
8
9
 
10
+ function resolveActionHref(template: string, entityId: string): string {
11
+ return template.replace('{entityId}', encodeURIComponent(entityId))
12
+ }
13
+
9
14
  export function SalesDocumentMessageDetail(props: ObjectDetailProps) {
10
15
  const t = useT()
11
16
  const [executingActionId, setExecutingActionId] = React.useState<string | null>(null)
12
17
 
18
+ const viewAction = props.actions.find((a) => a.id === 'view')
19
+ const otherActions = props.actions.filter((a) => a.id !== 'view')
20
+
21
+ const preview = (
22
+ <SalesDocumentMessagePreview
23
+ entityId={props.entityId}
24
+ entityModule={props.entityModule}
25
+ entityType={props.entityType}
26
+ snapshot={props.snapshot}
27
+ previewData={props.previewData}
28
+ actionRequired={props.actionRequired}
29
+ actionType={props.actionType}
30
+ actionLabel={props.actionLabel}
31
+ />
32
+ )
33
+
13
34
  return (
14
35
  <div className="space-y-3 rounded border p-3">
15
- <SalesDocumentMessagePreview
16
- entityId={props.entityId}
17
- entityModule={props.entityModule}
18
- entityType={props.entityType}
19
- snapshot={props.snapshot}
20
- previewData={props.previewData}
21
- actionRequired={props.actionRequired}
22
- actionType={props.actionType}
23
- actionLabel={props.actionLabel}
24
- />
25
-
26
- {props.actions.length > 0 ? (
36
+ {viewAction?.href ? (
37
+ <Link
38
+ href={resolveActionHref(viewAction.href, props.entityId)}
39
+ className="block rounded-md transition-opacity hover:opacity-75"
40
+ >
41
+ {preview}
42
+ </Link>
43
+ ) : (
44
+ preview
45
+ )}
46
+
47
+ {otherActions.length > 0 ? (
27
48
  <div className="flex flex-wrap gap-2">
28
- {props.actions.map((action) => (
29
- <Button
30
- key={action.id}
31
- type="button"
32
- size="sm"
33
- variant={action.variant ?? 'default'}
34
- disabled={executingActionId !== null}
35
- onClick={async () => {
36
- if (executingActionId) return
37
- setExecutingActionId(action.id)
38
- try {
39
- await props.onAction(action.id, { id: props.entityId })
40
- } finally {
41
- setExecutingActionId(null)
42
- }
43
- }}
44
- >
45
- {executingActionId === action.id
46
- ? t('messages.actions.executing', 'Executing...')
47
- : t(action.labelKey ?? action.id, action.id)}
48
- </Button>
49
- ))}
49
+ {otherActions.map((action) => {
50
+ if (action.href) {
51
+ return (
52
+ <Button
53
+ key={action.id}
54
+ type="button"
55
+ size="sm"
56
+ variant={action.variant ?? 'default'}
57
+ asChild
58
+ >
59
+ <Link href={resolveActionHref(action.href, props.entityId)}>
60
+ {t(action.labelKey ?? action.id, action.id)}
61
+ </Link>
62
+ </Button>
63
+ )
64
+ }
65
+ return (
66
+ <Button
67
+ key={action.id}
68
+ type="button"
69
+ size="sm"
70
+ variant={action.variant ?? 'default'}
71
+ disabled={executingActionId !== null}
72
+ onClick={async () => {
73
+ if (executingActionId) return
74
+ setExecutingActionId(action.id)
75
+ try {
76
+ await props.onAction(action.id, { id: props.entityId })
77
+ } finally {
78
+ setExecutingActionId(null)
79
+ }
80
+ }}
81
+ >
82
+ {executingActionId === action.id
83
+ ? t('messages.actions.executing', 'Executing...')
84
+ : t(action.labelKey ?? action.id, action.id)}
85
+ </Button>
86
+ )
87
+ })}
50
88
  </div>
51
89
  ) : null}
52
90
  </div>
@@ -19,7 +19,7 @@ export function SalesDocumentMessagePreview({
19
19
  ? t('sales.documents.detail.quote', 'Sales quote')
20
20
  : t('sales.documents.detail.order', 'Sales order')
21
21
  const title = previewData?.title || fallbackTitle
22
- const subtitle = previewData?.subtitle || entityId
22
+ const subtitle = previewData?.subtitle || ""
23
23
 
24
24
  return (
25
25
  <div className="flex items-start gap-3 rounded-md border bg-muted/20 p-3">
@@ -112,21 +112,7 @@ export default function StaffLeaveRequestDetailPage({ params }: { params?: { id?
112
112
  note: record?.note ?? null,
113
113
  }), [record, memberLabel])
114
114
 
115
- const messageContextPreview = React.useMemo(() => (
116
- <div className="space-y-1">
117
- <p className="font-medium">{t('staff.leaveRequests.messages.contextTitle', 'Linked leave request')}</p>
118
- {memberLabel ? (
119
- <p className="text-xs text-muted-foreground">
120
- {t('staff.leaveRequests.detail.member', 'Team member')}: {memberLabel}
121
- </p>
122
- ) : null}
123
- <p className="text-xs text-muted-foreground">
124
- {t('staff.leaveRequests.detail.dates', 'Dates')}: {dateSummary}
125
- </p>
126
- </div>
127
- ), [dateSummary, memberLabel, t])
128
-
129
- const handleSubmit = React.useCallback(async (values: LeaveRequestFormValues) => {
115
+ const handleSubmit = React.useCallback(async (values: LeaveRequestFormValues) => {
130
116
  if (!record?.id) return
131
117
  const payload = buildLeaveRequestPayload(values, { id: record.id })
132
118
  await updateCrud('staff/leave-requests', payload, {
@@ -239,7 +225,13 @@ export default function StaffLeaveRequestDetailPage({ params }: { params?: { id?
239
225
  entityId: record.id,
240
226
  sourceEntityType: 'staff:leave_request',
241
227
  sourceEntityId: record.id,
228
+ previewData: {
229
+ title: memberLabel || t('staff.leaveRequests.messages.contextTitle', 'Linked leave request'),
230
+ subtitle: dateSummary || undefined,
231
+ status: record?.status ?? undefined,
232
+ },
242
233
  }}
234
+ viewHref={`/backend/staff/leave-requests/${record.id}`}
243
235
  lockedType="staff.leave_request_approval"
244
236
  requiredActionConfig={{
245
237
  mode: 'required',
@@ -253,20 +245,6 @@ export default function StaffLeaveRequestDetailPage({ params }: { params?: { id?
253
245
  subject: t('staff.leaveRequests.messages.compose.subject', 'Leave request approval needed'),
254
246
  body: t('staff.leaveRequests.messages.compose.body', 'Please review this leave request and take action.'),
255
247
  }}
256
- contextPreview={messageContextPreview}
257
- renderTrigger={({ openComposer, disabled }) => (
258
- <Button
259
- type="button"
260
- size="icon"
261
- variant="outline"
262
- onClick={openComposer}
263
- disabled={disabled}
264
- aria-label={t('staff.leaveRequests.messages.compose.action', 'Send for review')}
265
- title={t('staff.leaveRequests.messages.compose.action', 'Send for review')}
266
- >
267
- <Send className="h-4 w-4" />
268
- </Button>
269
- )}
270
248
  />
271
249
  ) : null}
272
250
  />
@@ -9,6 +9,7 @@ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
9
9
  import { AvailabilityRulesEditor } from '@open-mercato/core/modules/planner/components/AvailabilityRulesEditor'
10
10
  import { buildMemberScheduleItems } from '@open-mercato/core/modules/staff/lib/memberSchedule'
11
11
  import { useT } from '@open-mercato/shared/lib/i18n/context'
12
+ import { SendObjectMessageDialog } from '@open-mercato/ui/backend/messages'
12
13
 
13
14
  type SelfMemberResponse = {
14
15
  member?: {
@@ -112,6 +113,19 @@ export default function StaffMyAvailabilityPage() {
112
113
  <Page>
113
114
  <PageBody>
114
115
  <div className="space-y-4">
116
+ {member.availabilityRuleSetId ? (
117
+ <div className="flex justify-end">
118
+ <SendObjectMessageDialog
119
+ object={{
120
+ entityModule: 'staff',
121
+ entityType: 'my_availability',
122
+ entityId: member.availabilityRuleSetId,
123
+ previewData: { title: member.displayName ?? t('staff.myAvailability.title', 'My Availability') },
124
+ }}
125
+ viewHref="/backend/staff/my-availability"
126
+ />
127
+ </div>
128
+ ) : null}
115
129
  {!canManageAvailability ? (
116
130
  <div className="space-y-2 rounded-lg border bg-card p-4 text-sm text-muted-foreground">
117
131
  <p className="font-medium text-foreground">
@@ -2,7 +2,7 @@
2
2
 
3
3
  import * as React from 'react'
4
4
  import { useRouter } from 'next/navigation'
5
- import { Leaf } from 'lucide-react'
5
+ import { Send } from 'lucide-react'
6
6
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
7
7
  import { Badge } from '@open-mercato/ui/primitives/badge'
8
8
  import { Button } from '@open-mercato/ui/primitives/button'
@@ -102,21 +102,7 @@ export default function StaffMyLeaveRequestDetailPage({ params }: { params?: { i
102
102
  record?.startDate ?? record?.start_date ?? null,
103
103
  record?.endDate ?? record?.end_date ?? null,
104
104
  )
105
- const messageContextPreview = React.useMemo(() => (
106
- <div className="space-y-1">
107
- <p className="font-medium">{t('staff.leaveRequests.messages.contextTitle', 'Linked leave request')}</p>
108
- {memberLabel ? (
109
- <p className="text-xs text-muted-foreground">
110
- {t('staff.leaveRequests.detail.member', 'Team member')}: {memberLabel}
111
- </p>
112
- ) : null}
113
- <p className="text-xs text-muted-foreground">
114
- {t('staff.leaveRequests.detail.dates', 'Dates')}: {dateSummary}
115
- </p>
116
- </div>
117
- ), [dateSummary, memberLabel, t])
118
-
119
- const handleSubmit = React.useCallback(async (values: LeaveRequestFormValues) => {
105
+ const handleSubmit = React.useCallback(async (values: LeaveRequestFormValues) => {
120
106
  if (!record?.id) return
121
107
  const payload = buildLeaveRequestPayload(values, { id: record.id })
122
108
  await updateCrud('staff/leave-requests', payload, {
@@ -186,7 +172,13 @@ export default function StaffMyLeaveRequestDetailPage({ params }: { params?: { i
186
172
  entityId: record.id,
187
173
  sourceEntityType: 'staff:leave_request',
188
174
  sourceEntityId: record.id,
175
+ previewData: {
176
+ title: memberLabel || t('staff.leaveRequests.messages.contextTitle', 'Linked leave request'),
177
+ subtitle: dateSummary || undefined,
178
+ status: record?.status ?? undefined,
179
+ },
189
180
  }}
181
+ viewHref={`/backend/staff/leave-requests/${record.id}`}
190
182
  lockedType="staff.leave_request_approval"
191
183
  requiredActionConfig={{
192
184
  mode: 'required',
@@ -200,20 +192,6 @@ export default function StaffMyLeaveRequestDetailPage({ params }: { params?: { i
200
192
  subject: t('staff.leaveRequests.messages.compose.subject', 'Leave request approval needed'),
201
193
  body: t('staff.leaveRequests.messages.compose.body', 'Please review this leave request and take action.'),
202
194
  }}
203
- contextPreview={messageContextPreview}
204
- renderTrigger={({ openComposer, disabled }) => (
205
- <Button
206
- type="button"
207
- size="icon"
208
- variant="outline"
209
- onClick={openComposer}
210
- disabled={disabled}
211
- aria-label={t('staff.leaveRequests.messages.compose.action', 'Send for review')}
212
- title={t('staff.leaveRequests.messages.compose.action', 'Send for review')}
213
- >
214
- <Leaf className="h-4 w-4" />
215
- </Button>
216
- )}
217
195
  />
218
196
  ) : null}
219
197
  />
@@ -31,6 +31,7 @@ import { JobHistorySection } from '@open-mercato/core/modules/staff/components/d
31
31
  import type { DictionarySelectLabels } from '@open-mercato/core/modules/dictionaries/components/DictionaryEntrySelect'
32
32
  import { Plus } from 'lucide-react'
33
33
  import { TranslationDrawerAction } from '@open-mercato/core/modules/translations/components/TranslationDrawerAction'
34
+ import { SendObjectMessageDialog } from '@open-mercato/ui/backend/messages'
34
35
 
35
36
  const MARKDOWN_CLASSNAME =
36
37
  'text-sm text-muted-foreground break-words [&>*]:mb-2 [&>*:last-child]:mb-0 [&_ul]:ml-4 [&_ul]:list-disc [&_ol]:ml-4 [&_ol]:list-decimal [&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:p-3 [&_pre]:text-xs'
@@ -325,13 +326,33 @@ export default function StaffTeamMemberDetailPage({ params }: { params?: { id?:
325
326
  </p>
326
327
  </div>
327
328
  </div>
328
- <TranslationDrawerAction
329
- config={memberId ? {
330
- entityType: 'staff:staff_team_member',
331
- recordId: memberId,
332
- baseValues: memberRecord ?? undefined,
333
- } : null}
334
- />
329
+ <div className="flex items-center gap-2">
330
+ {memberId ? (
331
+ <SendObjectMessageDialog
332
+ object={{
333
+ entityModule: 'staff',
334
+ entityType: 'team_member',
335
+ entityId: memberId,
336
+ previewData: {
337
+ title: displayName,
338
+ metadata: {
339
+ [t('staff.teamMembers.detail.fields.team')]: teamLabel,
340
+ [t('staff.teamMembers.detail.fields.user')]: userEmail ?? t('staff.teamMembers.detail.fields.userEmpty', 'No user linked'),
341
+ [t('staff.teamMembers.detail.fields.roles')]: roleLabels.join(', '),
342
+ },
343
+ },
344
+ }}
345
+ viewHref={`/backend/staff/team-members/${memberId}`}
346
+ />
347
+ ) : null}
348
+ <TranslationDrawerAction
349
+ config={memberId ? {
350
+ entityType: 'staff:staff_team_member',
351
+ recordId: memberId,
352
+ baseValues: memberRecord ?? undefined,
353
+ } : null}
354
+ />
355
+ </div>
335
356
  </div>
336
357
 
337
358
  <div className="border-b">
@@ -8,6 +8,7 @@ import { updateCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
8
8
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
9
9
  import { useT } from '@open-mercato/shared/lib/i18n/context'
10
10
  import { TeamRoleForm, type TeamRoleFormValues, type TeamRoleOption, buildTeamRolePayload } from '@open-mercato/core/modules/staff/components/TeamRoleForm'
11
+ import { SendObjectMessageDialog } from '@open-mercato/ui/backend/messages'
11
12
  import { extractCustomFieldEntries } from '@open-mercato/shared/lib/crud/custom-fields-client'
12
13
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
13
14
 
@@ -143,6 +144,17 @@ export default function StaffTeamRoleEditPage({ params }: { params?: { id?: stri
143
144
  onDelete={handleDelete}
144
145
  isLoading={!initialValues}
145
146
  loadingMessage={t('staff.teamRoles.form.loading', 'Loading team role...')}
147
+ extraActions={roleId ? (
148
+ <SendObjectMessageDialog
149
+ object={{
150
+ entityModule: 'staff',
151
+ entityType: 'team_role',
152
+ entityId: roleId,
153
+ previewData: { title: initialValues?.name ?? ''},
154
+ }}
155
+ viewHref={`/backend/staff/team-roles/${roleId}/edit`}
156
+ />
157
+ ) : undefined}
146
158
  />
147
159
  </PageBody>
148
160
  </Page>