@open-mercato/core 0.4.2-canary-1b99a1211d → 0.4.2-canary-f6b7824b47
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/api_docs/backend/docs/page.js +1 -4
- package/dist/modules/api_docs/backend/docs/page.js.map +2 -2
- package/dist/modules/auth/lib/setup-app.js +0 -4
- package/dist/modules/auth/lib/setup-app.js.map +2 -2
- package/dist/modules/sales/acl.js +1 -3
- package/dist/modules/sales/acl.js.map +2 -2
- package/package.json +2 -2
- package/src/modules/api_docs/backend/docs/page.tsx +1 -2
- package/src/modules/auth/lib/setup-app.ts +0 -4
- package/src/modules/customers/README.md +1 -2
- package/src/modules/entities/README.md +1 -1
- package/src/modules/sales/acl.ts +0 -2
- package/src/modules/sales/i18n/de.json +1 -32
- package/src/modules/sales/i18n/en.json +1 -32
- package/src/modules/sales/i18n/es.json +1 -32
- package/src/modules/sales/i18n/pl.json +1 -32
- package/dist/modules/sales/api/dashboard/widgets/new-orders/route.js +0 -163
- package/dist/modules/sales/api/dashboard/widgets/new-orders/route.js.map +0 -7
- package/dist/modules/sales/api/dashboard/widgets/new-quotes/route.js +0 -165
- package/dist/modules/sales/api/dashboard/widgets/new-quotes/route.js.map +0 -7
- package/dist/modules/sales/api/dashboard/widgets/utils.js +0 -38
- package/dist/modules/sales/api/dashboard/widgets/utils.js.map +0 -7
- package/dist/modules/sales/lib/customerSnapshot.js +0 -21
- package/dist/modules/sales/lib/customerSnapshot.js.map +0 -7
- package/dist/modules/sales/lib/dateRange.js +0 -39
- package/dist/modules/sales/lib/dateRange.js.map +0 -7
- package/dist/modules/sales/widgets/dashboard/new-orders/config.js +0 -41
- package/dist/modules/sales/widgets/dashboard/new-orders/config.js.map +0 -7
- package/dist/modules/sales/widgets/dashboard/new-orders/widget.client.js +0 -252
- package/dist/modules/sales/widgets/dashboard/new-orders/widget.client.js.map +0 -7
- package/dist/modules/sales/widgets/dashboard/new-orders/widget.js +0 -33
- package/dist/modules/sales/widgets/dashboard/new-orders/widget.js.map +0 -7
- package/dist/modules/sales/widgets/dashboard/new-quotes/config.js +0 -41
- package/dist/modules/sales/widgets/dashboard/new-quotes/config.js.map +0 -7
- package/dist/modules/sales/widgets/dashboard/new-quotes/widget.client.js +0 -272
- package/dist/modules/sales/widgets/dashboard/new-quotes/widget.client.js.map +0 -7
- package/dist/modules/sales/widgets/dashboard/new-quotes/widget.js +0 -33
- package/dist/modules/sales/widgets/dashboard/new-quotes/widget.js.map +0 -7
- package/src/modules/sales/api/dashboard/widgets/new-orders/__tests__/route.test.ts +0 -60
- package/src/modules/sales/api/dashboard/widgets/new-orders/route.ts +0 -192
- package/src/modules/sales/api/dashboard/widgets/new-quotes/__tests__/route.test.ts +0 -61
- package/src/modules/sales/api/dashboard/widgets/new-quotes/route.ts +0 -194
- package/src/modules/sales/api/dashboard/widgets/utils.ts +0 -53
- package/src/modules/sales/lib/__tests__/dateRange.test.ts +0 -26
- package/src/modules/sales/lib/customerSnapshot.ts +0 -17
- package/src/modules/sales/lib/dateRange.ts +0 -42
- package/src/modules/sales/widgets/dashboard/new-orders/__tests__/config.test.ts +0 -28
- package/src/modules/sales/widgets/dashboard/new-orders/config.ts +0 -55
- package/src/modules/sales/widgets/dashboard/new-orders/widget.client.tsx +0 -295
- package/src/modules/sales/widgets/dashboard/new-orders/widget.ts +0 -33
- package/src/modules/sales/widgets/dashboard/new-quotes/__tests__/config.test.ts +0 -28
- package/src/modules/sales/widgets/dashboard/new-quotes/config.ts +0 -55
- package/src/modules/sales/widgets/dashboard/new-quotes/widget.client.tsx +0 -322
- package/src/modules/sales/widgets/dashboard/new-quotes/widget.ts +0 -33
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { GET } from '../route'
|
|
2
|
-
|
|
3
|
-
jest.mock('../../utils', () => ({
|
|
4
|
-
resolveWidgetScope: jest.fn(async () => ({
|
|
5
|
-
container: {},
|
|
6
|
-
em: {},
|
|
7
|
-
tenantId: '33333333-3333-3333-3333-333333333333',
|
|
8
|
-
organizationIds: ['22222222-2222-2222-2222-222222222222'],
|
|
9
|
-
})),
|
|
10
|
-
}))
|
|
11
|
-
|
|
12
|
-
jest.mock('@open-mercato/shared/lib/i18n/server', () => ({
|
|
13
|
-
resolveTranslations: async () => ({
|
|
14
|
-
translate: (k: string, fb?: string) => fb ?? k,
|
|
15
|
-
}),
|
|
16
|
-
}))
|
|
17
|
-
|
|
18
|
-
jest.mock('@open-mercato/shared/lib/encryption/find', () => ({
|
|
19
|
-
findAndCountWithDecryption: jest.fn(async () => [
|
|
20
|
-
[
|
|
21
|
-
{
|
|
22
|
-
id: '11111111-1111-1111-1111-111111111111',
|
|
23
|
-
quoteNumber: 'QT-2001',
|
|
24
|
-
status: 'draft',
|
|
25
|
-
customerSnapshot: { displayName: 'Acme Corp' },
|
|
26
|
-
customerEntityId: '44444444-4444-4444-4444-444444444444',
|
|
27
|
-
validFrom: new Date('2026-01-20T00:00:00.000Z'),
|
|
28
|
-
validUntil: new Date('2026-02-01T00:00:00.000Z'),
|
|
29
|
-
grandTotalNetAmount: '80.00',
|
|
30
|
-
grandTotalGrossAmount: '96.00',
|
|
31
|
-
currencyCode: 'USD',
|
|
32
|
-
createdAt: new Date('2026-01-27T10:00:00.000Z'),
|
|
33
|
-
convertedOrderId: null,
|
|
34
|
-
},
|
|
35
|
-
],
|
|
36
|
-
1,
|
|
37
|
-
]),
|
|
38
|
-
}))
|
|
39
|
-
|
|
40
|
-
describe('sales new-quotes widget route', () => {
|
|
41
|
-
it('returns 200 with items on happy path', async () => {
|
|
42
|
-
const req = new Request('http://localhost/api?limit=5')
|
|
43
|
-
const res = await GET(req)
|
|
44
|
-
expect(res.status).toBe(200)
|
|
45
|
-
const body = await res.json()
|
|
46
|
-
expect(Array.isArray(body.items)).toBe(true)
|
|
47
|
-
expect(body.items[0]).toMatchObject({
|
|
48
|
-
id: '11111111-1111-1111-1111-111111111111',
|
|
49
|
-
quoteNumber: 'QT-2001',
|
|
50
|
-
customerName: 'Acme Corp',
|
|
51
|
-
})
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it('returns 400 on invalid limit', async () => {
|
|
55
|
-
const req = new Request('http://localhost/api?limit=0')
|
|
56
|
-
const res = await GET(req)
|
|
57
|
-
expect(res.status).toBe(400)
|
|
58
|
-
const body = await res.json()
|
|
59
|
-
expect(body).toHaveProperty('error')
|
|
60
|
-
})
|
|
61
|
-
})
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server'
|
|
2
|
-
import { z } from 'zod'
|
|
3
|
-
import type { FilterQuery } from '@mikro-orm/core'
|
|
4
|
-
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
5
|
-
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
6
|
-
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
7
|
-
import { findAndCountWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
8
|
-
import { SalesQuote } from '../../../../data/entities'
|
|
9
|
-
import { resolveWidgetScope, type WidgetScopeContext } from '../utils'
|
|
10
|
-
import { extractCustomerName } from '../../../../lib/customerSnapshot'
|
|
11
|
-
import { parseDateInput, resolveDateRange, type DatePeriodOption } from '../../../../lib/dateRange'
|
|
12
|
-
|
|
13
|
-
const querySchema = z.object({
|
|
14
|
-
limit: z.coerce.number().min(1).max(20).default(5),
|
|
15
|
-
datePeriod: z.enum(['last24h', 'last7d', 'last30d', 'custom']).default('last24h'),
|
|
16
|
-
customFrom: z.string().optional(),
|
|
17
|
-
customTo: z.string().optional(),
|
|
18
|
-
tenantId: z.string().uuid().optional(),
|
|
19
|
-
organizationId: z.string().uuid().optional(),
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
export const metadata = {
|
|
23
|
-
GET: { requireAuth: true, requireFeatures: ['dashboards.view', 'sales.widgets.new-quotes'] },
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
type WidgetContext = WidgetScopeContext & {
|
|
27
|
-
limit: number
|
|
28
|
-
datePeriod: DatePeriodOption
|
|
29
|
-
customFrom?: string
|
|
30
|
-
customTo?: string
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function resolveContext(
|
|
34
|
-
req: Request,
|
|
35
|
-
translate: (key: string, fallback?: string) => string,
|
|
36
|
-
): Promise<WidgetContext> {
|
|
37
|
-
const url = new URL(req.url)
|
|
38
|
-
const rawQuery: Record<string, string> = {}
|
|
39
|
-
for (const [key, value] of url.searchParams.entries()) {
|
|
40
|
-
rawQuery[key] = value
|
|
41
|
-
}
|
|
42
|
-
const parsed = querySchema.safeParse(rawQuery)
|
|
43
|
-
if (!parsed.success) {
|
|
44
|
-
throw new CrudHttpError(400, { error: translate('sales.errors.invalid_query', 'Invalid query parameters') })
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const { container, em, tenantId, organizationIds } = await resolveWidgetScope(req, translate, {
|
|
48
|
-
tenantId: parsed.data.tenantId ?? null,
|
|
49
|
-
organizationId: parsed.data.organizationId ?? null,
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
return {
|
|
53
|
-
container,
|
|
54
|
-
em,
|
|
55
|
-
tenantId,
|
|
56
|
-
organizationIds,
|
|
57
|
-
limit: parsed.data.limit,
|
|
58
|
-
datePeriod: parsed.data.datePeriod,
|
|
59
|
-
customFrom: parsed.data.customFrom,
|
|
60
|
-
customTo: parsed.data.customTo,
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function resolveDateRangeOrThrow(
|
|
65
|
-
period: DatePeriodOption,
|
|
66
|
-
customFrom: string | undefined,
|
|
67
|
-
customTo: string | undefined,
|
|
68
|
-
translate: (key: string, fallback?: string) => string,
|
|
69
|
-
): { from: Date; to: Date } {
|
|
70
|
-
const parsedFrom = parseDateInput(customFrom)
|
|
71
|
-
const parsedTo = parseDateInput(customTo)
|
|
72
|
-
if (customFrom && !parsedFrom) {
|
|
73
|
-
throw new CrudHttpError(400, { error: translate('sales.errors.invalid_date', 'Invalid date range') })
|
|
74
|
-
}
|
|
75
|
-
if (customTo && !parsedTo) {
|
|
76
|
-
throw new CrudHttpError(400, { error: translate('sales.errors.invalid_date', 'Invalid date range') })
|
|
77
|
-
}
|
|
78
|
-
return resolveDateRange(period, parsedFrom, parsedTo)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export async function GET(req: Request) {
|
|
82
|
-
const { translate } = await resolveTranslations()
|
|
83
|
-
try {
|
|
84
|
-
const { em, tenantId, organizationIds, limit, datePeriod, customFrom, customTo } = await resolveContext(
|
|
85
|
-
req,
|
|
86
|
-
translate,
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
const { from, to } = resolveDateRangeOrThrow(datePeriod, customFrom, customTo, translate)
|
|
90
|
-
|
|
91
|
-
const where: FilterQuery<SalesQuote> = {
|
|
92
|
-
tenantId,
|
|
93
|
-
deletedAt: null,
|
|
94
|
-
createdAt: { $gte: from, $lte: to },
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (Array.isArray(organizationIds)) {
|
|
98
|
-
where.organizationId =
|
|
99
|
-
organizationIds.length === 1 ? organizationIds[0] : { $in: Array.from(new Set(organizationIds)) }
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const [items, total] = await findAndCountWithDecryption(
|
|
103
|
-
em,
|
|
104
|
-
SalesQuote,
|
|
105
|
-
where,
|
|
106
|
-
{
|
|
107
|
-
limit,
|
|
108
|
-
orderBy: { createdAt: 'desc' as const },
|
|
109
|
-
},
|
|
110
|
-
{ tenantId },
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
const responseItems = items.map((quote) => ({
|
|
114
|
-
id: quote.id,
|
|
115
|
-
quoteNumber: quote.quoteNumber,
|
|
116
|
-
status: quote.status ?? null,
|
|
117
|
-
customerName: extractCustomerName(quote.customerSnapshot) ?? null,
|
|
118
|
-
customerEntityId: quote.customerEntityId ?? null,
|
|
119
|
-
validFrom: quote.validFrom ? quote.validFrom.toISOString() : null,
|
|
120
|
-
validUntil: quote.validUntil ? quote.validUntil.toISOString() : null,
|
|
121
|
-
netAmount: quote.grandTotalNetAmount,
|
|
122
|
-
grossAmount: quote.grandTotalGrossAmount,
|
|
123
|
-
currency: quote.currencyCode ?? null,
|
|
124
|
-
createdAt: quote.createdAt.toISOString(),
|
|
125
|
-
convertedOrderId: quote.convertedOrderId ?? null,
|
|
126
|
-
}))
|
|
127
|
-
|
|
128
|
-
return NextResponse.json({
|
|
129
|
-
items: responseItems,
|
|
130
|
-
total,
|
|
131
|
-
dateRange: {
|
|
132
|
-
from: from.toISOString(),
|
|
133
|
-
to: to.toISOString(),
|
|
134
|
-
},
|
|
135
|
-
})
|
|
136
|
-
} catch (err) {
|
|
137
|
-
if (err instanceof CrudHttpError) {
|
|
138
|
-
return NextResponse.json(err.body, { status: err.status })
|
|
139
|
-
}
|
|
140
|
-
console.error('sales.widgets.newQuotes failed', err)
|
|
141
|
-
return NextResponse.json(
|
|
142
|
-
{ error: translate('sales.widgets.newQuotes.error', 'Failed to load recent quotes') },
|
|
143
|
-
{ status: 500 },
|
|
144
|
-
)
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const quoteItemSchema = z.object({
|
|
149
|
-
id: z.string().uuid(),
|
|
150
|
-
quoteNumber: z.string(),
|
|
151
|
-
status: z.string().nullable(),
|
|
152
|
-
customerName: z.string().nullable(),
|
|
153
|
-
customerEntityId: z.string().uuid().nullable(),
|
|
154
|
-
validFrom: z.string().nullable(),
|
|
155
|
-
validUntil: z.string().nullable(),
|
|
156
|
-
netAmount: z.string(),
|
|
157
|
-
grossAmount: z.string(),
|
|
158
|
-
currency: z.string().nullable(),
|
|
159
|
-
createdAt: z.string(),
|
|
160
|
-
convertedOrderId: z.string().uuid().nullable(),
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
const responseSchema = z.object({
|
|
164
|
-
items: z.array(quoteItemSchema),
|
|
165
|
-
total: z.number(),
|
|
166
|
-
dateRange: z.object({
|
|
167
|
-
from: z.string(),
|
|
168
|
-
to: z.string(),
|
|
169
|
-
}),
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
const widgetErrorSchema = z.object({
|
|
173
|
-
error: z.string(),
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
export const openApi: OpenApiRouteDoc = {
|
|
177
|
-
tag: 'Sales',
|
|
178
|
-
summary: 'New quotes widget',
|
|
179
|
-
methods: {
|
|
180
|
-
GET: {
|
|
181
|
-
summary: 'Fetch recently created sales quotes',
|
|
182
|
-
description: 'Returns the most recent sales quotes within the scoped tenant/organization.',
|
|
183
|
-
query: querySchema,
|
|
184
|
-
responses: [
|
|
185
|
-
{ status: 200, description: 'Widget payload', schema: responseSchema },
|
|
186
|
-
],
|
|
187
|
-
errors: [
|
|
188
|
-
{ status: 400, description: 'Invalid query parameters', schema: widgetErrorSchema },
|
|
189
|
-
{ status: 401, description: 'Unauthorized', schema: widgetErrorSchema },
|
|
190
|
-
{ status: 500, description: 'Widget failed to load', schema: widgetErrorSchema },
|
|
191
|
-
],
|
|
192
|
-
},
|
|
193
|
-
},
|
|
194
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
-
import { createRequestContainer, type AppContainer } from '@open-mercato/shared/lib/di/container'
|
|
3
|
-
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
4
|
-
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
5
|
-
import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
|
|
6
|
-
|
|
7
|
-
export type WidgetScopeContext = {
|
|
8
|
-
container: AppContainer
|
|
9
|
-
em: EntityManager
|
|
10
|
-
tenantId: string
|
|
11
|
-
organizationIds: string[] | null
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export async function resolveWidgetScope(
|
|
15
|
-
req: Request,
|
|
16
|
-
translate: (key: string, fallback?: string) => string,
|
|
17
|
-
overrides?: { tenantId?: string | null; organizationId?: string | null },
|
|
18
|
-
): Promise<WidgetScopeContext> {
|
|
19
|
-
const auth = await getAuthFromRequest(req)
|
|
20
|
-
if (!auth) {
|
|
21
|
-
throw new CrudHttpError(401, { error: translate('sales.errors.unauthorized', 'Unauthorized') })
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const container = await createRequestContainer()
|
|
25
|
-
const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })
|
|
26
|
-
|
|
27
|
-
const tenantId = overrides?.tenantId ?? auth.tenantId ?? null
|
|
28
|
-
if (!tenantId) {
|
|
29
|
-
throw new CrudHttpError(400, { error: translate('sales.errors.tenant_required', 'Tenant context is required') })
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const organizationIds = (() => {
|
|
33
|
-
if (overrides?.organizationId) return [overrides.organizationId]
|
|
34
|
-
if (scope?.selectedId) return [scope.selectedId]
|
|
35
|
-
if (Array.isArray(scope?.filterIds) && scope.filterIds.length > 0) return scope.filterIds
|
|
36
|
-
if (scope?.allowedIds === null) return null
|
|
37
|
-
if (auth.orgId) return [auth.orgId]
|
|
38
|
-
return []
|
|
39
|
-
})()
|
|
40
|
-
|
|
41
|
-
if (organizationIds !== null && organizationIds.length === 0) {
|
|
42
|
-
throw new CrudHttpError(400, { error: translate('sales.errors.organization_required', 'Organization context is required') })
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const em = container.resolve('em') as EntityManager
|
|
46
|
-
|
|
47
|
-
return {
|
|
48
|
-
container,
|
|
49
|
-
em,
|
|
50
|
-
tenantId,
|
|
51
|
-
organizationIds,
|
|
52
|
-
}
|
|
53
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { parseDateInput, resolveDateRange } from '../dateRange'
|
|
2
|
-
|
|
3
|
-
describe('resolveDateRange', () => {
|
|
4
|
-
it('resolves last24h', () => {
|
|
5
|
-
const now = new Date('2026-01-27T12:00:00.000Z')
|
|
6
|
-
const { from, to } = resolveDateRange('last24h', null, null, now)
|
|
7
|
-
expect(to.toISOString()).toBe(now.toISOString())
|
|
8
|
-
const diff = to.getTime() - from.getTime()
|
|
9
|
-
expect(diff).toBe(24 * 60 * 60 * 1000)
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
it('resolves last7d', () => {
|
|
13
|
-
const now = new Date('2026-01-27T12:00:00.000Z')
|
|
14
|
-
const { from, to } = resolveDateRange('last7d', null, null, now)
|
|
15
|
-
const diff = to.getTime() - from.getTime()
|
|
16
|
-
expect(diff).toBe(7 * 24 * 60 * 60 * 1000)
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
it('resolves custom range', () => {
|
|
20
|
-
const fromInput = parseDateInput('2026-01-20T00:00:00Z')
|
|
21
|
-
const toInput = parseDateInput('2026-01-27T23:59:59Z')
|
|
22
|
-
const { from, to } = resolveDateRange('custom', fromInput, toInput, new Date('2026-01-27T12:00:00Z'))
|
|
23
|
-
expect(from.toISOString()).toBe('2026-01-20T00:00:00.000Z')
|
|
24
|
-
expect(to.toISOString()).toBe('2026-01-27T23:59:59.000Z')
|
|
25
|
-
})
|
|
26
|
-
})
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export function extractCustomerName(snapshot: unknown): string | null {
|
|
2
|
-
if (!snapshot || typeof snapshot !== 'object') return null
|
|
3
|
-
const data = snapshot as Record<string, unknown>
|
|
4
|
-
const candidates = [
|
|
5
|
-
data.display_name,
|
|
6
|
-
data.displayName,
|
|
7
|
-
data.name,
|
|
8
|
-
data.company_name,
|
|
9
|
-
data.companyName,
|
|
10
|
-
data.full_name,
|
|
11
|
-
data.fullName,
|
|
12
|
-
]
|
|
13
|
-
for (const candidate of candidates) {
|
|
14
|
-
if (typeof candidate === 'string' && candidate.trim().length > 0) return candidate
|
|
15
|
-
}
|
|
16
|
-
return null
|
|
17
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
export type DatePeriodOption = 'last24h' | 'last7d' | 'last30d' | 'custom'
|
|
2
|
-
|
|
3
|
-
export const DATE_PERIOD_OPTIONS: DatePeriodOption[] = ['last24h', 'last7d', 'last30d', 'custom']
|
|
4
|
-
|
|
5
|
-
export function parseDateInput(value?: string | null): Date | null {
|
|
6
|
-
if (!value || typeof value !== 'string') return null
|
|
7
|
-
const date = new Date(value)
|
|
8
|
-
if (Number.isNaN(date.getTime())) return null
|
|
9
|
-
return date
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function resolveDateRange(
|
|
13
|
-
period: DatePeriodOption,
|
|
14
|
-
customFrom?: Date | null,
|
|
15
|
-
customTo?: Date | null,
|
|
16
|
-
now: Date = new Date(),
|
|
17
|
-
): { from: Date; to: Date } {
|
|
18
|
-
const to = now
|
|
19
|
-
switch (period) {
|
|
20
|
-
case 'last24h': {
|
|
21
|
-
const from = new Date(now.getTime() - 24 * 60 * 60 * 1000)
|
|
22
|
-
return { from, to }
|
|
23
|
-
}
|
|
24
|
-
case 'last7d': {
|
|
25
|
-
const from = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
|
|
26
|
-
return { from, to }
|
|
27
|
-
}
|
|
28
|
-
case 'last30d': {
|
|
29
|
-
const from = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000)
|
|
30
|
-
return { from, to }
|
|
31
|
-
}
|
|
32
|
-
case 'custom': {
|
|
33
|
-
const from = customFrom ?? new Date(0)
|
|
34
|
-
const boundedTo = customTo ?? now
|
|
35
|
-
return { from, to: boundedTo }
|
|
36
|
-
}
|
|
37
|
-
default: {
|
|
38
|
-
const from = new Date(now.getTime() - 24 * 60 * 60 * 1000)
|
|
39
|
-
return { from, to }
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { DEFAULT_SETTINGS, hydrateSalesNewOrdersSettings } from '../config'
|
|
2
|
-
|
|
3
|
-
describe('hydrateSalesNewOrdersSettings', () => {
|
|
4
|
-
it('returns default settings for null input', () => {
|
|
5
|
-
expect(hydrateSalesNewOrdersSettings(null)).toEqual(DEFAULT_SETTINGS)
|
|
6
|
-
})
|
|
7
|
-
|
|
8
|
-
it('validates and clamps pageSize', () => {
|
|
9
|
-
expect(hydrateSalesNewOrdersSettings({ pageSize: 0 })).toHaveProperty('pageSize', DEFAULT_SETTINGS.pageSize)
|
|
10
|
-
expect(hydrateSalesNewOrdersSettings({ pageSize: 25 })).toHaveProperty('pageSize', 20)
|
|
11
|
-
expect(hydrateSalesNewOrdersSettings({ pageSize: 10 })).toHaveProperty('pageSize', 10)
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
it('validates datePeriod', () => {
|
|
15
|
-
expect(hydrateSalesNewOrdersSettings({ datePeriod: 'invalid' })).toHaveProperty('datePeriod', DEFAULT_SETTINGS.datePeriod)
|
|
16
|
-
expect(hydrateSalesNewOrdersSettings({ datePeriod: 'last7d' })).toHaveProperty('datePeriod', 'last7d')
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
it('includes custom dates only when datePeriod is custom', () => {
|
|
20
|
-
const result = hydrateSalesNewOrdersSettings({
|
|
21
|
-
datePeriod: 'custom',
|
|
22
|
-
customFrom: '2026-01-20T00:00:00Z',
|
|
23
|
-
customTo: '2026-01-27T23:59:59Z',
|
|
24
|
-
})
|
|
25
|
-
expect(result.customFrom).toBe('2026-01-20T00:00:00Z')
|
|
26
|
-
expect(result.customTo).toBe('2026-01-27T23:59:59Z')
|
|
27
|
-
})
|
|
28
|
-
})
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import type { DatePeriodOption } from '../../../lib/dateRange'
|
|
2
|
-
|
|
3
|
-
export type SalesNewOrdersSettings = {
|
|
4
|
-
pageSize: number
|
|
5
|
-
datePeriod: DatePeriodOption
|
|
6
|
-
customFrom?: string
|
|
7
|
-
customTo?: string
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export const DEFAULT_SETTINGS: SalesNewOrdersSettings = {
|
|
11
|
-
pageSize: 5,
|
|
12
|
-
datePeriod: 'last24h',
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function hydrateSalesNewOrdersSettings(raw: unknown): SalesNewOrdersSettings {
|
|
16
|
-
if (!raw || typeof raw !== 'object') return { ...DEFAULT_SETTINGS }
|
|
17
|
-
const input = raw as Partial<SalesNewOrdersSettings>
|
|
18
|
-
const parsedPageSize = Number(input.pageSize)
|
|
19
|
-
let pageSize = DEFAULT_SETTINGS.pageSize
|
|
20
|
-
if (Number.isFinite(parsedPageSize)) {
|
|
21
|
-
if (parsedPageSize < 1) {
|
|
22
|
-
pageSize = DEFAULT_SETTINGS.pageSize
|
|
23
|
-
} else if (parsedPageSize > 20) {
|
|
24
|
-
pageSize = 20
|
|
25
|
-
} else {
|
|
26
|
-
pageSize = Math.floor(parsedPageSize)
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const datePeriod: DatePeriodOption =
|
|
31
|
-
input.datePeriod === 'last24h' ||
|
|
32
|
-
input.datePeriod === 'last7d' ||
|
|
33
|
-
input.datePeriod === 'last30d' ||
|
|
34
|
-
input.datePeriod === 'custom'
|
|
35
|
-
? input.datePeriod
|
|
36
|
-
: DEFAULT_SETTINGS.datePeriod
|
|
37
|
-
|
|
38
|
-
let customFrom: string | undefined
|
|
39
|
-
let customTo: string | undefined
|
|
40
|
-
if (datePeriod === 'custom') {
|
|
41
|
-
if (typeof input.customFrom === 'string' && !Number.isNaN(new Date(input.customFrom).getTime())) {
|
|
42
|
-
customFrom = input.customFrom
|
|
43
|
-
}
|
|
44
|
-
if (typeof input.customTo === 'string' && !Number.isNaN(new Date(input.customTo).getTime())) {
|
|
45
|
-
customTo = input.customTo
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
pageSize,
|
|
51
|
-
datePeriod,
|
|
52
|
-
customFrom,
|
|
53
|
-
customTo,
|
|
54
|
-
}
|
|
55
|
-
}
|