@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.
- package/dist/modules/currencies/backend/exchange-rates/[id]/page.js +17 -154
- package/dist/modules/currencies/backend/exchange-rates/[id]/page.js.map +3 -3
- package/dist/modules/currencies/backend/exchange-rates/create/page.js +14 -152
- package/dist/modules/currencies/backend/exchange-rates/create/page.js.map +2 -2
- package/dist/modules/currencies/lib/exchangeRateFormConfig.js +167 -0
- package/dist/modules/currencies/lib/exchangeRateFormConfig.js.map +7 -0
- package/dist/modules/customers/api/dashboard/widgets/utils.js +1 -34
- package/dist/modules/customers/api/dashboard/widgets/utils.js.map +2 -2
- package/dist/modules/customers/commands/activities.js +3 -8
- package/dist/modules/customers/commands/activities.js.map +2 -2
- package/dist/modules/customers/commands/comments.js +2 -8
- package/dist/modules/customers/commands/comments.js.map +2 -2
- package/dist/modules/dashboards/lib/widgetScope.js +38 -0
- package/dist/modules/dashboards/lib/widgetScope.js.map +7 -0
- package/dist/modules/entities/lib/makeActivityRoute.js +265 -0
- package/dist/modules/entities/lib/makeActivityRoute.js.map +7 -0
- package/dist/modules/resources/api/activities.js +24 -232
- package/dist/modules/resources/api/activities.js.map +2 -2
- package/dist/modules/resources/commands/activities.js +3 -8
- package/dist/modules/resources/commands/activities.js.map +2 -2
- package/dist/modules/resources/commands/comments.js +2 -8
- package/dist/modules/resources/commands/comments.js.map +2 -2
- package/dist/modules/sales/api/dashboard/widgets/new-orders/route.js +27 -182
- package/dist/modules/sales/api/dashboard/widgets/new-orders/route.js.map +2 -2
- package/dist/modules/sales/api/dashboard/widgets/new-quotes/route.js +28 -183
- package/dist/modules/sales/api/dashboard/widgets/new-quotes/route.js.map +2 -2
- package/dist/modules/sales/api/order-line-statuses/route.js +15 -194
- package/dist/modules/sales/api/order-line-statuses/route.js.map +2 -2
- package/dist/modules/sales/api/order-lines/route.js +15 -281
- package/dist/modules/sales/api/order-lines/route.js.map +2 -2
- package/dist/modules/sales/api/order-statuses/route.js +15 -194
- package/dist/modules/sales/api/order-statuses/route.js.map +2 -2
- package/dist/modules/sales/api/payment-statuses/route.js +15 -194
- package/dist/modules/sales/api/payment-statuses/route.js.map +2 -2
- package/dist/modules/sales/api/quote-lines/route.js +15 -279
- package/dist/modules/sales/api/quote-lines/route.js.map +2 -2
- package/dist/modules/sales/api/shipment-statuses/route.js +15 -194
- package/dist/modules/sales/api/shipment-statuses/route.js.map +2 -2
- package/dist/modules/sales/components/PaymentMethodsSettings.js +3 -84
- package/dist/modules/sales/components/PaymentMethodsSettings.js.map +2 -2
- package/dist/modules/sales/components/ProviderFieldInput.js +86 -0
- package/dist/modules/sales/components/ProviderFieldInput.js.map +7 -0
- package/dist/modules/sales/components/ShippingMethodsSettings.js +3 -82
- package/dist/modules/sales/components/ShippingMethodsSettings.js.map +2 -2
- package/dist/modules/sales/lib/makeSalesLineRoute.js +308 -0
- package/dist/modules/sales/lib/makeSalesLineRoute.js.map +7 -0
- package/dist/modules/sales/lib/makeStatusDictionaryRoute.js +206 -0
- package/dist/modules/sales/lib/makeStatusDictionaryRoute.js.map +7 -0
- package/dist/modules/sales/widgets/dashboard/makeDashboardWidgetRoute.js +178 -0
- package/dist/modules/sales/widgets/dashboard/makeDashboardWidgetRoute.js.map +7 -0
- package/dist/modules/sales/widgets/dashboard/new-orders/widget.client.js +1 -39
- package/dist/modules/sales/widgets/dashboard/new-orders/widget.client.js.map +2 -2
- package/dist/modules/sales/widgets/dashboard/new-quotes/widget.client.js +1 -39
- package/dist/modules/sales/widgets/dashboard/new-quotes/widget.client.js.map +2 -2
- package/dist/modules/sales/widgets/dashboard/shared.js +46 -0
- package/dist/modules/sales/widgets/dashboard/shared.js.map +7 -0
- package/dist/modules/staff/api/activities.js +24 -232
- package/dist/modules/staff/api/activities.js.map +2 -2
- package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js +14 -34
- package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/my-leave-requests/[id]/page.js +15 -34
- package/dist/modules/staff/backend/staff/my-leave-requests/[id]/page.js.map +2 -2
- package/dist/modules/staff/commands/activities.js +3 -8
- package/dist/modules/staff/commands/activities.js.map +2 -2
- package/dist/modules/staff/commands/comments.js +2 -8
- package/dist/modules/staff/commands/comments.js.map +2 -2
- package/dist/modules/staff/lib/leaveRequestHelpers.js +41 -0
- package/dist/modules/staff/lib/leaveRequestHelpers.js.map +7 -0
- package/package.json +2 -2
- package/src/modules/currencies/backend/exchange-rates/[id]/page.tsx +20 -180
- package/src/modules/currencies/backend/exchange-rates/create/page.tsx +16 -175
- package/src/modules/currencies/lib/exchangeRateFormConfig.ts +200 -0
- package/src/modules/customers/api/dashboard/widgets/utils.ts +1 -53
- package/src/modules/customers/commands/activities.ts +2 -8
- package/src/modules/customers/commands/comments.ts +2 -8
- package/src/modules/dashboards/i18n/de.json +3 -0
- package/src/modules/dashboards/i18n/en.json +3 -0
- package/src/modules/dashboards/i18n/es.json +3 -0
- package/src/modules/dashboards/i18n/pl.json +3 -0
- package/src/modules/dashboards/lib/widgetScope.ts +53 -0
- package/src/modules/entities/lib/makeActivityRoute.ts +327 -0
- package/src/modules/resources/api/activities.ts +25 -269
- package/src/modules/resources/commands/activities.ts +2 -7
- package/src/modules/resources/commands/comments.ts +2 -8
- package/src/modules/sales/api/dashboard/widgets/new-orders/route.ts +29 -244
- package/src/modules/sales/api/dashboard/widgets/new-quotes/route.ts +30 -245
- package/src/modules/sales/api/order-line-statuses/route.ts +16 -209
- package/src/modules/sales/api/order-lines/route.ts +16 -300
- package/src/modules/sales/api/order-statuses/route.ts +16 -209
- package/src/modules/sales/api/payment-statuses/route.ts +16 -209
- package/src/modules/sales/api/quote-lines/route.ts +16 -298
- package/src/modules/sales/api/shipment-statuses/route.ts +16 -209
- package/src/modules/sales/components/PaymentMethodsSettings.tsx +3 -88
- package/src/modules/sales/components/ProviderFieldInput.tsx +85 -0
- package/src/modules/sales/components/ShippingMethodsSettings.tsx +3 -86
- package/src/modules/sales/lib/makeSalesLineRoute.ts +345 -0
- package/src/modules/sales/lib/makeStatusDictionaryRoute.ts +229 -0
- package/src/modules/sales/widgets/dashboard/makeDashboardWidgetRoute.ts +247 -0
- package/src/modules/sales/widgets/dashboard/new-orders/widget.client.tsx +7 -50
- package/src/modules/sales/widgets/dashboard/new-quotes/widget.client.tsx +7 -49
- package/src/modules/sales/widgets/dashboard/shared.ts +44 -0
- package/src/modules/staff/api/activities.ts +25 -269
- package/src/modules/staff/backend/staff/leave-requests/[id]/page.tsx +15 -69
- package/src/modules/staff/backend/staff/my-leave-requests/[id]/page.tsx +16 -65
- package/src/modules/staff/commands/activities.ts +2 -7
- package/src/modules/staff/commands/comments.ts +2 -8
- 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
|
+
}
|