@open-mercato/core 0.4.2-canary-f51f197c8f → 0.4.2-canary-92bc12ea91
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 +4 -1
- package/dist/modules/api_docs/backend/docs/page.js.map +2 -2
- package/dist/modules/auth/lib/setup-app.js +4 -0
- package/dist/modules/auth/lib/setup-app.js.map +2 -2
- package/dist/modules/sales/acl.js +3 -1
- package/dist/modules/sales/acl.js.map +2 -2
- package/dist/modules/sales/api/dashboard/widgets/new-orders/route.js +163 -0
- package/dist/modules/sales/api/dashboard/widgets/new-orders/route.js.map +7 -0
- package/dist/modules/sales/api/dashboard/widgets/new-quotes/route.js +165 -0
- package/dist/modules/sales/api/dashboard/widgets/new-quotes/route.js.map +7 -0
- package/dist/modules/sales/api/dashboard/widgets/utils.js +38 -0
- package/dist/modules/sales/api/dashboard/widgets/utils.js.map +7 -0
- package/dist/modules/sales/lib/customerSnapshot.js +21 -0
- package/dist/modules/sales/lib/customerSnapshot.js.map +7 -0
- package/dist/modules/sales/lib/dateRange.js +39 -0
- package/dist/modules/sales/lib/dateRange.js.map +7 -0
- package/dist/modules/sales/widgets/dashboard/new-orders/config.js +32 -0
- package/dist/modules/sales/widgets/dashboard/new-orders/config.js.map +7 -0
- package/dist/modules/sales/widgets/dashboard/new-orders/widget.client.js +252 -0
- package/dist/modules/sales/widgets/dashboard/new-orders/widget.client.js.map +7 -0
- package/dist/modules/sales/widgets/dashboard/new-orders/widget.js +33 -0
- package/dist/modules/sales/widgets/dashboard/new-orders/widget.js.map +7 -0
- package/dist/modules/sales/widgets/dashboard/new-quotes/config.js +32 -0
- package/dist/modules/sales/widgets/dashboard/new-quotes/config.js.map +7 -0
- package/dist/modules/sales/widgets/dashboard/new-quotes/widget.client.js +272 -0
- package/dist/modules/sales/widgets/dashboard/new-quotes/widget.client.js.map +7 -0
- package/dist/modules/sales/widgets/dashboard/new-quotes/widget.js +33 -0
- package/dist/modules/sales/widgets/dashboard/new-quotes/widget.js.map +7 -0
- package/package.json +2 -2
- package/src/modules/api_docs/backend/docs/page.tsx +2 -1
- package/src/modules/auth/lib/setup-app.ts +4 -0
- package/src/modules/customers/README.md +2 -1
- package/src/modules/entities/README.md +1 -1
- package/src/modules/sales/acl.ts +2 -0
- package/src/modules/sales/api/dashboard/widgets/new-orders/__tests__/route.test.ts +60 -0
- package/src/modules/sales/api/dashboard/widgets/new-orders/route.ts +192 -0
- package/src/modules/sales/api/dashboard/widgets/new-quotes/__tests__/route.test.ts +61 -0
- package/src/modules/sales/api/dashboard/widgets/new-quotes/route.ts +194 -0
- package/src/modules/sales/api/dashboard/widgets/utils.ts +53 -0
- package/src/modules/sales/i18n/de.json +32 -1
- package/src/modules/sales/i18n/en.json +32 -1
- package/src/modules/sales/i18n/es.json +32 -1
- package/src/modules/sales/i18n/pl.json +32 -1
- package/src/modules/sales/lib/__tests__/dateRange.test.ts +26 -0
- package/src/modules/sales/lib/customerSnapshot.ts +17 -0
- package/src/modules/sales/lib/dateRange.ts +42 -0
- package/src/modules/sales/widgets/dashboard/new-orders/__tests__/config.test.ts +28 -0
- package/src/modules/sales/widgets/dashboard/new-orders/config.ts +49 -0
- package/src/modules/sales/widgets/dashboard/new-orders/widget.client.tsx +295 -0
- package/src/modules/sales/widgets/dashboard/new-orders/widget.ts +33 -0
- package/src/modules/sales/widgets/dashboard/new-quotes/__tests__/config.test.ts +28 -0
- package/src/modules/sales/widgets/dashboard/new-quotes/config.ts +49 -0
- package/src/modules/sales/widgets/dashboard/new-quotes/widget.client.tsx +322 -0
- package/src/modules/sales/widgets/dashboard/new-quotes/widget.ts +33 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import Link from 'next/link'
|
|
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 { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_SETTINGS,
|
|
12
|
+
hydrateSalesNewQuotesSettings,
|
|
13
|
+
type SalesNewQuotesSettings,
|
|
14
|
+
} from './config'
|
|
15
|
+
import type { DatePeriodOption } from '../../../lib/dateRange'
|
|
16
|
+
|
|
17
|
+
type QuoteItem = {
|
|
18
|
+
id: string
|
|
19
|
+
quoteNumber: string
|
|
20
|
+
status: string | null
|
|
21
|
+
customerName: string | null
|
|
22
|
+
customerEntityId: string | null
|
|
23
|
+
validFrom: string | null
|
|
24
|
+
validUntil: string | null
|
|
25
|
+
netAmount: string
|
|
26
|
+
grossAmount: string
|
|
27
|
+
currency: string | null
|
|
28
|
+
createdAt: string
|
|
29
|
+
convertedOrderId: string | null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function formatCurrency(value: string | null, currency: string | null, locale?: string): string {
|
|
33
|
+
const amount = typeof value === 'string' ? Number(value) : Number.NaN
|
|
34
|
+
if (!Number.isFinite(amount)) return '—'
|
|
35
|
+
const code = currency || 'USD'
|
|
36
|
+
try {
|
|
37
|
+
return new Intl.NumberFormat(locale ?? undefined, { style: 'currency', currency: code }).format(amount)
|
|
38
|
+
} catch {
|
|
39
|
+
return `${amount.toFixed(2)} ${code}`
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function formatRelativeDate(value: string, locale?: string): string {
|
|
44
|
+
const date = new Date(value)
|
|
45
|
+
if (Number.isNaN(date.getTime())) return ''
|
|
46
|
+
const now = new Date()
|
|
47
|
+
const diffMs = date.getTime() - now.getTime()
|
|
48
|
+
const absMs = Math.abs(diffMs)
|
|
49
|
+
const rtf = new Intl.RelativeTimeFormat(locale ?? undefined, { numeric: 'auto' })
|
|
50
|
+
if (absMs < 60 * 1000) {
|
|
51
|
+
return rtf.format(Math.round(diffMs / 1000), 'second')
|
|
52
|
+
}
|
|
53
|
+
if (absMs < 60 * 60 * 1000) {
|
|
54
|
+
return rtf.format(Math.round(diffMs / (60 * 1000)), 'minute')
|
|
55
|
+
}
|
|
56
|
+
if (absMs < 24 * 60 * 60 * 1000) {
|
|
57
|
+
return rtf.format(Math.round(diffMs / (60 * 60 * 1000)), 'hour')
|
|
58
|
+
}
|
|
59
|
+
return rtf.format(Math.round(diffMs / (24 * 60 * 60 * 1000)), 'day')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function formatDate(value: string | null, locale?: string): string {
|
|
63
|
+
if (!value) return ''
|
|
64
|
+
const date = new Date(value)
|
|
65
|
+
if (Number.isNaN(date.getTime())) return ''
|
|
66
|
+
return date.toLocaleDateString(locale ?? undefined, { dateStyle: 'medium' })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function loadQuotes(settings: SalesNewQuotesSettings): Promise<QuoteItem[]> {
|
|
70
|
+
const params = new URLSearchParams({
|
|
71
|
+
limit: String(settings.pageSize),
|
|
72
|
+
datePeriod: settings.datePeriod,
|
|
73
|
+
})
|
|
74
|
+
if (settings.datePeriod === 'custom') {
|
|
75
|
+
if (settings.customFrom) params.set('customFrom', settings.customFrom)
|
|
76
|
+
if (settings.customTo) params.set('customTo', settings.customTo)
|
|
77
|
+
}
|
|
78
|
+
const call = await apiCall<{ items?: unknown[]; error?: string }>(
|
|
79
|
+
`/api/sales/dashboard/widgets/new-quotes?${params.toString()}`,
|
|
80
|
+
)
|
|
81
|
+
if (!call.ok) {
|
|
82
|
+
const message =
|
|
83
|
+
typeof (call.result as Record<string, unknown> | null)?.error === 'string'
|
|
84
|
+
? ((call.result as Record<string, unknown>).error as string)
|
|
85
|
+
: `Request failed with status ${call.status}`
|
|
86
|
+
throw new Error(message)
|
|
87
|
+
}
|
|
88
|
+
const payload = call.result ?? {}
|
|
89
|
+
const rawItems = Array.isArray((payload as { items?: unknown[] }).items)
|
|
90
|
+
? (payload as { items: unknown[] }).items
|
|
91
|
+
: []
|
|
92
|
+
return rawItems
|
|
93
|
+
.map((item: unknown): QuoteItem | null => {
|
|
94
|
+
if (!item || typeof item !== 'object') return null
|
|
95
|
+
const data = item as Record<string, unknown>
|
|
96
|
+
if (typeof data.id !== 'string' || typeof data.createdAt !== 'string') return null
|
|
97
|
+
const netAmount = data.netAmount
|
|
98
|
+
const grossAmount = data.grossAmount
|
|
99
|
+
return {
|
|
100
|
+
id: data.id,
|
|
101
|
+
quoteNumber: typeof data.quoteNumber === 'string' ? data.quoteNumber : '',
|
|
102
|
+
status: typeof data.status === 'string' ? data.status : null,
|
|
103
|
+
customerName: typeof data.customerName === 'string' ? data.customerName : null,
|
|
104
|
+
customerEntityId: typeof data.customerEntityId === 'string' ? data.customerEntityId : null,
|
|
105
|
+
validFrom: typeof data.validFrom === 'string' ? data.validFrom : null,
|
|
106
|
+
validUntil: typeof data.validUntil === 'string' ? data.validUntil : null,
|
|
107
|
+
netAmount: typeof netAmount === 'string' ? netAmount : typeof netAmount === 'number' ? String(netAmount) : '0',
|
|
108
|
+
grossAmount:
|
|
109
|
+
typeof grossAmount === 'string' ? grossAmount : typeof grossAmount === 'number' ? String(grossAmount) : '0',
|
|
110
|
+
currency: typeof data.currency === 'string' ? data.currency : null,
|
|
111
|
+
createdAt: data.createdAt,
|
|
112
|
+
convertedOrderId: typeof data.convertedOrderId === 'string' ? data.convertedOrderId : null,
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
.filter((item: QuoteItem | null): item is QuoteItem => !!item)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const SalesNewQuotesWidget: React.FC<DashboardWidgetComponentProps<SalesNewQuotesSettings>> = ({
|
|
119
|
+
mode,
|
|
120
|
+
settings = DEFAULT_SETTINGS,
|
|
121
|
+
onSettingsChange,
|
|
122
|
+
refreshToken,
|
|
123
|
+
onRefreshStateChange,
|
|
124
|
+
}) => {
|
|
125
|
+
const t = useT()
|
|
126
|
+
const hydrated = React.useMemo(() => hydrateSalesNewQuotesSettings(settings), [settings])
|
|
127
|
+
const [items, setItems] = React.useState<QuoteItem[]>([])
|
|
128
|
+
const [loading, setLoading] = React.useState(true)
|
|
129
|
+
const [error, setError] = React.useState<string | null>(null)
|
|
130
|
+
const [locale, setLocale] = React.useState<string | undefined>(undefined)
|
|
131
|
+
|
|
132
|
+
React.useEffect(() => {
|
|
133
|
+
if (typeof navigator !== 'undefined') {
|
|
134
|
+
setLocale(navigator.language)
|
|
135
|
+
}
|
|
136
|
+
}, [])
|
|
137
|
+
|
|
138
|
+
const refresh = React.useCallback(async () => {
|
|
139
|
+
onRefreshStateChange?.(true)
|
|
140
|
+
setLoading(true)
|
|
141
|
+
setError(null)
|
|
142
|
+
try {
|
|
143
|
+
const data = await loadQuotes(hydrated)
|
|
144
|
+
setItems(data)
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.error('Failed to load new quotes widget data', err)
|
|
147
|
+
setError(t('sales.widgets.newQuotes.error', 'Failed to load quotes'))
|
|
148
|
+
} finally {
|
|
149
|
+
setLoading(false)
|
|
150
|
+
onRefreshStateChange?.(false)
|
|
151
|
+
}
|
|
152
|
+
}, [hydrated, onRefreshStateChange, t])
|
|
153
|
+
|
|
154
|
+
React.useEffect(() => {
|
|
155
|
+
refresh().catch(() => {})
|
|
156
|
+
}, [refresh, refreshToken])
|
|
157
|
+
|
|
158
|
+
if (mode === 'settings') {
|
|
159
|
+
return (
|
|
160
|
+
<div className="space-y-4 text-sm">
|
|
161
|
+
<div className="space-y-1.5">
|
|
162
|
+
<label
|
|
163
|
+
htmlFor="sales-new-quotes-page-size"
|
|
164
|
+
className="text-xs font-semibold uppercase text-muted-foreground"
|
|
165
|
+
>
|
|
166
|
+
{t('sales.widgets.newQuotes.settings.pageSize', 'Number of Quotes')}
|
|
167
|
+
</label>
|
|
168
|
+
<input
|
|
169
|
+
id="sales-new-quotes-page-size"
|
|
170
|
+
type="number"
|
|
171
|
+
min={1}
|
|
172
|
+
max={20}
|
|
173
|
+
className="w-24 rounded-md border px-2 py-1 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
|
174
|
+
value={hydrated.pageSize}
|
|
175
|
+
onChange={(event) => {
|
|
176
|
+
const next = Number(event.target.value)
|
|
177
|
+
const value = Number.isFinite(next)
|
|
178
|
+
? Math.min(20, Math.max(1, Math.floor(next)))
|
|
179
|
+
: hydrated.pageSize
|
|
180
|
+
onSettingsChange({ ...hydrated, pageSize: value })
|
|
181
|
+
}}
|
|
182
|
+
/>
|
|
183
|
+
</div>
|
|
184
|
+
<div className="space-y-1.5">
|
|
185
|
+
<label
|
|
186
|
+
htmlFor="sales-new-quotes-date-period"
|
|
187
|
+
className="text-xs font-semibold uppercase text-muted-foreground"
|
|
188
|
+
>
|
|
189
|
+
{t('sales.widgets.newQuotes.settings.datePeriod', 'Date Period')}
|
|
190
|
+
</label>
|
|
191
|
+
<select
|
|
192
|
+
id="sales-new-quotes-date-period"
|
|
193
|
+
className="w-full rounded-md border px-2 py-1 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
|
194
|
+
value={hydrated.datePeriod}
|
|
195
|
+
onChange={(event) => {
|
|
196
|
+
onSettingsChange({ ...hydrated, datePeriod: event.target.value as DatePeriodOption })
|
|
197
|
+
}}
|
|
198
|
+
>
|
|
199
|
+
<option value="last24h">{t('sales.widgets.newQuotes.settings.last24h', 'Last 24 hours')}</option>
|
|
200
|
+
<option value="last7d">{t('sales.widgets.newQuotes.settings.last7d', 'Last 7 days')}</option>
|
|
201
|
+
<option value="last30d">{t('sales.widgets.newQuotes.settings.last30d', 'Last 30 days')}</option>
|
|
202
|
+
<option value="custom">{t('sales.widgets.newQuotes.settings.custom', 'Custom range')}</option>
|
|
203
|
+
</select>
|
|
204
|
+
</div>
|
|
205
|
+
{hydrated.datePeriod === 'custom' ? (
|
|
206
|
+
<>
|
|
207
|
+
<div className="space-y-1.5">
|
|
208
|
+
<label
|
|
209
|
+
htmlFor="sales-new-quotes-custom-from"
|
|
210
|
+
className="text-xs font-semibold uppercase text-muted-foreground"
|
|
211
|
+
>
|
|
212
|
+
{t('sales.widgets.newQuotes.settings.customFrom', 'From')}
|
|
213
|
+
</label>
|
|
214
|
+
<input
|
|
215
|
+
id="sales-new-quotes-custom-from"
|
|
216
|
+
type="date"
|
|
217
|
+
className="w-full rounded-md border px-2 py-1 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
|
218
|
+
value={hydrated.customFrom ?? ''}
|
|
219
|
+
onChange={(event) => {
|
|
220
|
+
onSettingsChange({ ...hydrated, customFrom: event.target.value })
|
|
221
|
+
}}
|
|
222
|
+
/>
|
|
223
|
+
</div>
|
|
224
|
+
<div className="space-y-1.5">
|
|
225
|
+
<label
|
|
226
|
+
htmlFor="sales-new-quotes-custom-to"
|
|
227
|
+
className="text-xs font-semibold uppercase text-muted-foreground"
|
|
228
|
+
>
|
|
229
|
+
{t('sales.widgets.newQuotes.settings.customTo', 'To')}
|
|
230
|
+
</label>
|
|
231
|
+
<input
|
|
232
|
+
id="sales-new-quotes-custom-to"
|
|
233
|
+
type="date"
|
|
234
|
+
className="w-full rounded-md border px-2 py-1 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
|
235
|
+
value={hydrated.customTo ?? ''}
|
|
236
|
+
onChange={(event) => {
|
|
237
|
+
onSettingsChange({ ...hydrated, customTo: event.target.value })
|
|
238
|
+
}}
|
|
239
|
+
/>
|
|
240
|
+
</div>
|
|
241
|
+
</>
|
|
242
|
+
) : null}
|
|
243
|
+
</div>
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (error) {
|
|
248
|
+
return <p className="text-sm text-destructive">{error}</p>
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (loading) {
|
|
252
|
+
return (
|
|
253
|
+
<div className="flex h-32 items-center justify-center">
|
|
254
|
+
<Spinner className="h-6 w-6 text-muted-foreground" />
|
|
255
|
+
</div>
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (items.length === 0) {
|
|
260
|
+
return (
|
|
261
|
+
<p className="text-sm text-muted-foreground">
|
|
262
|
+
{t('sales.widgets.newQuotes.empty', 'No quotes found in this period')}
|
|
263
|
+
</p>
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<ul className="space-y-3">
|
|
269
|
+
{items.map((quote) => {
|
|
270
|
+
const createdLabel = formatRelativeDate(quote.createdAt, locale)
|
|
271
|
+
const validUntilLabel = formatDate(quote.validUntil, locale)
|
|
272
|
+
const isExpired = quote.validUntil ? new Date(quote.validUntil) < new Date() : false
|
|
273
|
+
return (
|
|
274
|
+
<li key={quote.id} className="rounded-md border p-3">
|
|
275
|
+
<div className="flex items-start justify-between gap-3">
|
|
276
|
+
<div className="space-y-1">
|
|
277
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
278
|
+
<Link
|
|
279
|
+
className="text-sm font-medium text-foreground hover:underline"
|
|
280
|
+
href={`/backend/sales/quotes/${encodeURIComponent(quote.id)}`}
|
|
281
|
+
>
|
|
282
|
+
{quote.quoteNumber}
|
|
283
|
+
</Link>
|
|
284
|
+
{quote.status ? (
|
|
285
|
+
<Badge variant="outline" className="text-[11px]">
|
|
286
|
+
{quote.status}
|
|
287
|
+
</Badge>
|
|
288
|
+
) : null}
|
|
289
|
+
{quote.convertedOrderId ? (
|
|
290
|
+
<Badge variant="secondary" className="text-[11px]">
|
|
291
|
+
{t('sales.widgets.newQuotes.converted', 'Converted')}
|
|
292
|
+
</Badge>
|
|
293
|
+
) : null}
|
|
294
|
+
</div>
|
|
295
|
+
<p className="text-xs text-muted-foreground">
|
|
296
|
+
{quote.customerName ?? t('sales.widgets.newQuotes.noCustomer', 'No customer')}
|
|
297
|
+
</p>
|
|
298
|
+
{validUntilLabel ? (
|
|
299
|
+
<p
|
|
300
|
+
className={`text-xs ${isExpired ? 'text-muted-foreground line-through' : 'text-muted-foreground'}`}
|
|
301
|
+
>
|
|
302
|
+
{t('sales.widgets.newQuotes.validUntil', 'Valid until {{date}}', { date: validUntilLabel })}
|
|
303
|
+
</p>
|
|
304
|
+
) : null}
|
|
305
|
+
<p className="text-xs text-muted-foreground">
|
|
306
|
+
{createdLabel || t('sales.widgets.newQuotes.unknownDate', 'Unknown date')}
|
|
307
|
+
</p>
|
|
308
|
+
</div>
|
|
309
|
+
<div className="text-right">
|
|
310
|
+
<p className="text-sm font-semibold">
|
|
311
|
+
{formatCurrency(quote.grossAmount, quote.currency, locale)}
|
|
312
|
+
</p>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
</li>
|
|
316
|
+
)
|
|
317
|
+
})}
|
|
318
|
+
</ul>
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export default SalesNewQuotesWidget
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { DashboardWidgetModule } from '@open-mercato/shared/modules/dashboard/widgets'
|
|
2
|
+
import SalesNewQuotesWidget from './widget.client'
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_SETTINGS,
|
|
5
|
+
hydrateSalesNewQuotesSettings,
|
|
6
|
+
type SalesNewQuotesSettings,
|
|
7
|
+
} from './config'
|
|
8
|
+
|
|
9
|
+
const widget: DashboardWidgetModule<SalesNewQuotesSettings> = {
|
|
10
|
+
metadata: {
|
|
11
|
+
id: 'sales.dashboard.newQuotes',
|
|
12
|
+
title: 'New Quotes',
|
|
13
|
+
description: 'Displays recently created sales quotes.',
|
|
14
|
+
features: ['dashboards.view', 'sales.widgets.new-quotes'],
|
|
15
|
+
defaultSize: 'md',
|
|
16
|
+
defaultEnabled: true,
|
|
17
|
+
defaultSettings: DEFAULT_SETTINGS,
|
|
18
|
+
tags: ['sales', 'quotes'],
|
|
19
|
+
category: 'sales',
|
|
20
|
+
icon: 'lucide:file-text',
|
|
21
|
+
supportsRefresh: true,
|
|
22
|
+
},
|
|
23
|
+
Widget: SalesNewQuotesWidget,
|
|
24
|
+
hydrateSettings: hydrateSalesNewQuotesSettings,
|
|
25
|
+
dehydrateSettings: (settings) => ({
|
|
26
|
+
pageSize: settings.pageSize,
|
|
27
|
+
datePeriod: settings.datePeriod,
|
|
28
|
+
customFrom: settings.customFrom,
|
|
29
|
+
customTo: settings.customTo,
|
|
30
|
+
}),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default widget
|