@open-mercato/core 0.4.5-develop-636d33c995 → 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 (136) 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/lib/messageObjectPreviews.js +146 -0
  8. package/dist/modules/catalog/lib/messageObjectPreviews.js.map +7 -0
  9. package/dist/modules/catalog/message-objects.js +95 -0
  10. package/dist/modules/catalog/message-objects.js.map +7 -0
  11. package/dist/modules/currencies/backend/currencies/[id]/page.js +21 -0
  12. package/dist/modules/currencies/backend/currencies/[id]/page.js.map +2 -2
  13. package/dist/modules/currencies/lib/messageObjectPreviews.js +51 -0
  14. package/dist/modules/currencies/lib/messageObjectPreviews.js.map +7 -0
  15. package/dist/modules/currencies/message-objects.js +41 -0
  16. package/dist/modules/currencies/message-objects.js.map +7 -0
  17. package/dist/modules/customers/backend/customers/companies/[id]/page.js +20 -0
  18. package/dist/modules/customers/backend/customers/companies/[id]/page.js.map +2 -2
  19. package/dist/modules/customers/backend/customers/deals/[id]/page.js +12 -1
  20. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  21. package/dist/modules/customers/backend/customers/people/[id]/page.js +20 -0
  22. package/dist/modules/customers/backend/customers/people/[id]/page.js.map +2 -2
  23. package/dist/modules/customers/components/detail/CompanyHighlights.js +18 -14
  24. package/dist/modules/customers/components/detail/CompanyHighlights.js.map +2 -2
  25. package/dist/modules/customers/components/detail/PersonHighlights.js +18 -14
  26. package/dist/modules/customers/components/detail/PersonHighlights.js.map +2 -2
  27. package/dist/modules/customers/lib/messageObjectPreviews.js +41 -5
  28. package/dist/modules/customers/lib/messageObjectPreviews.js.map +2 -2
  29. package/dist/modules/customers/message-objects.js +31 -11
  30. package/dist/modules/customers/message-objects.js.map +2 -2
  31. package/dist/modules/messages/commands/messages.js +3 -0
  32. package/dist/modules/messages/commands/messages.js.map +2 -2
  33. package/dist/modules/messages/components/message-detail/panels/objects-panel.js +6 -1
  34. package/dist/modules/messages/components/message-detail/panels/objects-panel.js.map +2 -2
  35. package/dist/modules/messages/components/message-detail/panels/thread-panel.js +4 -1
  36. package/dist/modules/messages/components/message-detail/panels/thread-panel.js.map +2 -2
  37. package/dist/modules/messages/frontend/messages/view/[token]/page.js +1 -0
  38. package/dist/modules/messages/frontend/messages/view/[token]/page.js.map +2 -2
  39. package/dist/modules/resources/backend/resources/resources/[id]/page.js +24 -7
  40. package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
  41. package/dist/modules/resources/lib/messageObjectPreviews.js +43 -0
  42. package/dist/modules/resources/lib/messageObjectPreviews.js.map +7 -0
  43. package/dist/modules/resources/message-objects.js +37 -0
  44. package/dist/modules/resources/message-objects.js.map +7 -0
  45. package/dist/modules/sales/backend/sales/channels/[channelId]/edit/page.js +19 -0
  46. package/dist/modules/sales/backend/sales/channels/[channelId]/edit/page.js.map +2 -2
  47. package/dist/modules/sales/backend/sales/documents/[id]/page.js +23 -2
  48. package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
  49. package/dist/modules/sales/backend/sales/quotes/[id]/page.js +1 -1
  50. package/dist/modules/sales/backend/sales/quotes/[id]/page.js.map +2 -2
  51. package/dist/modules/sales/lib/messageObjectPreviews.js +49 -4
  52. package/dist/modules/sales/lib/messageObjectPreviews.js.map +2 -2
  53. package/dist/modules/sales/message-objects.js +44 -2
  54. package/dist/modules/sales/message-objects.js.map +2 -2
  55. package/dist/modules/sales/widgets/messages/SalesDocumentMessageDetail.js +59 -30
  56. package/dist/modules/sales/widgets/messages/SalesDocumentMessageDetail.js.map +2 -2
  57. package/dist/modules/sales/widgets/messages/SalesDocumentMessagePreview.js +1 -1
  58. package/dist/modules/sales/widgets/messages/SalesDocumentMessagePreview.js.map +1 -1
  59. package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js +8 -30
  60. package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js.map +2 -2
  61. package/dist/modules/staff/backend/staff/my-availability/page.js +13 -0
  62. package/dist/modules/staff/backend/staff/my-availability/page.js.map +2 -2
  63. package/dist/modules/staff/backend/staff/my-leave-requests/[id]/page.js +8 -31
  64. package/dist/modules/staff/backend/staff/my-leave-requests/[id]/page.js.map +2 -2
  65. package/dist/modules/staff/backend/staff/team-members/[id]/page.js +32 -10
  66. package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
  67. package/dist/modules/staff/backend/staff/team-roles/[id]/edit/page.js +14 -1
  68. package/dist/modules/staff/backend/staff/team-roles/[id]/edit/page.js.map +2 -2
  69. package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js +14 -1
  70. package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js.map +2 -2
  71. package/dist/modules/staff/components/TeamForm.js +4 -2
  72. package/dist/modules/staff/components/TeamForm.js.map +2 -2
  73. package/dist/modules/staff/components/TeamRoleForm.js +4 -2
  74. package/dist/modules/staff/components/TeamRoleForm.js.map +2 -2
  75. package/dist/modules/staff/lib/messageObjectPreviews.js +111 -2
  76. package/dist/modules/staff/lib/messageObjectPreviews.js.map +2 -2
  77. package/dist/modules/staff/message-objects.js +79 -8
  78. package/dist/modules/staff/message-objects.js.map +2 -2
  79. package/package.json +2 -2
  80. package/src/modules/catalog/backend/catalog/categories/[id]/edit/page.tsx +19 -5
  81. package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +14 -0
  82. package/src/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.tsx +40 -0
  83. package/src/modules/catalog/lib/messageObjectPreviews.ts +176 -0
  84. package/src/modules/catalog/message-objects.ts +102 -0
  85. package/src/modules/currencies/backend/currencies/[id]/page.tsx +20 -0
  86. package/src/modules/currencies/lib/messageObjectPreviews.ts +65 -0
  87. package/src/modules/currencies/message-objects.ts +40 -0
  88. package/src/modules/customers/backend/customers/companies/[id]/page.tsx +19 -0
  89. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +13 -0
  90. package/src/modules/customers/backend/customers/people/[id]/page.tsx +19 -0
  91. package/src/modules/customers/components/detail/CompanyHighlights.tsx +14 -9
  92. package/src/modules/customers/components/detail/PersonHighlights.tsx +14 -9
  93. package/src/modules/customers/lib/messageObjectPreviews.ts +43 -3
  94. package/src/modules/customers/message-objects.ts +31 -11
  95. package/src/modules/messages/commands/messages.ts +4 -0
  96. package/src/modules/messages/components/message-detail/panels/objects-panel.tsx +8 -1
  97. package/src/modules/messages/components/message-detail/panels/thread-panel.tsx +3 -0
  98. package/src/modules/messages/frontend/messages/view/[token]/page.tsx +1 -0
  99. package/src/modules/resources/backend/resources/resources/[id]/page.tsx +20 -4
  100. package/src/modules/resources/lib/messageObjectPreviews.ts +55 -0
  101. package/src/modules/resources/message-objects.ts +36 -0
  102. package/src/modules/sales/backend/sales/channels/[channelId]/edit/page.tsx +18 -0
  103. package/src/modules/sales/backend/sales/documents/[id]/page.tsx +23 -0
  104. package/src/modules/sales/backend/sales/quotes/[id]/page.tsx +1 -1
  105. package/src/modules/sales/lib/messageObjectPreviews.ts +54 -4
  106. package/src/modules/sales/message-objects.ts +44 -2
  107. package/src/modules/sales/widgets/messages/SalesDocumentMessageDetail.tsx +72 -34
  108. package/src/modules/sales/widgets/messages/SalesDocumentMessagePreview.tsx +1 -1
  109. package/src/modules/staff/backend/staff/leave-requests/[id]/page.tsx +7 -29
  110. package/src/modules/staff/backend/staff/my-availability/page.tsx +14 -0
  111. package/src/modules/staff/backend/staff/my-leave-requests/[id]/page.tsx +8 -30
  112. package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +28 -7
  113. package/src/modules/staff/backend/staff/team-roles/[id]/edit/page.tsx +12 -0
  114. package/src/modules/staff/backend/staff/teams/[id]/edit/page.tsx +12 -0
  115. package/src/modules/staff/components/TeamForm.tsx +3 -0
  116. package/src/modules/staff/components/TeamRoleForm.tsx +3 -0
  117. package/src/modules/staff/lib/messageObjectPreviews.ts +133 -2
  118. package/src/modules/staff/message-objects.ts +79 -8
  119. package/dist/modules/customers/widgets/messages/CustomerMessageObjectDetail.js +0 -51
  120. package/dist/modules/customers/widgets/messages/CustomerMessageObjectDetail.js.map +0 -7
  121. package/dist/modules/customers/widgets/messages/CustomerMessageObjectPreview.js +0 -35
  122. package/dist/modules/customers/widgets/messages/CustomerMessageObjectPreview.js.map +0 -7
  123. package/dist/modules/customers/widgets/messages/index.js +0 -7
  124. package/dist/modules/customers/widgets/messages/index.js.map +0 -7
  125. package/dist/modules/staff/widgets/messages/StaffMessageObjectDetail.js +0 -51
  126. package/dist/modules/staff/widgets/messages/StaffMessageObjectDetail.js.map +0 -7
  127. package/dist/modules/staff/widgets/messages/StaffMessageObjectPreview.js +0 -34
  128. package/dist/modules/staff/widgets/messages/StaffMessageObjectPreview.js.map +0 -7
  129. package/dist/modules/staff/widgets/messages/index.js +0 -7
  130. package/dist/modules/staff/widgets/messages/index.js.map +0 -7
  131. package/src/modules/customers/widgets/messages/CustomerMessageObjectDetail.tsx +0 -57
  132. package/src/modules/customers/widgets/messages/CustomerMessageObjectPreview.tsx +0 -49
  133. package/src/modules/customers/widgets/messages/index.ts +0 -2
  134. package/src/modules/staff/widgets/messages/StaffMessageObjectDetail.tsx +0 -57
  135. package/src/modules/staff/widgets/messages/StaffMessageObjectPreview.tsx +0 -44
  136. package/src/modules/staff/widgets/messages/index.ts +0 -2
@@ -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>
@@ -14,6 +14,7 @@ import { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'
14
14
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
15
15
  import { useT } from '@open-mercato/shared/lib/i18n/context'
16
16
  import { TeamForm, type TeamFormValues, buildTeamPayload } from '@open-mercato/core/modules/staff/components/TeamForm'
17
+ import { SendObjectMessageDialog } from '@open-mercato/ui/backend/messages'
17
18
  import { extractCustomFieldEntries } from '@open-mercato/shared/lib/crud/custom-fields-client'
18
19
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
19
20
  import { Plus } from 'lucide-react'
@@ -333,6 +334,17 @@ export default function StaffTeamEditPage({ params }: { params?: { id?: string }
333
334
  onDelete={handleDelete}
334
335
  isLoading={!initialValues}
335
336
  loadingMessage={t('staff.teams.form.loading', 'Loading team...')}
337
+ extraActions={teamId ? (
338
+ <SendObjectMessageDialog
339
+ object={{
340
+ entityModule: 'staff',
341
+ entityType: 'team',
342
+ entityId: teamId,
343
+ previewData: { title: initialValues?.name ?? ''},
344
+ }}
345
+ viewHref={`/backend/staff/teams/${teamId}/edit`}
346
+ />
347
+ ) : undefined}
336
348
  />
337
349
  ) : (
338
350
  <DataTable<TeamMemberRow>
@@ -24,6 +24,7 @@ export type TeamFormProps = {
24
24
  onDelete?: () => Promise<void>
25
25
  isLoading?: boolean
26
26
  loadingMessage?: string
27
+ extraActions?: React.ReactNode
27
28
  }
28
29
 
29
30
  const normalizeCustomFieldSubmitValue = (value: unknown): unknown => {
@@ -60,6 +61,7 @@ export function TeamForm(props: TeamFormProps) {
60
61
  onDelete,
61
62
  isLoading,
62
63
  loadingMessage,
64
+ extraActions,
63
65
  } = props
64
66
  const t = useT()
65
67
 
@@ -91,6 +93,7 @@ export function TeamForm(props: TeamFormProps) {
91
93
  onDelete={onDelete}
92
94
  isLoading={isLoading}
93
95
  loadingMessage={loadingMessage}
96
+ extraActions={extraActions}
94
97
  />
95
98
  )
96
99
  }
@@ -32,6 +32,7 @@ export type TeamRoleFormProps = {
32
32
  onDelete?: () => Promise<void>
33
33
  isLoading?: boolean
34
34
  loadingMessage?: string
35
+ extraActions?: React.ReactNode
35
36
  }
36
37
 
37
38
  const normalizeCustomFieldSubmitValue = (value: unknown): unknown => {
@@ -75,6 +76,7 @@ export function TeamRoleForm(props: TeamRoleFormProps) {
75
76
  onDelete,
76
77
  isLoading,
77
78
  loadingMessage,
79
+ extraActions,
78
80
  } = props
79
81
  const t = useT()
80
82
 
@@ -154,6 +156,7 @@ export function TeamRoleForm(props: TeamRoleFormProps) {
154
156
  onDelete={onDelete}
155
157
  isLoading={isLoading}
156
158
  loadingMessage={loadingMessage}
159
+ extraActions={extraActions}
157
160
  />
158
161
  )
159
162
  }
@@ -1,9 +1,11 @@
1
1
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
2
- import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
2
+ import { findOneWithDecryption, findWithDecryption } 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 { StaffLeaveRequest, StaffTeam, StaffTeamMember } from '../data/entities'
6
+ import { StaffLeaveRequest, StaffTeam, StaffTeamMember, StaffTeamRole } from '../data/entities'
7
+ import { PlannerAvailabilityRuleSet } from '../../planner/data/entities'
8
+ import { User } from '../../auth/data/entities'
7
9
 
8
10
  type PreviewContext = {
9
11
  tenantId: string
@@ -170,6 +172,56 @@ export async function loadTeamMemberPreview(
170
172
 
171
173
  const tags = Array.isArray(member.tags) ? member.tags : []
172
174
  const metadata: Record<string, string> = {}
175
+ const teamLabel = t('staff.teamMembers.detail.fields.team')
176
+ const userLabel = t('staff.teamMembers.detail.fields.user')
177
+ const rolesLabel = t('staff.teamMembers.detail.fields.roles')
178
+
179
+ if (member.teamId) {
180
+ const team = await findOneWithDecryption(
181
+ em,
182
+ StaffTeam,
183
+ {
184
+ id: member.teamId,
185
+ tenantId: ctx.tenantId,
186
+ organizationId: ctx.organizationId,
187
+ deletedAt: null,
188
+ },
189
+ undefined,
190
+ { tenantId: ctx.tenantId, organizationId: ctx.organizationId },
191
+ )
192
+ if (team?.name) metadata[teamLabel] = team.name
193
+ }
194
+
195
+ if (member.userId) {
196
+ const user = await findOneWithDecryption(
197
+ em,
198
+ User,
199
+ { id: member.userId },
200
+ undefined,
201
+ { tenantId: ctx.tenantId, organizationId: ctx.organizationId },
202
+ )
203
+ if (user?.email) metadata[userLabel] = user.email
204
+ }
205
+
206
+ if (Array.isArray(member.roleIds) && member.roleIds.length > 0) {
207
+ const roles = await findWithDecryption(
208
+ em,
209
+ StaffTeamRole,
210
+ {
211
+ id: { $in: member.roleIds },
212
+ tenantId: ctx.tenantId,
213
+ organizationId: ctx.organizationId,
214
+ deletedAt: null,
215
+ },
216
+ { orderBy: { name: 'ASC' } },
217
+ { tenantId: ctx.tenantId, organizationId: ctx.organizationId },
218
+ )
219
+ const roleNames = roles
220
+ .map((role) => role.name?.trim())
221
+ .filter((name): name is string => Boolean(name && name.length > 0))
222
+ if (roleNames.length > 0) metadata[rolesLabel] = roleNames.join(', ')
223
+ }
224
+
173
225
  if (tags.length > 0) metadata.Tags = tags.slice(0, 5).join(', ')
174
226
 
175
227
  return {
@@ -180,3 +232,82 @@ export async function loadTeamMemberPreview(
180
232
  metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
181
233
  }
182
234
  }
235
+
236
+ export async function loadStaffTeamRolePreview(
237
+ entityId: string,
238
+ ctx: PreviewContext,
239
+ ): Promise<ObjectPreviewData> {
240
+ const { t } = await resolveTranslations()
241
+ const defaultTitle = t('staff.messageObjects.teamRole.title')
242
+
243
+ if (!ctx.organizationId) {
244
+ return { title: defaultTitle, subtitle: entityId }
245
+ }
246
+
247
+ const { resolve } = await createRequestContainer()
248
+ const em = resolve('em') as EntityManager
249
+
250
+ const role = await findOneWithDecryption(
251
+ em,
252
+ StaffTeamRole,
253
+ {
254
+ id: entityId,
255
+ tenantId: ctx.tenantId,
256
+ organizationId: ctx.organizationId,
257
+ deletedAt: null,
258
+ },
259
+ undefined,
260
+ { tenantId: ctx.tenantId, organizationId: ctx.organizationId },
261
+ )
262
+
263
+ if (!role) {
264
+ return {
265
+ title: defaultTitle,
266
+ subtitle: entityId,
267
+ status: t('staff.messageObjects.notFound'),
268
+ statusColor: 'gray',
269
+ }
270
+ }
271
+
272
+ return {
273
+ title: role.name,
274
+ subtitle: role.description ?? undefined,
275
+ }
276
+ }
277
+
278
+ export async function loadStaffAvailabilityPreview(
279
+ entityId: string,
280
+ ctx: PreviewContext,
281
+ ): Promise<ObjectPreviewData> {
282
+ const { t } = await resolveTranslations()
283
+ const defaultTitle = t('staff.messageObjects.myAvailability.title')
284
+
285
+ if (!ctx.organizationId) {
286
+ return { title: defaultTitle, subtitle: entityId }
287
+ }
288
+
289
+ const { resolve } = await createRequestContainer()
290
+ const em = resolve('em') as EntityManager
291
+
292
+ const ruleSet = await findOneWithDecryption(
293
+ em,
294
+ PlannerAvailabilityRuleSet,
295
+ {
296
+ id: entityId,
297
+ tenantId: ctx.tenantId,
298
+ organizationId: ctx.organizationId,
299
+ deletedAt: null,
300
+ },
301
+ undefined,
302
+ { tenantId: ctx.tenantId, organizationId: ctx.organizationId },
303
+ )
304
+
305
+ if (!ruleSet) {
306
+ return { title: defaultTitle, subtitle: entityId }
307
+ }
308
+
309
+ return {
310
+ title: ruleSet.name,
311
+ subtitle: ruleSet.description ?? undefined,
312
+ }
313
+ }