@open-mercato/core 0.4.6-develop-9ff1d4a9a2 → 0.4.6-develop-219dae16c5

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
@@ -0,0 +1,247 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { NextResponse } from 'next/server'
3
+ import { z } from 'zod'
4
+ import type { EntityName, FilterQuery, FindOptions } from '@mikro-orm/core'
5
+ import type { CacheStrategy } from '@open-mercato/cache'
6
+ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
7
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
8
+ import { runWithCacheTenant } from '@open-mercato/cache'
9
+ import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
10
+ import { findAndCountWithDecryption } from '@open-mercato/shared/lib/encryption/find'
11
+ import { resolveDateRange } from '@open-mercato/ui/backend/date-range'
12
+ import type { DatePeriodOption } from '../../api/dashboard/widgets/helpers'
13
+ import { resolveWidgetScope, type WidgetScopeContext } from '@open-mercato/core/modules/dashboards/lib/widgetScope'
14
+
15
+ const WIDGET_CACHE_TTL = 120_000
16
+ const WIDGET_CACHE_SEGMENT_TTL = 86_400_000
17
+ const WIDGET_CACHE_SEGMENT_KEY = 'widget-data:__segment__'
18
+
19
+ const querySchema = z.object({
20
+ limit: z.coerce.number().min(1).max(20).default(5),
21
+ datePeriod: z.enum(['last24h', 'last7d', 'last30d', 'custom']).default('last24h'),
22
+ customFrom: z.string().optional(),
23
+ customTo: z.string().optional(),
24
+ tenantId: z.string().uuid().optional(),
25
+ organizationId: z.string().uuid().optional(),
26
+ })
27
+
28
+ type WidgetContext = WidgetScopeContext & {
29
+ limit: number
30
+ datePeriod: DatePeriodOption
31
+ customFrom?: string
32
+ customTo?: string
33
+ }
34
+
35
+ function normalizeOrganizationIds(organizationIds: string[] | null): string[] | null {
36
+ if (organizationIds === null) return null
37
+ const set = new Set(organizationIds)
38
+ return Array.from(set).sort((a, b) => a.localeCompare(b))
39
+ }
40
+
41
+ function buildCacheKey(
42
+ cacheId: string,
43
+ params: {
44
+ tenantId: string
45
+ organizationIds: string[] | null
46
+ limit: number
47
+ datePeriod: DatePeriodOption
48
+ customFrom?: string
49
+ customTo?: string
50
+ }
51
+ ): string {
52
+ const hash = createHash('sha256')
53
+ hash.update(
54
+ JSON.stringify({
55
+ widget: cacheId,
56
+ ...params,
57
+ organizationIds: normalizeOrganizationIds(params.organizationIds),
58
+ })
59
+ )
60
+ return `widget-data:${hash.digest('hex').slice(0, 16)}`
61
+ }
62
+
63
+ async function resolveContext(req: Request, translate: (key: string, fallback?: string) => string): Promise<WidgetContext> {
64
+ const url = new URL(req.url)
65
+ const rawQuery: Record<string, string> = {}
66
+ for (const [key, value] of url.searchParams.entries()) rawQuery[key] = value
67
+ const parsed = querySchema.safeParse(rawQuery)
68
+ if (!parsed.success) {
69
+ throw new CrudHttpError(400, { error: translate('sales.errors.invalid_query', 'Invalid query parameters') })
70
+ }
71
+
72
+ const { container, em, tenantId, organizationIds } = await resolveWidgetScope(req, translate, {
73
+ tenantId: parsed.data.tenantId ?? null,
74
+ organizationId: parsed.data.organizationId ?? null,
75
+ })
76
+
77
+ return {
78
+ container,
79
+ em,
80
+ tenantId,
81
+ organizationIds,
82
+ limit: parsed.data.limit,
83
+ datePeriod: parsed.data.datePeriod,
84
+ customFrom: parsed.data.customFrom,
85
+ customTo: parsed.data.customTo,
86
+ }
87
+ }
88
+
89
+ export interface DashboardWidgetRouteConfig<TEntity extends object, TItem extends Record<string, unknown>> {
90
+ entity: { new (...args: unknown[]): TEntity }
91
+ cacheId: string
92
+ cacheTags: string[]
93
+ feature: string
94
+ mapItem: (entity: Record<string, unknown>) => TItem
95
+ itemSchema: z.ZodTypeAny
96
+ openApi: {
97
+ summary: string
98
+ description: string
99
+ getSummary: string
100
+ itemDescription: string
101
+ errorFallback: string
102
+ }
103
+ errorPrefix: string
104
+ }
105
+
106
+ type WidgetResponse<TItem> = {
107
+ items: TItem[]
108
+ total: number
109
+ dateRange: {
110
+ from: string
111
+ to: string
112
+ }
113
+ }
114
+
115
+ const widgetErrorSchema = z.object({ error: z.string() })
116
+
117
+ export function makeDashboardWidgetRoute<TEntity extends object, TItem extends Record<string, unknown>>(config: DashboardWidgetRouteConfig<TEntity, TItem>) {
118
+ const cacheTags = ['widget-data', ...config.cacheTags]
119
+
120
+ const metadata = {
121
+ GET: { requireAuth: true, requireFeatures: ['dashboards.view', config.feature] },
122
+ }
123
+
124
+ async function GET(req: Request) {
125
+ const { translate } = await resolveTranslations()
126
+ try {
127
+ const { container, em, tenantId, organizationIds, limit, datePeriod, customFrom, customTo } = await resolveContext(
128
+ req,
129
+ translate
130
+ )
131
+ const range = (() => {
132
+ if (datePeriod === 'custom') {
133
+ const from = customFrom ? new Date(customFrom) : new Date(0)
134
+ const to = customTo ? new Date(customTo) : new Date()
135
+ return { start: from, end: to }
136
+ }
137
+ const preset = datePeriod === 'last7d' ? 'last_7_days' : datePeriod === 'last30d' ? 'last_30_days' : 'today'
138
+ return resolveDateRange(preset)
139
+ })()
140
+
141
+ let cache: CacheStrategy | null = null
142
+ try {
143
+ cache = container.resolve<CacheStrategy>('cache')
144
+ } catch {
145
+ cache = null
146
+ }
147
+
148
+ const cacheKey = buildCacheKey(config.cacheId, { tenantId, organizationIds, limit, datePeriod, customFrom, customTo })
149
+ const tenantScope = tenantId ?? null
150
+
151
+ if (cache) {
152
+ try {
153
+ const cached = await runWithCacheTenant(tenantScope, () => cache!.get(cacheKey))
154
+ if (cached && typeof cached === 'object' && 'items' in (cached as object)) {
155
+ return NextResponse.json(cached)
156
+ }
157
+ } catch (err) {
158
+ console.debug('[widget-cache] read failed', err)
159
+ }
160
+ }
161
+
162
+ const where: FilterQuery<{ tenantId: string; deletedAt: Date | null; createdAt: Date; organizationId: string }> = {
163
+ tenantId,
164
+ deletedAt: null,
165
+ createdAt: { $gte: range.start, $lte: range.end },
166
+ }
167
+
168
+ if (Array.isArray(organizationIds)) {
169
+ const unique = Array.from(new Set(organizationIds))
170
+ where.organizationId = unique.length === 1 ? unique[0] : { $in: unique }
171
+ }
172
+
173
+ const organizationIdScope = Array.isArray(organizationIds) && organizationIds.length === 1 ? organizationIds[0] : null
174
+ // Generic boundary: config.entity is a class constructor from the factory caller,
175
+ // so we cast to EntityName/FilterQuery at the call site (matching findAndCountWithDecryption's own internal casts)
176
+ const [entities, total] = await findAndCountWithDecryption(
177
+ em,
178
+ config.entity as EntityName<TEntity>,
179
+ where as FilterQuery<TEntity>,
180
+ { limit, orderBy: { createdAt: 'desc' as const } } as FindOptions<TEntity>,
181
+ { tenantId, organizationId: organizationIdScope },
182
+ )
183
+
184
+ const items = (entities as unknown as Record<string, unknown>[]).map(config.mapItem)
185
+
186
+ const response: WidgetResponse<TItem> = {
187
+ items,
188
+ total,
189
+ dateRange: { from: range.start.toISOString(), to: range.end.toISOString() },
190
+ }
191
+
192
+ if (cache) {
193
+ try {
194
+ await runWithCacheTenant(tenantScope, () => cache!.set(cacheKey, response, { ttl: WIDGET_CACHE_TTL, tags: cacheTags }))
195
+ await runWithCacheTenant(tenantScope, () => cache!.set(
196
+ WIDGET_CACHE_SEGMENT_KEY,
197
+ { updatedAt: response.dateRange.to },
198
+ { ttl: WIDGET_CACHE_SEGMENT_TTL, tags: ['widget-data'] },
199
+ ))
200
+ } catch (err) {
201
+ console.debug('[widget-cache] write failed', err)
202
+ }
203
+ }
204
+
205
+ return NextResponse.json(response)
206
+ } catch (err) {
207
+ if (err instanceof CrudHttpError) {
208
+ return NextResponse.json(err.body, { status: err.status })
209
+ }
210
+ console.error(`${config.errorPrefix} failed`, err)
211
+ return NextResponse.json(
212
+ { error: translate(`${config.errorPrefix}.error`, config.openApi.errorFallback) },
213
+ { status: 500 },
214
+ )
215
+ }
216
+ }
217
+
218
+ const responseSchema = z.object({
219
+ items: z.array(config.itemSchema),
220
+ total: z.number(),
221
+ dateRange: z.object({
222
+ from: z.string(),
223
+ to: z.string(),
224
+ }),
225
+ })
226
+
227
+ const openApi: OpenApiRouteDoc = {
228
+ tag: 'Sales',
229
+ summary: config.openApi.summary,
230
+ description: config.openApi.description,
231
+ methods: {
232
+ GET: {
233
+ summary: config.openApi.getSummary,
234
+ query: querySchema,
235
+ responses: [{ status: 200, description: config.openApi.itemDescription, schema: responseSchema }],
236
+ errors: [
237
+ { status: 400, description: 'Invalid query parameters', schema: widgetErrorSchema },
238
+ { status: 401, description: 'Unauthorized', schema: widgetErrorSchema },
239
+ { status: 403, description: 'Forbidden', schema: widgetErrorSchema },
240
+ { status: 500, description: 'Widget failed to load', schema: widgetErrorSchema },
241
+ ],
242
+ },
243
+ },
244
+ }
245
+
246
+ return { GET, metadata, openApi }
247
+ }
@@ -3,12 +3,13 @@
3
3
  import * as React from 'react'
4
4
  import Link from 'next/link'
5
5
  import type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'
6
- import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
7
- import { Spinner } from '@open-mercato/ui/primitives/spinner'
8
- import { Badge } from '@open-mercato/ui/primitives/badge'
9
- import { formatRelativeTime } from '@open-mercato/shared/lib/time'
10
- import { useT } from '@open-mercato/shared/lib/i18n/context'
11
- import { DEFAULT_SETTINGS, hydrateSalesNewOrdersSettings, type DatePeriodOption, type SalesNewOrdersSettings } from './config'
6
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
7
+ import { Spinner } from '@open-mercato/ui/primitives/spinner'
8
+ import { Badge } from '@open-mercato/ui/primitives/badge'
9
+ import { formatRelativeTime } from '@open-mercato/shared/lib/time'
10
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
11
+ import { DEFAULT_SETTINGS, hydrateSalesNewOrdersSettings, type DatePeriodOption, type SalesNewOrdersSettings } from './config'
12
+ import { readString, toDateInputValue, openNativeDatePicker, formatAmount } from '../shared'
12
13
 
13
14
  type NewOrderItem = {
14
15
  id: string
@@ -27,10 +28,6 @@ type NewOrdersApiPayload = {
27
28
  error?: string
28
29
  }
29
30
 
30
- function readString(value: unknown): string | null {
31
- return typeof value === 'string' ? value : null
32
- }
33
-
34
31
  function parseNewOrderItems(payload: NewOrdersApiPayload | null): NewOrderItem[] {
35
32
  const rawItems = Array.isArray(payload?.items) ? payload?.items : []
36
33
  return rawItems
@@ -81,46 +78,6 @@ function resolveDetailHref(item: NewOrderItem): string | null {
81
78
  return item.id ? `/backend/sales/orders/${encodeURIComponent(item.id)}` : null
82
79
  }
83
80
 
84
- function toDateInputValue(value: string | null | undefined): string {
85
- if (!value) return ''
86
- const parsed = new Date(value)
87
- if (Number.isNaN(parsed.getTime())) return ''
88
- const year = String(parsed.getFullYear())
89
- const month = String(parsed.getMonth() + 1).padStart(2, '0')
90
- const day = String(parsed.getDate()).padStart(2, '0')
91
- return `${year}-${month}-${day}`
92
- }
93
-
94
- function openNativeDatePicker(event: React.SyntheticEvent<HTMLInputElement>) {
95
- const input = event.currentTarget
96
- if (typeof input.showPicker === 'function') {
97
- input.showPicker()
98
- }
99
- }
100
-
101
- function formatAmount(value: string, currency: string | null, locale?: string): string {
102
- const numeric = Number(value)
103
- if (!Number.isFinite(numeric)) return '--'
104
- try {
105
- if (currency && currency.trim().length > 0) {
106
- return new Intl.NumberFormat(locale ?? undefined, {
107
- style: 'currency',
108
- currency,
109
- minimumFractionDigits: 2,
110
- maximumFractionDigits: 2,
111
- }).format(numeric)
112
- }
113
- return new Intl.NumberFormat(locale ?? undefined, {
114
- style: 'decimal',
115
- minimumFractionDigits: 0,
116
- maximumFractionDigits: 2,
117
- }).format(numeric)
118
- } catch {
119
- return String(numeric)
120
- }
121
- }
122
-
123
-
124
81
 
125
82
  const SalesNewOrdersWidget: React.FC<DashboardWidgetComponentProps<SalesNewOrdersSettings>> = ({
126
83
  mode,
@@ -3,12 +3,13 @@
3
3
  import * as React from 'react'
4
4
  import Link from 'next/link'
5
5
  import type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'
6
- import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
7
- import { Spinner } from '@open-mercato/ui/primitives/spinner'
8
- import { Badge } from '@open-mercato/ui/primitives/badge'
9
- import { formatRelativeTime } from '@open-mercato/shared/lib/time'
10
- import { useT } from '@open-mercato/shared/lib/i18n/context'
11
- import { DEFAULT_SETTINGS, hydrateSalesNewQuotesSettings, type DatePeriodOption, type SalesNewQuotesSettings } from './config'
6
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
7
+ import { Spinner } from '@open-mercato/ui/primitives/spinner'
8
+ import { Badge } from '@open-mercato/ui/primitives/badge'
9
+ import { formatRelativeTime } from '@open-mercato/shared/lib/time'
10
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
11
+ import { DEFAULT_SETTINGS, hydrateSalesNewQuotesSettings, type DatePeriodOption, type SalesNewQuotesSettings } from './config'
12
+ import { readString, toDateInputValue, openNativeDatePicker, formatAmount } from '../shared'
12
13
 
13
14
  type NewQuoteItem = {
14
15
  id: string
@@ -30,10 +31,6 @@ type NewQuotesApiPayload = {
30
31
  error?: string
31
32
  }
32
33
 
33
- function readString(value: unknown): string | null {
34
- return typeof value === 'string' ? value : null
35
- }
36
-
37
34
  function parseNewQuoteItems(payload: NewQuotesApiPayload | null): NewQuoteItem[] {
38
35
  const rawItems = Array.isArray(payload?.items) ? payload?.items : []
39
36
  return rawItems
@@ -87,45 +84,6 @@ function resolveDetailHref(item: NewQuoteItem): string | null {
87
84
  return item.id ? `/backend/sales/quotes/${encodeURIComponent(item.id)}` : null
88
85
  }
89
86
 
90
- function toDateInputValue(value: string | null | undefined): string {
91
- if (!value) return ''
92
- const parsed = new Date(value)
93
- if (Number.isNaN(parsed.getTime())) return ''
94
- const year = String(parsed.getFullYear())
95
- const month = String(parsed.getMonth() + 1).padStart(2, '0')
96
- const day = String(parsed.getDate()).padStart(2, '0')
97
- return `${year}-${month}-${day}`
98
- }
99
-
100
- function openNativeDatePicker(event: React.SyntheticEvent<HTMLInputElement>) {
101
- const input = event.currentTarget
102
- if (typeof input.showPicker === 'function') {
103
- input.showPicker()
104
- }
105
- }
106
-
107
- function formatAmount(value: string, currency: string | null, locale?: string): string {
108
- const numeric = Number(value)
109
- if (!Number.isFinite(numeric)) return '--'
110
- try {
111
- if (currency && currency.trim().length > 0) {
112
- return new Intl.NumberFormat(locale ?? undefined, {
113
- style: 'currency',
114
- currency,
115
- minimumFractionDigits: 2,
116
- maximumFractionDigits: 2,
117
- }).format(numeric)
118
- }
119
- return new Intl.NumberFormat(locale ?? undefined, {
120
- style: 'decimal',
121
- minimumFractionDigits: 0,
122
- maximumFractionDigits: 2,
123
- }).format(numeric)
124
- } catch {
125
- return String(numeric)
126
- }
127
- }
128
-
129
87
 
130
88
  const SalesNewQuotesWidget: React.FC<DashboardWidgetComponentProps<SalesNewQuotesSettings>> = ({
131
89
  mode,
@@ -0,0 +1,44 @@
1
+ import type React from 'react'
2
+
3
+ export function readString(value: unknown): string | null {
4
+ return typeof value === 'string' ? value : null
5
+ }
6
+
7
+ export function toDateInputValue(value: string | null | undefined): string {
8
+ if (!value) return ''
9
+ const parsed = new Date(value)
10
+ if (Number.isNaN(parsed.getTime())) return ''
11
+ const year = String(parsed.getFullYear())
12
+ const month = String(parsed.getMonth() + 1).padStart(2, '0')
13
+ const day = String(parsed.getDate()).padStart(2, '0')
14
+ return `${year}-${month}-${day}`
15
+ }
16
+
17
+ export function openNativeDatePicker(event: React.SyntheticEvent<HTMLInputElement>) {
18
+ const input = event.currentTarget
19
+ if (typeof input.showPicker === 'function') {
20
+ input.showPicker()
21
+ }
22
+ }
23
+
24
+ export function formatAmount(value: string, currency: string | null, locale?: string): string {
25
+ const numeric = Number(value)
26
+ if (!Number.isFinite(numeric)) return '--'
27
+ try {
28
+ if (currency && currency.trim().length > 0) {
29
+ return new Intl.NumberFormat(locale ?? undefined, {
30
+ style: 'currency',
31
+ currency,
32
+ minimumFractionDigits: 2,
33
+ maximumFractionDigits: 2,
34
+ }).format(numeric)
35
+ }
36
+ return new Intl.NumberFormat(locale ?? undefined, {
37
+ style: 'decimal',
38
+ minimumFractionDigits: 0,
39
+ maximumFractionDigits: 2,
40
+ }).format(numeric)
41
+ } catch {
42
+ return String(numeric)
43
+ }
44
+ }