@open-mercato/core 0.4.6-develop-f7d3079656 → 0.4.6-develop-0861f05ea9

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 (107) hide show
  1. package/dist/modules/currencies/backend/exchange-rates/[id]/page.js +17 -154
  2. package/dist/modules/currencies/backend/exchange-rates/[id]/page.js.map +3 -3
  3. package/dist/modules/currencies/backend/exchange-rates/create/page.js +14 -152
  4. package/dist/modules/currencies/backend/exchange-rates/create/page.js.map +2 -2
  5. package/dist/modules/currencies/lib/exchangeRateFormConfig.js +167 -0
  6. package/dist/modules/currencies/lib/exchangeRateFormConfig.js.map +7 -0
  7. package/dist/modules/customers/api/dashboard/widgets/utils.js +1 -34
  8. package/dist/modules/customers/api/dashboard/widgets/utils.js.map +2 -2
  9. package/dist/modules/customers/commands/activities.js +3 -8
  10. package/dist/modules/customers/commands/activities.js.map +2 -2
  11. package/dist/modules/customers/commands/comments.js +2 -8
  12. package/dist/modules/customers/commands/comments.js.map +2 -2
  13. package/dist/modules/dashboards/lib/widgetScope.js +38 -0
  14. package/dist/modules/dashboards/lib/widgetScope.js.map +7 -0
  15. package/dist/modules/entities/lib/makeActivityRoute.js +265 -0
  16. package/dist/modules/entities/lib/makeActivityRoute.js.map +7 -0
  17. package/dist/modules/resources/api/activities.js +24 -232
  18. package/dist/modules/resources/api/activities.js.map +2 -2
  19. package/dist/modules/resources/commands/activities.js +3 -8
  20. package/dist/modules/resources/commands/activities.js.map +2 -2
  21. package/dist/modules/resources/commands/comments.js +2 -8
  22. package/dist/modules/resources/commands/comments.js.map +2 -2
  23. package/dist/modules/sales/api/dashboard/widgets/new-orders/route.js +27 -182
  24. package/dist/modules/sales/api/dashboard/widgets/new-orders/route.js.map +2 -2
  25. package/dist/modules/sales/api/dashboard/widgets/new-quotes/route.js +28 -183
  26. package/dist/modules/sales/api/dashboard/widgets/new-quotes/route.js.map +2 -2
  27. package/dist/modules/sales/api/order-line-statuses/route.js +15 -194
  28. package/dist/modules/sales/api/order-line-statuses/route.js.map +2 -2
  29. package/dist/modules/sales/api/order-lines/route.js +15 -281
  30. package/dist/modules/sales/api/order-lines/route.js.map +2 -2
  31. package/dist/modules/sales/api/order-statuses/route.js +15 -194
  32. package/dist/modules/sales/api/order-statuses/route.js.map +2 -2
  33. package/dist/modules/sales/api/payment-statuses/route.js +15 -194
  34. package/dist/modules/sales/api/payment-statuses/route.js.map +2 -2
  35. package/dist/modules/sales/api/quote-lines/route.js +15 -279
  36. package/dist/modules/sales/api/quote-lines/route.js.map +2 -2
  37. package/dist/modules/sales/api/shipment-statuses/route.js +15 -194
  38. package/dist/modules/sales/api/shipment-statuses/route.js.map +2 -2
  39. package/dist/modules/sales/components/PaymentMethodsSettings.js +3 -84
  40. package/dist/modules/sales/components/PaymentMethodsSettings.js.map +2 -2
  41. package/dist/modules/sales/components/ProviderFieldInput.js +86 -0
  42. package/dist/modules/sales/components/ProviderFieldInput.js.map +7 -0
  43. package/dist/modules/sales/components/ShippingMethodsSettings.js +3 -82
  44. package/dist/modules/sales/components/ShippingMethodsSettings.js.map +2 -2
  45. package/dist/modules/sales/lib/makeSalesLineRoute.js +308 -0
  46. package/dist/modules/sales/lib/makeSalesLineRoute.js.map +7 -0
  47. package/dist/modules/sales/lib/makeStatusDictionaryRoute.js +206 -0
  48. package/dist/modules/sales/lib/makeStatusDictionaryRoute.js.map +7 -0
  49. package/dist/modules/sales/widgets/dashboard/makeDashboardWidgetRoute.js +178 -0
  50. package/dist/modules/sales/widgets/dashboard/makeDashboardWidgetRoute.js.map +7 -0
  51. package/dist/modules/sales/widgets/dashboard/new-orders/widget.client.js +1 -39
  52. package/dist/modules/sales/widgets/dashboard/new-orders/widget.client.js.map +2 -2
  53. package/dist/modules/sales/widgets/dashboard/new-quotes/widget.client.js +1 -39
  54. package/dist/modules/sales/widgets/dashboard/new-quotes/widget.client.js.map +2 -2
  55. package/dist/modules/sales/widgets/dashboard/shared.js +46 -0
  56. package/dist/modules/sales/widgets/dashboard/shared.js.map +7 -0
  57. package/dist/modules/staff/api/activities.js +24 -232
  58. package/dist/modules/staff/api/activities.js.map +2 -2
  59. package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js +14 -34
  60. package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js.map +2 -2
  61. package/dist/modules/staff/backend/staff/my-leave-requests/[id]/page.js +15 -34
  62. package/dist/modules/staff/backend/staff/my-leave-requests/[id]/page.js.map +2 -2
  63. package/dist/modules/staff/commands/activities.js +3 -8
  64. package/dist/modules/staff/commands/activities.js.map +2 -2
  65. package/dist/modules/staff/commands/comments.js +2 -8
  66. package/dist/modules/staff/commands/comments.js.map +2 -2
  67. package/dist/modules/staff/lib/leaveRequestHelpers.js +41 -0
  68. package/dist/modules/staff/lib/leaveRequestHelpers.js.map +7 -0
  69. package/package.json +2 -2
  70. package/src/modules/currencies/backend/exchange-rates/[id]/page.tsx +20 -180
  71. package/src/modules/currencies/backend/exchange-rates/create/page.tsx +16 -175
  72. package/src/modules/currencies/lib/exchangeRateFormConfig.ts +200 -0
  73. package/src/modules/customers/api/dashboard/widgets/utils.ts +1 -53
  74. package/src/modules/customers/commands/activities.ts +2 -8
  75. package/src/modules/customers/commands/comments.ts +2 -8
  76. package/src/modules/dashboards/i18n/de.json +3 -0
  77. package/src/modules/dashboards/i18n/en.json +3 -0
  78. package/src/modules/dashboards/i18n/es.json +3 -0
  79. package/src/modules/dashboards/i18n/pl.json +3 -0
  80. package/src/modules/dashboards/lib/widgetScope.ts +53 -0
  81. package/src/modules/entities/lib/makeActivityRoute.ts +327 -0
  82. package/src/modules/resources/api/activities.ts +25 -269
  83. package/src/modules/resources/commands/activities.ts +2 -7
  84. package/src/modules/resources/commands/comments.ts +2 -8
  85. package/src/modules/sales/api/dashboard/widgets/new-orders/route.ts +29 -244
  86. package/src/modules/sales/api/dashboard/widgets/new-quotes/route.ts +30 -245
  87. package/src/modules/sales/api/order-line-statuses/route.ts +16 -209
  88. package/src/modules/sales/api/order-lines/route.ts +16 -300
  89. package/src/modules/sales/api/order-statuses/route.ts +16 -209
  90. package/src/modules/sales/api/payment-statuses/route.ts +16 -209
  91. package/src/modules/sales/api/quote-lines/route.ts +16 -298
  92. package/src/modules/sales/api/shipment-statuses/route.ts +16 -209
  93. package/src/modules/sales/components/PaymentMethodsSettings.tsx +3 -88
  94. package/src/modules/sales/components/ProviderFieldInput.tsx +85 -0
  95. package/src/modules/sales/components/ShippingMethodsSettings.tsx +3 -86
  96. package/src/modules/sales/lib/makeSalesLineRoute.ts +345 -0
  97. package/src/modules/sales/lib/makeStatusDictionaryRoute.ts +229 -0
  98. package/src/modules/sales/widgets/dashboard/makeDashboardWidgetRoute.ts +247 -0
  99. package/src/modules/sales/widgets/dashboard/new-orders/widget.client.tsx +7 -50
  100. package/src/modules/sales/widgets/dashboard/new-quotes/widget.client.tsx +7 -49
  101. package/src/modules/sales/widgets/dashboard/shared.ts +44 -0
  102. package/src/modules/staff/api/activities.ts +25 -269
  103. package/src/modules/staff/backend/staff/leave-requests/[id]/page.tsx +15 -69
  104. package/src/modules/staff/backend/staff/my-leave-requests/[id]/page.tsx +16 -65
  105. package/src/modules/staff/commands/activities.ts +2 -7
  106. package/src/modules/staff/commands/comments.ts +2 -8
  107. package/src/modules/staff/lib/leaveRequestHelpers.ts +78 -0
@@ -1,278 +1,34 @@
1
- import { z } from 'zod'
2
- import type { EntityManager } from '@mikro-orm/postgresql'
3
- import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
4
- import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
5
- import { resolveCrudRecordId, parseScopedCommandInput } from '@open-mercato/shared/lib/api/scoped'
6
- import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
1
+ import { makeActivityRoute } from '@open-mercato/core/modules/entities/lib/makeActivityRoute'
7
2
  import { StaffTeamMemberActivity } from '../data/entities'
8
3
  import {
9
4
  staffTeamMemberActivityCreateSchema,
10
5
  staffTeamMemberActivityUpdateSchema,
11
6
  } from '../data/validators'
12
- import { User } from '@open-mercato/core/modules/auth/data/entities'
13
7
  import { E } from '#generated/entities.ids.generated'
14
- import { createStaffCrudOpenApi, createPagedListResponseSchema, defaultOkResponseSchema } from './openapi'
15
-
16
- const rawBodySchema = z.object({}).passthrough()
17
-
18
- const listSchema = z
19
- .object({
20
- page: z.coerce.number().min(1).default(1),
21
- pageSize: z.coerce.number().min(1).max(100).default(50),
22
- entityId: z.string().uuid().optional(),
23
- sortField: z.string().optional(),
24
- sortDir: z.enum(['asc', 'desc']).optional(),
25
- })
26
- .passthrough()
27
-
28
- const routeMetadata = {
29
- GET: { requireAuth: true, requireFeatures: ['staff.view'] },
30
- POST: { requireAuth: true, requireFeatures: ['staff.manage_team'] },
31
- PUT: { requireAuth: true, requireFeatures: ['staff.manage_team'] },
32
- DELETE: { requireAuth: true, requireFeatures: ['staff.manage_team'] },
33
- }
34
-
35
- export const metadata = routeMetadata
36
-
37
- const crud = makeCrudRoute({
38
- metadata: routeMetadata,
39
- orm: {
40
- entity: StaffTeamMemberActivity,
41
- idField: 'id',
42
- orgField: 'organizationId',
43
- tenantField: 'tenantId',
44
- },
45
- indexer: {
46
- entityType: E.staff.staff_team_member_activity,
47
- },
48
- list: {
49
- schema: listSchema,
50
- entityId: E.staff.staff_team_member_activity,
51
- fields: [
52
- 'id',
53
- 'member_id',
54
- 'activity_type',
55
- 'subject',
56
- 'body',
57
- 'occurred_at',
58
- 'author_user_id',
59
- 'appearance_icon',
60
- 'appearance_color',
61
- 'organization_id',
62
- 'tenant_id',
63
- 'created_at',
64
- 'updated_at',
65
- ],
66
- decorateCustomFields: {
67
- entityIds: E.staff.staff_team_member_activity,
68
- },
69
- sortFieldMap: {
70
- occurredAt: 'occurred_at',
71
- createdAt: 'created_at',
72
- updatedAt: 'updated_at',
73
- },
74
- buildFilters: async (query) => {
75
- const filters: Record<string, unknown> = {}
76
- if (query.entityId) filters.member_id = { $eq: query.entityId }
77
- return filters
78
- },
79
- transformItem: (item: Record<string, unknown>) => {
80
- const record = (item ?? {}) as Record<string, unknown>
81
- const toIsoString = (value: unknown): string | null => {
82
- if (value == null) return null
83
- if (value instanceof Date) return value.toISOString()
84
- if (typeof value === 'string') {
85
- const trimmed = value.trim()
86
- if (!trimmed.length) return null
87
- const date = new Date(trimmed)
88
- return Number.isNaN(date.getTime()) ? trimmed : date.toISOString()
89
- }
90
- return null
91
- }
92
- const readString = (value: unknown): string | null => (typeof value === 'string' ? value : null)
93
- const idValue = readString(record.id) ?? (record.id != null ? String(record.id) : '')
94
- const memberId = readString(record['member_id']) ?? readString(record['memberId']) ?? null
95
- const activityType =
96
- readString(record['activity_type']) ??
97
- readString(record['activityType']) ??
98
- ''
99
- const subject =
100
- readString(record.subject) ??
101
- (record.subject == null ? null : String(record.subject))
102
- const body =
103
- readString(record.body) ??
104
- (record.body == null ? null : String(record.body))
105
- const authorUserId =
106
- readString(record['author_user_id']) ?? readString(record['authorUserId']) ?? null
107
- const appearanceIconRaw =
108
- readString(record['appearance_icon']) ?? readString(record['appearanceIcon'])
109
- const appearanceColorRaw =
110
- readString(record['appearance_color']) ?? readString(record['appearanceColor'])
111
- const organizationId =
112
- readString(record['organization_id']) ?? readString(record['organizationId'])
113
- const tenantId =
114
- readString(record['tenant_id']) ?? readString(record['tenantId'])
115
- const output: Record<string, unknown> = {
116
- id: idValue,
117
- entityId: memberId,
118
- memberId,
119
- activityType,
120
- subject,
121
- body,
122
- occurredAt: toIsoString(record['occurred_at'] ?? record['occurredAt']),
123
- createdAt: toIsoString(record['created_at'] ?? record['createdAt']),
124
- authorUserId,
125
- organizationId,
126
- tenantId,
127
- appearanceIcon: appearanceIconRaw && appearanceIconRaw.trim().length ? appearanceIconRaw : null,
128
- appearanceColor: appearanceColorRaw && appearanceColorRaw.trim().length ? appearanceColorRaw : null,
129
- customFields: Array.isArray(record.customFields) ? record.customFields : undefined,
130
- customValues: record.customValues ?? undefined,
131
- }
132
- for (const [key, value] of Object.entries(record)) {
133
- if (key.startsWith('cf_') || key.startsWith('cf:')) {
134
- output[key] = value
135
- }
136
- }
137
- return output
138
- },
139
- },
140
- actions: {
141
- create: {
142
- commandId: 'staff.team-member-activities.create',
143
- schema: rawBodySchema,
144
- mapInput: async ({ raw, ctx }) => {
145
- const { translate } = await resolveTranslations()
146
- return parseScopedCommandInput(staffTeamMemberActivityCreateSchema, raw ?? {}, ctx, translate)
147
- },
148
- response: ({ result }) => ({
149
- id: result?.activityId ?? result?.id ?? null,
150
- authorUserId: result?.authorUserId ?? null,
151
- }),
152
- status: 201,
153
- },
154
- update: {
155
- commandId: 'staff.team-member-activities.update',
156
- schema: rawBodySchema,
157
- mapInput: async ({ raw, ctx }) => {
158
- const { translate } = await resolveTranslations()
159
- return parseScopedCommandInput(staffTeamMemberActivityUpdateSchema, raw ?? {}, ctx, translate)
160
- },
161
- response: () => ({ ok: true }),
162
- },
163
- delete: {
164
- commandId: 'staff.team-member-activities.delete',
165
- schema: rawBodySchema,
166
- mapInput: async ({ parsed, ctx }) => {
167
- const { translate } = await resolveTranslations()
168
- const id = resolveCrudRecordId(parsed, ctx, translate)
169
- return { id }
170
- },
171
- response: () => ({ ok: true }),
172
- },
173
- },
174
- hooks: {
175
- afterList: async (payload, ctx) => {
176
- const items = Array.isArray(payload.items) ? payload.items : []
177
- if (!items.length) return
178
- const userIds = new Set<string>()
179
- items.forEach((item: unknown) => {
180
- if (!item || typeof item !== 'object') return
181
- const record = item as Record<string, unknown>
182
- const userId =
183
- typeof record.author_user_id === 'string'
184
- ? record.author_user_id
185
- : typeof record.authorUserId === 'string'
186
- ? record.authorUserId
187
- : null
188
- if (userId) userIds.add(userId)
189
- })
190
- if (!userIds.size) return
191
- try {
192
- const em = (ctx.container.resolve('em') as EntityManager).fork()
193
- const users = await findWithDecryption(
194
- em,
195
- User,
196
- { id: { $in: Array.from(userIds) } },
197
- undefined,
198
- { tenantId: ctx.auth?.tenantId ?? null, organizationId: ctx.selectedOrganizationId ?? null },
199
- )
200
- const map = new Map<string, { name: string | null; email: string | null }>()
201
- users.forEach((user) => {
202
- const name = typeof user.name === 'string' && user.name.trim().length
203
- ? user.name.trim()
204
- : null
205
- map.set(user.id, { name, email: user.email ?? null })
206
- })
207
- items.forEach((item: unknown) => {
208
- if (!item || typeof item !== 'object') return
209
- const record = item as Record<string, unknown>
210
- const userId =
211
- typeof record.author_user_id === 'string'
212
- ? record.author_user_id
213
- : typeof record.authorUserId === 'string'
214
- ? record.authorUserId
215
- : null
216
- if (!userId) return
217
- const meta = map.get(userId)
218
- if (!meta) return
219
- record.authorName = meta.name
220
- record.authorEmail = meta.email
221
- if (!('author_name' in record)) record.author_name = meta.name
222
- if (!('author_email' in record)) record.author_email = meta.email
223
- })
224
- } catch (err) {
225
- console.warn('[staff.activities] failed to enrich author metadata', err)
226
- }
227
- },
8
+ import { createStaffCrudOpenApi } from './openapi'
9
+
10
+ const route = makeActivityRoute({
11
+ entity: StaffTeamMemberActivity,
12
+ entityId: E.staff.staff_team_member_activity,
13
+ parentFkColumn: 'member_id',
14
+ parentFkParam: 'memberId',
15
+ features: { view: 'staff.view', manage: 'staff.manage_team' },
16
+ createSchema: staffTeamMemberActivityCreateSchema,
17
+ updateSchema: staffTeamMemberActivityUpdateSchema,
18
+ commandPrefix: 'staff.team-member-activities',
19
+ logPrefix: '[staff.activities]',
20
+ openApiFactory: createStaffCrudOpenApi,
21
+ openApi: {
22
+ resourceName: 'TeamMemberActivity',
23
+ createDescription: 'Adds an activity to a team member timeline.',
24
+ updateDescription: 'Updates a team member activity.',
25
+ deleteDescription: 'Deletes a team member activity.',
228
26
  },
229
27
  })
230
28
 
231
- export const GET = crud.GET
232
- export const POST = crud.POST
233
- export const PUT = crud.PUT
234
- export const DELETE = crud.DELETE
235
-
236
- const activityListItemSchema = z
237
- .object({
238
- id: z.string().uuid(),
239
- member_id: z.string().uuid().nullable().optional(),
240
- activity_type: z.string().nullable().optional(),
241
- subject: z.string().nullable().optional(),
242
- body: z.string().nullable().optional(),
243
- occurred_at: z.string().nullable().optional(),
244
- author_user_id: z.string().uuid().nullable(),
245
- appearance_icon: z.string().nullable().optional(),
246
- appearance_color: z.string().nullable().optional(),
247
- organization_id: z.string().uuid().nullable().optional(),
248
- tenant_id: z.string().uuid().nullable().optional(),
249
- created_at: z.string().nullable(),
250
- updated_at: z.string().nullable().optional(),
251
- })
252
- .passthrough()
253
-
254
- const activityCreateResponseSchema = z.object({
255
- id: z.string().uuid().nullable(),
256
- authorUserId: z.string().uuid().nullable(),
257
- })
258
-
259
- export const openApi = createStaffCrudOpenApi({
260
- resourceName: 'TeamMemberActivity',
261
- querySchema: listSchema,
262
- listResponseSchema: createPagedListResponseSchema(activityListItemSchema),
263
- create: {
264
- schema: staffTeamMemberActivityCreateSchema,
265
- responseSchema: activityCreateResponseSchema,
266
- description: 'Adds an activity to a team member timeline.',
267
- },
268
- update: {
269
- schema: staffTeamMemberActivityUpdateSchema,
270
- responseSchema: defaultOkResponseSchema,
271
- description: 'Updates a team member activity.',
272
- },
273
- del: {
274
- schema: z.object({ id: z.string().uuid() }),
275
- responseSchema: defaultOkResponseSchema,
276
- description: 'Deletes a team member activity.',
277
- },
278
- })
29
+ export const metadata = route.metadata
30
+ export const openApi = route.openApi
31
+ export const GET = route.GET
32
+ export const POST = route.POST
33
+ export const PUT = route.PUT
34
+ export const DELETE = route.DELETE
@@ -2,7 +2,6 @@
2
2
 
3
3
  import * as React from 'react'
4
4
  import { useRouter } from 'next/navigation'
5
- import { Send } from 'lucide-react'
6
5
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
7
6
  import { Badge } from '@open-mercato/ui/primitives/badge'
8
7
  import { Button } from '@open-mercato/ui/primitives/button'
@@ -14,32 +13,7 @@ import { updateCrud } from '@open-mercato/ui/backend/utils/crud'
14
13
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
15
14
  import { useT } from '@open-mercato/shared/lib/i18n/context'
16
15
  import { LeaveRequestForm, buildLeaveRequestPayload, type LeaveRequestFormValues } from '@open-mercato/core/modules/staff/components/LeaveRequestForm'
17
-
18
- type LeaveRequestRecord = {
19
- id: string
20
- member?: { id?: string; displayName?: string }
21
- memberId?: string | null
22
- member_id?: string | null
23
- startDate?: string | null
24
- start_date?: string | null
25
- endDate?: string | null
26
- end_date?: string | null
27
- timezone?: string | null
28
- status?: 'pending' | 'approved' | 'rejected'
29
- unavailabilityReasonEntryId?: string | null
30
- unavailability_reason_entry_id?: string | null
31
- unavailabilityReasonValue?: string | null
32
- unavailability_reason_value?: string | null
33
- note?: string | null
34
- decisionComment?: string | null
35
- decision_comment?: string | null
36
- decidedAt?: string | null
37
- decided_at?: string | null
38
- } & Record<string, unknown>
39
-
40
- type LeaveRequestsResponse = {
41
- items?: LeaveRequestRecord[]
42
- }
16
+ import { type LeaveRequestRecord, type LeaveRequestsResponse, type NormalizedLeaveRequest, normalizeLeaveRequest, resolveStatusVariant, formatDateLabel, formatDateRange } from '../../../../lib/leaveRequestHelpers'
43
17
 
44
18
  export default function StaffLeaveRequestDetailPage({ params }: { params?: { id?: string } }) {
45
19
  const id = params?.id
@@ -47,7 +21,7 @@ export default function StaffLeaveRequestDetailPage({ params }: { params?: { id?
47
21
  const router = useRouter()
48
22
  const [isLoading, setIsLoading] = React.useState(true)
49
23
  const [error, setError] = React.useState<string | null>(null)
50
- const [record, setRecord] = React.useState<LeaveRequestRecord | null>(null)
24
+ const [record, setRecord] = React.useState<NormalizedLeaveRequest | null>(null)
51
25
  const [decisionComment, setDecisionComment] = React.useState('')
52
26
 
53
27
  React.useEffect(() => {
@@ -71,14 +45,9 @@ export default function StaffLeaveRequestDetailPage({ params }: { params?: { id?
71
45
  const entry = Array.isArray(payload.items) ? payload.items[0] : null
72
46
  if (!entry) throw new Error(t('staff.leaveRequests.errors.notFound', 'Leave request not found.'))
73
47
  if (!cancelled) {
74
- setRecord(entry)
75
- setDecisionComment(
76
- typeof entry.decisionComment === 'string'
77
- ? entry.decisionComment
78
- : typeof entry.decision_comment === 'string'
79
- ? entry.decision_comment
80
- : ''
81
- )
48
+ const normalized = normalizeLeaveRequest(entry)
49
+ setRecord(normalized)
50
+ setDecisionComment(normalized.decisionComment ?? '')
82
51
  }
83
52
  } catch (err) {
84
53
  if (!cancelled) {
@@ -96,19 +65,16 @@ export default function StaffLeaveRequestDetailPage({ params }: { params?: { id?
96
65
 
97
66
  const status = record?.status ?? 'pending'
98
67
  const memberLabel = record?.member?.displayName ?? null
99
- const dateSummary = formatDateRange(
100
- record?.startDate ?? record?.start_date ?? null,
101
- record?.endDate ?? record?.end_date ?? null,
102
- )
68
+ const dateSummary = formatDateRange(record?.startDate, record?.endDate)
103
69
  const initialValues = React.useMemo<LeaveRequestFormValues>(() => ({
104
70
  id: record?.id,
105
- memberId: record?.memberId ?? record?.member_id ?? null,
71
+ memberId: record?.memberId ?? null,
106
72
  memberLabel,
107
- startDate: record?.startDate ?? record?.start_date ?? null,
108
- endDate: record?.endDate ?? record?.end_date ?? null,
73
+ startDate: record?.startDate ?? null,
74
+ endDate: record?.endDate ?? null,
109
75
  timezone: record?.timezone ?? null,
110
- unavailabilityReasonEntryId: record?.unavailabilityReasonEntryId ?? record?.unavailability_reason_entry_id ?? null,
111
- unavailabilityReasonValue: record?.unavailabilityReasonValue ?? record?.unavailability_reason_value ?? null,
76
+ unavailabilityReasonEntryId: record?.unavailabilityReasonEntryId ?? null,
77
+ unavailabilityReasonValue: record?.unavailabilityReasonValue ?? null,
112
78
  note: record?.note ?? null,
113
79
  }), [record, memberLabel])
114
80
 
@@ -167,9 +133,9 @@ const handleSubmit = React.useCallback(async (values: LeaveRequestFormValues) =>
167
133
  <Badge variant={resolveStatusVariant(status)}>
168
134
  {t(`staff.leaveRequests.status.${status}`, status)}
169
135
  </Badge>
170
- {record.decided_at || record.decidedAt ? (
136
+ {record.decidedAt ? (
171
137
  <span className="text-xs text-muted-foreground">
172
- {t('staff.leaveRequests.decision.at', 'Decision at')} {formatDateLabel(record.decidedAt ?? record.decided_at ?? null)}
138
+ {t('staff.leaveRequests.decision.at', 'Decision at')} {formatDateLabel(record.decidedAt)}
173
139
  </span>
174
140
  ) : null}
175
141
  </div>
@@ -201,10 +167,10 @@ const handleSubmit = React.useCallback(async (values: LeaveRequestFormValues) =>
201
167
  </Button>
202
168
  </div>
203
169
  </div>
204
- ) : record.decisionComment || record.decision_comment ? (
170
+ ) : record.decisionComment ? (
205
171
  <div className="mb-6 rounded-lg border bg-card p-4 text-sm text-muted-foreground">
206
172
  <div className="mb-1 font-medium text-foreground">{t('staff.leaveRequests.decision.comment', 'Decision comment')}</div>
207
- <p>{record.decisionComment ?? record.decision_comment}</p>
173
+ <p>{record.decisionComment}</p>
208
174
  </div>
209
175
  ) : null}
210
176
 
@@ -252,23 +218,3 @@ const handleSubmit = React.useCallback(async (values: LeaveRequestFormValues) =>
252
218
  </Page>
253
219
  )
254
220
  }
255
-
256
- function resolveStatusVariant(status: 'pending' | 'approved' | 'rejected') {
257
- if (status === 'approved') return 'default'
258
- if (status === 'rejected') return 'destructive'
259
- return 'secondary'
260
- }
261
-
262
- function formatDateLabel(value?: string | null): string {
263
- if (!value) return ''
264
- const date = new Date(value)
265
- if (Number.isNaN(date.getTime())) return value
266
- return date.toLocaleDateString()
267
- }
268
-
269
- function formatDateRange(start?: string | null, end?: string | null): string {
270
- const startLabel = formatDateLabel(start)
271
- const endLabel = formatDateLabel(end)
272
- if (startLabel && endLabel) return `${startLabel} -> ${endLabel}`
273
- return startLabel || endLabel || '-'
274
- }
@@ -2,7 +2,6 @@
2
2
 
3
3
  import * as React from 'react'
4
4
  import { useRouter } from 'next/navigation'
5
- import { Send } from 'lucide-react'
6
5
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
7
6
  import { Badge } from '@open-mercato/ui/primitives/badge'
8
7
  import { Button } from '@open-mercato/ui/primitives/button'
@@ -13,32 +12,7 @@ import { updateCrud } from '@open-mercato/ui/backend/utils/crud'
13
12
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
14
13
  import { useT } from '@open-mercato/shared/lib/i18n/context'
15
14
  import { LeaveRequestForm, buildLeaveRequestPayload, type LeaveRequestFormValues } from '@open-mercato/core/modules/staff/components/LeaveRequestForm'
16
-
17
- type LeaveRequestRecord = {
18
- id: string
19
- member?: { id?: string; displayName?: string }
20
- memberId?: string | null
21
- member_id?: string | null
22
- startDate?: string | null
23
- start_date?: string | null
24
- endDate?: string | null
25
- end_date?: string | null
26
- timezone?: string | null
27
- status?: 'pending' | 'approved' | 'rejected'
28
- unavailabilityReasonEntryId?: string | null
29
- unavailability_reason_entry_id?: string | null
30
- unavailabilityReasonValue?: string | null
31
- unavailability_reason_value?: string | null
32
- note?: string | null
33
- decisionComment?: string | null
34
- decision_comment?: string | null
35
- decidedAt?: string | null
36
- decided_at?: string | null
37
- } & Record<string, unknown>
38
-
39
- type LeaveRequestsResponse = {
40
- items?: LeaveRequestRecord[]
41
- }
15
+ import { type LeaveRequestsResponse, type NormalizedLeaveRequest, normalizeLeaveRequest, resolveStatusVariant, formatDateLabel, formatDateRange } from '../../../../lib/leaveRequestHelpers'
42
16
 
43
17
  export default function StaffMyLeaveRequestDetailPage({ params }: { params?: { id?: string } }) {
44
18
  const id = params?.id
@@ -46,7 +20,7 @@ export default function StaffMyLeaveRequestDetailPage({ params }: { params?: { i
46
20
  const router = useRouter()
47
21
  const [isLoading, setIsLoading] = React.useState(true)
48
22
  const [error, setError] = React.useState<string | null>(null)
49
- const [record, setRecord] = React.useState<LeaveRequestRecord | null>(null)
23
+ const [record, setRecord] = React.useState<NormalizedLeaveRequest | null>(null)
50
24
 
51
25
  React.useEffect(() => {
52
26
  if (!id) {
@@ -69,7 +43,7 @@ export default function StaffMyLeaveRequestDetailPage({ params }: { params?: { i
69
43
  const entry = Array.isArray(payload.items) ? payload.items[0] : null
70
44
  if (!entry) throw new Error(t('staff.leaveRequests.errors.notFound', 'Leave request not found.'))
71
45
  if (!cancelled) {
72
- setRecord(entry)
46
+ setRecord(normalizeLeaveRequest(entry))
73
47
  }
74
48
  } catch (err) {
75
49
  if (!cancelled) {
@@ -89,19 +63,16 @@ export default function StaffMyLeaveRequestDetailPage({ params }: { params?: { i
89
63
  const memberLabel = record?.member?.displayName ?? null
90
64
  const initialValues = React.useMemo<LeaveRequestFormValues>(() => ({
91
65
  id: record?.id,
92
- memberId: record?.memberId ?? record?.member_id ?? null,
66
+ memberId: record?.memberId ?? null,
93
67
  memberLabel,
94
- startDate: record?.startDate ?? record?.start_date ?? null,
95
- endDate: record?.endDate ?? record?.end_date ?? null,
68
+ startDate: record?.startDate ?? null,
69
+ endDate: record?.endDate ?? null,
96
70
  timezone: record?.timezone ?? null,
97
- unavailabilityReasonEntryId: record?.unavailabilityReasonEntryId ?? record?.unavailability_reason_entry_id ?? null,
98
- unavailabilityReasonValue: record?.unavailabilityReasonValue ?? record?.unavailability_reason_value ?? null,
71
+ unavailabilityReasonEntryId: record?.unavailabilityReasonEntryId ?? null,
72
+ unavailabilityReasonValue: record?.unavailabilityReasonValue ?? null,
99
73
  note: record?.note ?? null,
100
74
  }), [record, memberLabel])
101
- const dateSummary = formatDateRange(
102
- record?.startDate ?? record?.start_date ?? null,
103
- record?.endDate ?? record?.end_date ?? null,
104
- )
75
+ const dateSummary = formatDateRange(record?.startDate, record?.endDate)
105
76
  const handleSubmit = React.useCallback(async (values: LeaveRequestFormValues) => {
106
77
  if (!record?.id) return
107
78
  const payload = buildLeaveRequestPayload(values, { id: record.id })
@@ -140,16 +111,16 @@ const handleSubmit = React.useCallback(async (values: LeaveRequestFormValues) =>
140
111
  <Badge variant={resolveStatusVariant(status)}>
141
112
  {t(`staff.leaveRequests.status.${status}`, status)}
142
113
  </Badge>
143
- {record.decided_at || record.decidedAt ? (
114
+ {record.decidedAt ? (
144
115
  <span className="text-xs text-muted-foreground">
145
- {t('staff.leaveRequests.decision.at', 'Decision at')} {formatDateLabel(record.decidedAt ?? record.decided_at ?? null)}
116
+ {t('staff.leaveRequests.decision.at', 'Decision at')} {formatDateLabel(record.decidedAt)}
146
117
  </span>
147
118
  ) : null}
148
119
  </div>
149
- {record.decisionComment || record.decision_comment ? (
120
+ {record.decisionComment ? (
150
121
  <div className="text-sm text-muted-foreground">
151
122
  <div className="font-medium text-foreground">{t('staff.leaveRequests.decision.comment', 'Decision comment')}</div>
152
- <p>{record.decisionComment ?? record.decision_comment}</p>
123
+ <p>{record.decisionComment}</p>
153
124
  </div>
154
125
  ) : null}
155
126
  </div>
@@ -199,9 +170,9 @@ const handleSubmit = React.useCallback(async (values: LeaveRequestFormValues) =>
199
170
  <div className="rounded-lg border bg-card p-4 text-sm text-muted-foreground">
200
171
  <div className="font-medium text-foreground">{t('staff.leaveRequests.detail.summary', 'Request details')}</div>
201
172
  <p>{memberLabel ? t('staff.leaveRequests.detail.member', 'Team member') + `: ${memberLabel}` : null}</p>
202
- <p>{t('staff.leaveRequests.detail.dates', 'Dates')}: {formatDateRange(record.startDate ?? record.start_date ?? null, record.endDate ?? record.end_date ?? null)}</p>
203
- {record.unavailabilityReasonValue || record.unavailability_reason_value ? (
204
- <p>{t('staff.leaveRequests.detail.reason', 'Reason')}: {record.unavailabilityReasonValue ?? record.unavailability_reason_value}</p>
173
+ <p>{t('staff.leaveRequests.detail.dates', 'Dates')}: {formatDateRange(record.startDate, record.endDate)}</p>
174
+ {record.unavailabilityReasonValue ? (
175
+ <p>{t('staff.leaveRequests.detail.reason', 'Reason')}: {record.unavailabilityReasonValue}</p>
205
176
  ) : null}
206
177
  {record.note ? <p>{t('staff.leaveRequests.detail.note', 'Note')}: {record.note}</p> : null}
207
178
  </div>
@@ -210,23 +181,3 @@ const handleSubmit = React.useCallback(async (values: LeaveRequestFormValues) =>
210
181
  </Page>
211
182
  )
212
183
  }
213
-
214
- function resolveStatusVariant(status: 'pending' | 'approved' | 'rejected') {
215
- if (status === 'approved') return 'default'
216
- if (status === 'rejected') return 'destructive'
217
- return 'secondary'
218
- }
219
-
220
- function formatDateLabel(value?: string | null): string {
221
- if (!value) return ''
222
- const date = new Date(value)
223
- if (Number.isNaN(date.getTime())) return value
224
- return date.toLocaleDateString()
225
- }
226
-
227
- function formatDateRange(start?: string | null, end?: string | null): string {
228
- const startLabel = formatDateLabel(start)
229
- const endLabel = formatDateLabel(end)
230
- if (startLabel && endLabel) return `${startLabel} -> ${endLabel}`
231
- return startLabel || endLabel || '-'
232
- }
@@ -7,6 +7,7 @@ import {
7
7
  requireId,
8
8
  parseWithCustomFields,
9
9
  setCustomFieldsIfAny,
10
+ normalizeAuthorUserId,
10
11
  } from '@open-mercato/shared/lib/commands/helpers'
11
12
  import type { DataEngine } from '@open-mercato/shared/lib/data/engine'
12
13
  import type { EntityManager } from '@mikro-orm/postgresql'
@@ -115,13 +116,7 @@ const createActivityCommand: CommandHandler<
115
116
  const { parsed, custom } = parseWithCustomFields(staffTeamMemberActivityCreateSchema, rawInput)
116
117
  ensureTenantScope(ctx, parsed.tenantId)
117
118
  ensureOrganizationScope(ctx, parsed.organizationId)
118
- const authSub = ctx.auth?.isApiKey ? null : ctx.auth?.sub ?? null
119
- const normalizedAuthor = (() => {
120
- if (parsed.authorUserId) return parsed.authorUserId
121
- if (!authSub) return null
122
- const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/
123
- return uuidRegex.test(authSub) ? authSub : null
124
- })()
119
+ const normalizedAuthor = normalizeAuthorUserId(parsed.authorUserId, ctx.auth)
125
120
 
126
121
  const em = (ctx.container.resolve('em') as EntityManager).fork()
127
122
  const member = await requireTeamMember(em, parsed.entityId, 'Team member not found')
@@ -1,6 +1,6 @@
1
1
  import { registerCommand } from '@open-mercato/shared/lib/commands'
2
2
  import type { CommandHandler } from '@open-mercato/shared/lib/commands'
3
- import { emitCrudSideEffects, emitCrudUndoSideEffects, buildChanges, requireId } from '@open-mercato/shared/lib/commands/helpers'
3
+ import { emitCrudSideEffects, emitCrudUndoSideEffects, buildChanges, requireId, normalizeAuthorUserId } from '@open-mercato/shared/lib/commands/helpers'
4
4
  import type { DataEngine } from '@open-mercato/shared/lib/data/engine'
5
5
  import type { EntityManager } from '@mikro-orm/postgresql'
6
6
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
@@ -60,13 +60,7 @@ const createCommentCommand: CommandHandler<
60
60
  const parsed = staffTeamMemberCommentCreateSchema.parse(rawInput)
61
61
  ensureTenantScope(ctx, parsed.tenantId)
62
62
  ensureOrganizationScope(ctx, parsed.organizationId)
63
- const authSub = ctx.auth?.isApiKey ? null : ctx.auth?.sub ?? null
64
- const normalizedAuthor = (() => {
65
- if (parsed.authorUserId) return parsed.authorUserId
66
- if (!authSub) return null
67
- const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/
68
- return uuidRegex.test(authSub) ? authSub : null
69
- })()
63
+ const normalizedAuthor = normalizeAuthorUserId(parsed.authorUserId, ctx.auth)
70
64
 
71
65
  const em = (ctx.container.resolve('em') as EntityManager).fork()
72
66
  const member = await requireTeamMember(em, parsed.entityId, 'Team member not found')