@open-mercato/core 0.4.2-canary-cae9dafa24 → 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.
Files changed (54) hide show
  1. package/dist/modules/api_docs/backend/docs/page.js +1 -4
  2. package/dist/modules/api_docs/backend/docs/page.js.map +2 -2
  3. package/dist/modules/auth/lib/setup-app.js +0 -4
  4. package/dist/modules/auth/lib/setup-app.js.map +2 -2
  5. package/dist/modules/sales/acl.js +1 -3
  6. package/dist/modules/sales/acl.js.map +2 -2
  7. package/package.json +2 -2
  8. package/src/modules/api_docs/backend/docs/page.tsx +1 -2
  9. package/src/modules/auth/lib/setup-app.ts +0 -4
  10. package/src/modules/customers/README.md +1 -2
  11. package/src/modules/entities/README.md +1 -1
  12. package/src/modules/sales/acl.ts +0 -2
  13. package/src/modules/sales/i18n/de.json +1 -32
  14. package/src/modules/sales/i18n/en.json +1 -32
  15. package/src/modules/sales/i18n/es.json +1 -32
  16. package/src/modules/sales/i18n/pl.json +1 -32
  17. package/dist/modules/sales/api/dashboard/widgets/new-orders/route.js +0 -163
  18. package/dist/modules/sales/api/dashboard/widgets/new-orders/route.js.map +0 -7
  19. package/dist/modules/sales/api/dashboard/widgets/new-quotes/route.js +0 -165
  20. package/dist/modules/sales/api/dashboard/widgets/new-quotes/route.js.map +0 -7
  21. package/dist/modules/sales/api/dashboard/widgets/utils.js +0 -38
  22. package/dist/modules/sales/api/dashboard/widgets/utils.js.map +0 -7
  23. package/dist/modules/sales/lib/customerSnapshot.js +0 -21
  24. package/dist/modules/sales/lib/customerSnapshot.js.map +0 -7
  25. package/dist/modules/sales/lib/dateRange.js +0 -39
  26. package/dist/modules/sales/lib/dateRange.js.map +0 -7
  27. package/dist/modules/sales/widgets/dashboard/new-orders/config.js +0 -32
  28. package/dist/modules/sales/widgets/dashboard/new-orders/config.js.map +0 -7
  29. package/dist/modules/sales/widgets/dashboard/new-orders/widget.client.js +0 -252
  30. package/dist/modules/sales/widgets/dashboard/new-orders/widget.client.js.map +0 -7
  31. package/dist/modules/sales/widgets/dashboard/new-orders/widget.js +0 -33
  32. package/dist/modules/sales/widgets/dashboard/new-orders/widget.js.map +0 -7
  33. package/dist/modules/sales/widgets/dashboard/new-quotes/config.js +0 -32
  34. package/dist/modules/sales/widgets/dashboard/new-quotes/config.js.map +0 -7
  35. package/dist/modules/sales/widgets/dashboard/new-quotes/widget.client.js +0 -272
  36. package/dist/modules/sales/widgets/dashboard/new-quotes/widget.client.js.map +0 -7
  37. package/dist/modules/sales/widgets/dashboard/new-quotes/widget.js +0 -33
  38. package/dist/modules/sales/widgets/dashboard/new-quotes/widget.js.map +0 -7
  39. package/src/modules/sales/api/dashboard/widgets/new-orders/__tests__/route.test.ts +0 -60
  40. package/src/modules/sales/api/dashboard/widgets/new-orders/route.ts +0 -192
  41. package/src/modules/sales/api/dashboard/widgets/new-quotes/__tests__/route.test.ts +0 -61
  42. package/src/modules/sales/api/dashboard/widgets/new-quotes/route.ts +0 -194
  43. package/src/modules/sales/api/dashboard/widgets/utils.ts +0 -53
  44. package/src/modules/sales/lib/__tests__/dateRange.test.ts +0 -26
  45. package/src/modules/sales/lib/customerSnapshot.ts +0 -17
  46. package/src/modules/sales/lib/dateRange.ts +0 -42
  47. package/src/modules/sales/widgets/dashboard/new-orders/__tests__/config.test.ts +0 -28
  48. package/src/modules/sales/widgets/dashboard/new-orders/config.ts +0 -48
  49. package/src/modules/sales/widgets/dashboard/new-orders/widget.client.tsx +0 -295
  50. package/src/modules/sales/widgets/dashboard/new-orders/widget.ts +0 -33
  51. package/src/modules/sales/widgets/dashboard/new-quotes/__tests__/config.test.ts +0 -28
  52. package/src/modules/sales/widgets/dashboard/new-quotes/config.ts +0 -48
  53. package/src/modules/sales/widgets/dashboard/new-quotes/widget.client.tsx +0 -322
  54. package/src/modules/sales/widgets/dashboard/new-quotes/widget.ts +0 -33
@@ -1,272 +0,0 @@
1
- "use client";
2
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import * as React from "react";
4
- import Link from "next/link";
5
- import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
6
- import { Spinner } from "@open-mercato/ui/primitives/spinner";
7
- import { Badge } from "@open-mercato/ui/primitives/badge";
8
- import { useT } from "@open-mercato/shared/lib/i18n/context";
9
- import {
10
- DEFAULT_SETTINGS,
11
- hydrateSalesNewQuotesSettings
12
- } from "./config.js";
13
- function formatCurrency(value, currency, locale) {
14
- const amount = typeof value === "string" ? Number(value) : Number.NaN;
15
- if (!Number.isFinite(amount)) return "\u2014";
16
- const code = currency || "USD";
17
- try {
18
- return new Intl.NumberFormat(locale ?? void 0, { style: "currency", currency: code }).format(amount);
19
- } catch {
20
- return `${amount.toFixed(2)} ${code}`;
21
- }
22
- }
23
- function formatRelativeDate(value, locale) {
24
- const date = new Date(value);
25
- if (Number.isNaN(date.getTime())) return "";
26
- const now = /* @__PURE__ */ new Date();
27
- const diffMs = date.getTime() - now.getTime();
28
- const absMs = Math.abs(diffMs);
29
- const rtf = new Intl.RelativeTimeFormat(locale ?? void 0, { numeric: "auto" });
30
- if (absMs < 60 * 1e3) {
31
- return rtf.format(Math.round(diffMs / 1e3), "second");
32
- }
33
- if (absMs < 60 * 60 * 1e3) {
34
- return rtf.format(Math.round(diffMs / (60 * 1e3)), "minute");
35
- }
36
- if (absMs < 24 * 60 * 60 * 1e3) {
37
- return rtf.format(Math.round(diffMs / (60 * 60 * 1e3)), "hour");
38
- }
39
- return rtf.format(Math.round(diffMs / (24 * 60 * 60 * 1e3)), "day");
40
- }
41
- function formatDate(value, locale) {
42
- if (!value) return "";
43
- const date = new Date(value);
44
- if (Number.isNaN(date.getTime())) return "";
45
- return date.toLocaleDateString(locale ?? void 0, { dateStyle: "medium" });
46
- }
47
- async function loadQuotes(settings) {
48
- const params = new URLSearchParams({
49
- limit: String(settings.pageSize),
50
- datePeriod: settings.datePeriod
51
- });
52
- if (settings.datePeriod === "custom") {
53
- if (settings.customFrom) params.set("customFrom", settings.customFrom);
54
- if (settings.customTo) params.set("customTo", settings.customTo);
55
- }
56
- const call = await apiCall(
57
- `/api/sales/dashboard/widgets/new-quotes?${params.toString()}`
58
- );
59
- if (!call.ok) {
60
- const message = typeof call.result?.error === "string" ? call.result.error : `Request failed with status ${call.status}`;
61
- throw new Error(message);
62
- }
63
- const payload = call.result ?? {};
64
- const rawItems = Array.isArray(payload.items) ? payload.items : [];
65
- return rawItems.map((item) => {
66
- if (!item || typeof item !== "object") return null;
67
- const data = item;
68
- if (typeof data.id !== "string" || typeof data.createdAt !== "string") return null;
69
- const netAmount = data.netAmount;
70
- const grossAmount = data.grossAmount;
71
- return {
72
- id: data.id,
73
- quoteNumber: typeof data.quoteNumber === "string" ? data.quoteNumber : "",
74
- status: typeof data.status === "string" ? data.status : null,
75
- customerName: typeof data.customerName === "string" ? data.customerName : null,
76
- customerEntityId: typeof data.customerEntityId === "string" ? data.customerEntityId : null,
77
- validFrom: typeof data.validFrom === "string" ? data.validFrom : null,
78
- validUntil: typeof data.validUntil === "string" ? data.validUntil : null,
79
- netAmount: typeof netAmount === "string" ? netAmount : typeof netAmount === "number" ? String(netAmount) : "0",
80
- grossAmount: typeof grossAmount === "string" ? grossAmount : typeof grossAmount === "number" ? String(grossAmount) : "0",
81
- currency: typeof data.currency === "string" ? data.currency : null,
82
- createdAt: data.createdAt,
83
- convertedOrderId: typeof data.convertedOrderId === "string" ? data.convertedOrderId : null
84
- };
85
- }).filter((item) => !!item);
86
- }
87
- const SalesNewQuotesWidget = ({
88
- mode,
89
- settings = DEFAULT_SETTINGS,
90
- onSettingsChange,
91
- refreshToken,
92
- onRefreshStateChange
93
- }) => {
94
- const t = useT();
95
- const hydrated = React.useMemo(() => hydrateSalesNewQuotesSettings(settings), [settings]);
96
- const [items, setItems] = React.useState([]);
97
- const [loading, setLoading] = React.useState(true);
98
- const [error, setError] = React.useState(null);
99
- const [locale, setLocale] = React.useState(void 0);
100
- React.useEffect(() => {
101
- if (typeof navigator !== "undefined") {
102
- setLocale(navigator.language);
103
- }
104
- }, []);
105
- const refresh = React.useCallback(async () => {
106
- onRefreshStateChange?.(true);
107
- setLoading(true);
108
- setError(null);
109
- try {
110
- const data = await loadQuotes(hydrated);
111
- setItems(data);
112
- } catch (err) {
113
- console.error("Failed to load new quotes widget data", err);
114
- setError(t("sales.widgets.newQuotes.error", "Failed to load quotes"));
115
- } finally {
116
- setLoading(false);
117
- onRefreshStateChange?.(false);
118
- }
119
- }, [hydrated, onRefreshStateChange, t]);
120
- React.useEffect(() => {
121
- refresh().catch(() => {
122
- });
123
- }, [refresh, refreshToken]);
124
- if (mode === "settings") {
125
- return /* @__PURE__ */ jsxs("div", { className: "space-y-4 text-sm", children: [
126
- /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
127
- /* @__PURE__ */ jsx(
128
- "label",
129
- {
130
- htmlFor: "sales-new-quotes-page-size",
131
- className: "text-xs font-semibold uppercase text-muted-foreground",
132
- children: t("sales.widgets.newQuotes.settings.pageSize", "Number of Quotes")
133
- }
134
- ),
135
- /* @__PURE__ */ jsx(
136
- "input",
137
- {
138
- id: "sales-new-quotes-page-size",
139
- type: "number",
140
- min: 1,
141
- max: 20,
142
- className: "w-24 rounded-md border px-2 py-1 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary",
143
- value: hydrated.pageSize,
144
- onChange: (event) => {
145
- const next = Number(event.target.value);
146
- const value = Number.isFinite(next) ? Math.min(20, Math.max(1, Math.floor(next))) : hydrated.pageSize;
147
- onSettingsChange({ ...hydrated, pageSize: value });
148
- }
149
- }
150
- )
151
- ] }),
152
- /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
153
- /* @__PURE__ */ jsx(
154
- "label",
155
- {
156
- htmlFor: "sales-new-quotes-date-period",
157
- className: "text-xs font-semibold uppercase text-muted-foreground",
158
- children: t("sales.widgets.newQuotes.settings.datePeriod", "Date Period")
159
- }
160
- ),
161
- /* @__PURE__ */ jsxs(
162
- "select",
163
- {
164
- id: "sales-new-quotes-date-period",
165
- className: "w-full rounded-md border px-2 py-1 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary",
166
- value: hydrated.datePeriod,
167
- onChange: (event) => {
168
- onSettingsChange({ ...hydrated, datePeriod: event.target.value });
169
- },
170
- children: [
171
- /* @__PURE__ */ jsx("option", { value: "last24h", children: t("sales.widgets.newQuotes.settings.last24h", "Last 24 hours") }),
172
- /* @__PURE__ */ jsx("option", { value: "last7d", children: t("sales.widgets.newQuotes.settings.last7d", "Last 7 days") }),
173
- /* @__PURE__ */ jsx("option", { value: "last30d", children: t("sales.widgets.newQuotes.settings.last30d", "Last 30 days") }),
174
- /* @__PURE__ */ jsx("option", { value: "custom", children: t("sales.widgets.newQuotes.settings.custom", "Custom range") })
175
- ]
176
- }
177
- )
178
- ] }),
179
- hydrated.datePeriod === "custom" ? /* @__PURE__ */ jsxs(Fragment, { children: [
180
- /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
181
- /* @__PURE__ */ jsx(
182
- "label",
183
- {
184
- htmlFor: "sales-new-quotes-custom-from",
185
- className: "text-xs font-semibold uppercase text-muted-foreground",
186
- children: t("sales.widgets.newQuotes.settings.customFrom", "From")
187
- }
188
- ),
189
- /* @__PURE__ */ jsx(
190
- "input",
191
- {
192
- id: "sales-new-quotes-custom-from",
193
- type: "date",
194
- className: "w-full rounded-md border px-2 py-1 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary",
195
- value: hydrated.customFrom ?? "",
196
- onChange: (event) => {
197
- onSettingsChange({ ...hydrated, customFrom: event.target.value });
198
- }
199
- }
200
- )
201
- ] }),
202
- /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
203
- /* @__PURE__ */ jsx(
204
- "label",
205
- {
206
- htmlFor: "sales-new-quotes-custom-to",
207
- className: "text-xs font-semibold uppercase text-muted-foreground",
208
- children: t("sales.widgets.newQuotes.settings.customTo", "To")
209
- }
210
- ),
211
- /* @__PURE__ */ jsx(
212
- "input",
213
- {
214
- id: "sales-new-quotes-custom-to",
215
- type: "date",
216
- className: "w-full rounded-md border px-2 py-1 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary",
217
- value: hydrated.customTo ?? "",
218
- onChange: (event) => {
219
- onSettingsChange({ ...hydrated, customTo: event.target.value });
220
- }
221
- }
222
- )
223
- ] })
224
- ] }) : null
225
- ] });
226
- }
227
- if (error) {
228
- return /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", children: error });
229
- }
230
- if (loading) {
231
- return /* @__PURE__ */ jsx("div", { className: "flex h-32 items-center justify-center", children: /* @__PURE__ */ jsx(Spinner, { className: "h-6 w-6 text-muted-foreground" }) });
232
- }
233
- if (items.length === 0) {
234
- return /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("sales.widgets.newQuotes.empty", "No quotes found in this period") });
235
- }
236
- return /* @__PURE__ */ jsx("ul", { className: "space-y-3", children: items.map((quote) => {
237
- const createdLabel = formatRelativeDate(quote.createdAt, locale);
238
- const validUntilLabel = formatDate(quote.validUntil, locale);
239
- const isExpired = quote.validUntil ? new Date(quote.validUntil) < /* @__PURE__ */ new Date() : false;
240
- return /* @__PURE__ */ jsx("li", { className: "rounded-md border p-3", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3", children: [
241
- /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
242
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
243
- /* @__PURE__ */ jsx(
244
- Link,
245
- {
246
- className: "text-sm font-medium text-foreground hover:underline",
247
- href: `/backend/sales/quotes/${encodeURIComponent(quote.id)}`,
248
- children: quote.quoteNumber
249
- }
250
- ),
251
- quote.status ? /* @__PURE__ */ jsx(Badge, { variant: "outline", className: "text-[11px]", children: quote.status }) : null,
252
- quote.convertedOrderId ? /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "text-[11px]", children: t("sales.widgets.newQuotes.converted", "Converted") }) : null
253
- ] }),
254
- /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: quote.customerName ?? t("sales.widgets.newQuotes.noCustomer", "No customer") }),
255
- validUntilLabel ? /* @__PURE__ */ jsx(
256
- "p",
257
- {
258
- className: `text-xs ${isExpired ? "text-muted-foreground line-through" : "text-muted-foreground"}`,
259
- children: t("sales.widgets.newQuotes.validUntil", "Valid until {{date}}", { date: validUntilLabel })
260
- }
261
- ) : null,
262
- /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: createdLabel || t("sales.widgets.newQuotes.unknownDate", "Unknown date") })
263
- ] }),
264
- /* @__PURE__ */ jsx("div", { className: "text-right", children: /* @__PURE__ */ jsx("p", { className: "text-sm font-semibold", children: formatCurrency(quote.grossAmount, quote.currency, locale) }) })
265
- ] }) }, quote.id);
266
- }) });
267
- };
268
- var widget_client_default = SalesNewQuotesWidget;
269
- export {
270
- widget_client_default as default
271
- };
272
- //# sourceMappingURL=widget.client.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../../../../src/modules/sales/widgets/dashboard/new-quotes/widget.client.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n DEFAULT_SETTINGS,\n hydrateSalesNewQuotesSettings,\n type SalesNewQuotesSettings,\n} from './config'\nimport type { DatePeriodOption } from '../../../lib/dateRange'\n\ntype QuoteItem = {\n id: string\n quoteNumber: string\n status: string | null\n customerName: string | null\n customerEntityId: string | null\n validFrom: string | null\n validUntil: string | null\n netAmount: string\n grossAmount: string\n currency: string | null\n createdAt: string\n convertedOrderId: string | null\n}\n\nfunction formatCurrency(value: string | null, currency: string | null, locale?: string): string {\n const amount = typeof value === 'string' ? Number(value) : Number.NaN\n if (!Number.isFinite(amount)) return '\u2014'\n const code = currency || 'USD'\n try {\n return new Intl.NumberFormat(locale ?? undefined, { style: 'currency', currency: code }).format(amount)\n } catch {\n return `${amount.toFixed(2)} ${code}`\n }\n}\n\nfunction formatRelativeDate(value: string, locale?: string): string {\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return ''\n const now = new Date()\n const diffMs = date.getTime() - now.getTime()\n const absMs = Math.abs(diffMs)\n const rtf = new Intl.RelativeTimeFormat(locale ?? undefined, { numeric: 'auto' })\n if (absMs < 60 * 1000) {\n return rtf.format(Math.round(diffMs / 1000), 'second')\n }\n if (absMs < 60 * 60 * 1000) {\n return rtf.format(Math.round(diffMs / (60 * 1000)), 'minute')\n }\n if (absMs < 24 * 60 * 60 * 1000) {\n return rtf.format(Math.round(diffMs / (60 * 60 * 1000)), 'hour')\n }\n return rtf.format(Math.round(diffMs / (24 * 60 * 60 * 1000)), 'day')\n}\n\nfunction formatDate(value: string | null, locale?: string): string {\n if (!value) return ''\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return ''\n return date.toLocaleDateString(locale ?? undefined, { dateStyle: 'medium' })\n}\n\nasync function loadQuotes(settings: SalesNewQuotesSettings): Promise<QuoteItem[]> {\n const params = new URLSearchParams({\n limit: String(settings.pageSize),\n datePeriod: settings.datePeriod,\n })\n if (settings.datePeriod === 'custom') {\n if (settings.customFrom) params.set('customFrom', settings.customFrom)\n if (settings.customTo) params.set('customTo', settings.customTo)\n }\n const call = await apiCall<{ items?: unknown[]; error?: string }>(\n `/api/sales/dashboard/widgets/new-quotes?${params.toString()}`,\n )\n if (!call.ok) {\n const message =\n typeof (call.result as Record<string, unknown> | null)?.error === 'string'\n ? ((call.result as Record<string, unknown>).error as string)\n : `Request failed with status ${call.status}`\n throw new Error(message)\n }\n const payload = call.result ?? {}\n const rawItems = Array.isArray((payload as { items?: unknown[] }).items)\n ? (payload as { items: unknown[] }).items\n : []\n return rawItems\n .map((item: unknown): QuoteItem | null => {\n if (!item || typeof item !== 'object') return null\n const data = item as Record<string, unknown>\n if (typeof data.id !== 'string' || typeof data.createdAt !== 'string') return null\n const netAmount = data.netAmount\n const grossAmount = data.grossAmount\n return {\n id: data.id,\n quoteNumber: typeof data.quoteNumber === 'string' ? data.quoteNumber : '',\n status: typeof data.status === 'string' ? data.status : null,\n customerName: typeof data.customerName === 'string' ? data.customerName : null,\n customerEntityId: typeof data.customerEntityId === 'string' ? data.customerEntityId : null,\n validFrom: typeof data.validFrom === 'string' ? data.validFrom : null,\n validUntil: typeof data.validUntil === 'string' ? data.validUntil : null,\n netAmount: typeof netAmount === 'string' ? netAmount : typeof netAmount === 'number' ? String(netAmount) : '0',\n grossAmount:\n typeof grossAmount === 'string' ? grossAmount : typeof grossAmount === 'number' ? String(grossAmount) : '0',\n currency: typeof data.currency === 'string' ? data.currency : null,\n createdAt: data.createdAt,\n convertedOrderId: typeof data.convertedOrderId === 'string' ? data.convertedOrderId : null,\n }\n })\n .filter((item: QuoteItem | null): item is QuoteItem => !!item)\n}\n\nconst SalesNewQuotesWidget: React.FC<DashboardWidgetComponentProps<SalesNewQuotesSettings>> = ({\n mode,\n settings = DEFAULT_SETTINGS,\n onSettingsChange,\n refreshToken,\n onRefreshStateChange,\n}) => {\n const t = useT()\n const hydrated = React.useMemo(() => hydrateSalesNewQuotesSettings(settings), [settings])\n const [items, setItems] = React.useState<QuoteItem[]>([])\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [locale, setLocale] = React.useState<string | undefined>(undefined)\n\n React.useEffect(() => {\n if (typeof navigator !== 'undefined') {\n setLocale(navigator.language)\n }\n }, [])\n\n const refresh = React.useCallback(async () => {\n onRefreshStateChange?.(true)\n setLoading(true)\n setError(null)\n try {\n const data = await loadQuotes(hydrated)\n setItems(data)\n } catch (err) {\n console.error('Failed to load new quotes widget data', err)\n setError(t('sales.widgets.newQuotes.error', 'Failed to load quotes'))\n } finally {\n setLoading(false)\n onRefreshStateChange?.(false)\n }\n }, [hydrated, onRefreshStateChange, t])\n\n React.useEffect(() => {\n refresh().catch(() => {})\n }, [refresh, refreshToken])\n\n if (mode === 'settings') {\n return (\n <div className=\"space-y-4 text-sm\">\n <div className=\"space-y-1.5\">\n <label\n htmlFor=\"sales-new-quotes-page-size\"\n className=\"text-xs font-semibold uppercase text-muted-foreground\"\n >\n {t('sales.widgets.newQuotes.settings.pageSize', 'Number of Quotes')}\n </label>\n <input\n id=\"sales-new-quotes-page-size\"\n type=\"number\"\n min={1}\n max={20}\n className=\"w-24 rounded-md border px-2 py-1 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary\"\n value={hydrated.pageSize}\n onChange={(event) => {\n const next = Number(event.target.value)\n const value = Number.isFinite(next)\n ? Math.min(20, Math.max(1, Math.floor(next)))\n : hydrated.pageSize\n onSettingsChange({ ...hydrated, pageSize: value })\n }}\n />\n </div>\n <div className=\"space-y-1.5\">\n <label\n htmlFor=\"sales-new-quotes-date-period\"\n className=\"text-xs font-semibold uppercase text-muted-foreground\"\n >\n {t('sales.widgets.newQuotes.settings.datePeriod', 'Date Period')}\n </label>\n <select\n id=\"sales-new-quotes-date-period\"\n className=\"w-full rounded-md border px-2 py-1 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary\"\n value={hydrated.datePeriod}\n onChange={(event) => {\n onSettingsChange({ ...hydrated, datePeriod: event.target.value as DatePeriodOption })\n }}\n >\n <option value=\"last24h\">{t('sales.widgets.newQuotes.settings.last24h', 'Last 24 hours')}</option>\n <option value=\"last7d\">{t('sales.widgets.newQuotes.settings.last7d', 'Last 7 days')}</option>\n <option value=\"last30d\">{t('sales.widgets.newQuotes.settings.last30d', 'Last 30 days')}</option>\n <option value=\"custom\">{t('sales.widgets.newQuotes.settings.custom', 'Custom range')}</option>\n </select>\n </div>\n {hydrated.datePeriod === 'custom' ? (\n <>\n <div className=\"space-y-1.5\">\n <label\n htmlFor=\"sales-new-quotes-custom-from\"\n className=\"text-xs font-semibold uppercase text-muted-foreground\"\n >\n {t('sales.widgets.newQuotes.settings.customFrom', 'From')}\n </label>\n <input\n id=\"sales-new-quotes-custom-from\"\n type=\"date\"\n className=\"w-full rounded-md border px-2 py-1 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary\"\n value={hydrated.customFrom ?? ''}\n onChange={(event) => {\n onSettingsChange({ ...hydrated, customFrom: event.target.value })\n }}\n />\n </div>\n <div className=\"space-y-1.5\">\n <label\n htmlFor=\"sales-new-quotes-custom-to\"\n className=\"text-xs font-semibold uppercase text-muted-foreground\"\n >\n {t('sales.widgets.newQuotes.settings.customTo', 'To')}\n </label>\n <input\n id=\"sales-new-quotes-custom-to\"\n type=\"date\"\n className=\"w-full rounded-md border px-2 py-1 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary\"\n value={hydrated.customTo ?? ''}\n onChange={(event) => {\n onSettingsChange({ ...hydrated, customTo: event.target.value })\n }}\n />\n </div>\n </>\n ) : null}\n </div>\n )\n }\n\n if (error) {\n return <p className=\"text-sm text-destructive\">{error}</p>\n }\n\n if (loading) {\n return (\n <div className=\"flex h-32 items-center justify-center\">\n <Spinner className=\"h-6 w-6 text-muted-foreground\" />\n </div>\n )\n }\n\n if (items.length === 0) {\n return (\n <p className=\"text-sm text-muted-foreground\">\n {t('sales.widgets.newQuotes.empty', 'No quotes found in this period')}\n </p>\n )\n }\n\n return (\n <ul className=\"space-y-3\">\n {items.map((quote) => {\n const createdLabel = formatRelativeDate(quote.createdAt, locale)\n const validUntilLabel = formatDate(quote.validUntil, locale)\n const isExpired = quote.validUntil ? new Date(quote.validUntil) < new Date() : false\n return (\n <li key={quote.id} className=\"rounded-md border p-3\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"space-y-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <Link\n className=\"text-sm font-medium text-foreground hover:underline\"\n href={`/backend/sales/quotes/${encodeURIComponent(quote.id)}`}\n >\n {quote.quoteNumber}\n </Link>\n {quote.status ? (\n <Badge variant=\"outline\" className=\"text-[11px]\">\n {quote.status}\n </Badge>\n ) : null}\n {quote.convertedOrderId ? (\n <Badge variant=\"secondary\" className=\"text-[11px]\">\n {t('sales.widgets.newQuotes.converted', 'Converted')}\n </Badge>\n ) : null}\n </div>\n <p className=\"text-xs text-muted-foreground\">\n {quote.customerName ?? t('sales.widgets.newQuotes.noCustomer', 'No customer')}\n </p>\n {validUntilLabel ? (\n <p\n className={`text-xs ${isExpired ? 'text-muted-foreground line-through' : 'text-muted-foreground'}`}\n >\n {t('sales.widgets.newQuotes.validUntil', 'Valid until {{date}}', { date: validUntilLabel })}\n </p>\n ) : null}\n <p className=\"text-xs text-muted-foreground\">\n {createdLabel || t('sales.widgets.newQuotes.unknownDate', 'Unknown date')}\n </p>\n </div>\n <div className=\"text-right\">\n <p className=\"text-sm font-semibold\">\n {formatCurrency(quote.grossAmount, quote.currency, locale)}\n </p>\n </div>\n </div>\n </li>\n )\n })}\n </ul>\n )\n}\n\nexport default SalesNewQuotesWidget\n"],
5
- "mappings": "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",
6
- "names": []
7
- }
@@ -1,33 +0,0 @@
1
- import SalesNewQuotesWidget from "./widget.client.js";
2
- import {
3
- DEFAULT_SETTINGS,
4
- hydrateSalesNewQuotesSettings
5
- } from "./config.js";
6
- const widget = {
7
- metadata: {
8
- id: "sales.dashboard.newQuotes",
9
- title: "New Quotes",
10
- description: "Displays recently created sales quotes.",
11
- features: ["dashboards.view", "sales.widgets.new-quotes"],
12
- defaultSize: "md",
13
- defaultEnabled: true,
14
- defaultSettings: DEFAULT_SETTINGS,
15
- tags: ["sales", "quotes"],
16
- category: "sales",
17
- icon: "lucide:file-text",
18
- supportsRefresh: true
19
- },
20
- Widget: SalesNewQuotesWidget,
21
- hydrateSettings: hydrateSalesNewQuotesSettings,
22
- dehydrateSettings: (settings) => ({
23
- pageSize: settings.pageSize,
24
- datePeriod: settings.datePeriod,
25
- customFrom: settings.customFrom,
26
- customTo: settings.customTo
27
- })
28
- };
29
- var widget_default = widget;
30
- export {
31
- widget_default as default
32
- };
33
- //# sourceMappingURL=widget.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../../../../src/modules/sales/widgets/dashboard/new-quotes/widget.ts"],
4
- "sourcesContent": ["import type { DashboardWidgetModule } from '@open-mercato/shared/modules/dashboard/widgets'\nimport SalesNewQuotesWidget from './widget.client'\nimport {\n DEFAULT_SETTINGS,\n hydrateSalesNewQuotesSettings,\n type SalesNewQuotesSettings,\n} from './config'\n\nconst widget: DashboardWidgetModule<SalesNewQuotesSettings> = {\n metadata: {\n id: 'sales.dashboard.newQuotes',\n title: 'New Quotes',\n description: 'Displays recently created sales quotes.',\n features: ['dashboards.view', 'sales.widgets.new-quotes'],\n defaultSize: 'md',\n defaultEnabled: true,\n defaultSettings: DEFAULT_SETTINGS,\n tags: ['sales', 'quotes'],\n category: 'sales',\n icon: 'lucide:file-text',\n supportsRefresh: true,\n },\n Widget: SalesNewQuotesWidget,\n hydrateSettings: hydrateSalesNewQuotesSettings,\n dehydrateSettings: (settings) => ({\n pageSize: settings.pageSize,\n datePeriod: settings.datePeriod,\n customFrom: settings.customFrom,\n customTo: settings.customTo,\n }),\n}\n\nexport default widget\n"],
5
- "mappings": "AACA,OAAO,0BAA0B;AACjC;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AAEP,MAAM,SAAwD;AAAA,EAC5D,UAAU;AAAA,IACR,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,aAAa;AAAA,IACb,UAAU,CAAC,mBAAmB,0BAA0B;AAAA,IACxD,aAAa;AAAA,IACb,gBAAgB;AAAA,IAChB,iBAAiB;AAAA,IACjB,MAAM,CAAC,SAAS,QAAQ;AAAA,IACxB,UAAU;AAAA,IACV,MAAM;AAAA,IACN,iBAAiB;AAAA,EACnB;AAAA,EACA,QAAQ;AAAA,EACR,iBAAiB;AAAA,EACjB,mBAAmB,CAAC,cAAc;AAAA,IAChC,UAAU,SAAS;AAAA,IACnB,YAAY,SAAS;AAAA,IACrB,YAAY,SAAS;AAAA,IACrB,UAAU,SAAS;AAAA,EACrB;AACF;AAEA,IAAO,iBAAQ;",
6
- "names": []
7
- }
@@ -1,60 +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
- orderNumber: 'SO-1001',
24
- status: 'pending',
25
- fulfillmentStatus: null,
26
- paymentStatus: null,
27
- customerSnapshot: { displayName: 'Acme Corp' },
28
- customerEntityId: '44444444-4444-4444-4444-444444444444',
29
- grandTotalNetAmount: '100.00',
30
- grandTotalGrossAmount: '120.00',
31
- currencyCode: 'USD',
32
- createdAt: new Date('2026-01-27T10:00:00.000Z'),
33
- },
34
- ],
35
- 1,
36
- ]),
37
- }))
38
-
39
- describe('sales new-orders widget route', () => {
40
- it('returns 200 with items on happy path', async () => {
41
- const req = new Request('http://localhost/api?limit=5')
42
- const res = await GET(req)
43
- expect(res.status).toBe(200)
44
- const body = await res.json()
45
- expect(Array.isArray(body.items)).toBe(true)
46
- expect(body.items[0]).toMatchObject({
47
- id: '11111111-1111-1111-1111-111111111111',
48
- orderNumber: 'SO-1001',
49
- customerName: 'Acme Corp',
50
- })
51
- })
52
-
53
- it('returns 400 on invalid limit', async () => {
54
- const req = new Request('http://localhost/api?limit=0')
55
- const res = await GET(req)
56
- expect(res.status).toBe(400)
57
- const body = await res.json()
58
- expect(body).toHaveProperty('error')
59
- })
60
- })
@@ -1,192 +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 { SalesOrder } 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-orders'] },
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<SalesOrder> = {
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
- SalesOrder,
105
- where,
106
- {
107
- limit,
108
- orderBy: { createdAt: 'desc' as const },
109
- },
110
- { tenantId },
111
- )
112
-
113
- const responseItems = items.map((order) => ({
114
- id: order.id,
115
- orderNumber: order.orderNumber,
116
- status: order.status ?? null,
117
- fulfillmentStatus: order.fulfillmentStatus ?? null,
118
- paymentStatus: order.paymentStatus ?? null,
119
- customerName: extractCustomerName(order.customerSnapshot) ?? null,
120
- customerEntityId: order.customerEntityId ?? null,
121
- netAmount: order.grandTotalNetAmount,
122
- grossAmount: order.grandTotalGrossAmount,
123
- currency: order.currencyCode ?? null,
124
- createdAt: order.createdAt.toISOString(),
125
- }))
126
-
127
- return NextResponse.json({
128
- items: responseItems,
129
- total,
130
- dateRange: {
131
- from: from.toISOString(),
132
- to: to.toISOString(),
133
- },
134
- })
135
- } catch (err) {
136
- if (err instanceof CrudHttpError) {
137
- return NextResponse.json(err.body, { status: err.status })
138
- }
139
- console.error('sales.widgets.newOrders failed', err)
140
- return NextResponse.json(
141
- { error: translate('sales.widgets.newOrders.error', 'Failed to load recent orders') },
142
- { status: 500 },
143
- )
144
- }
145
- }
146
-
147
- const orderItemSchema = z.object({
148
- id: z.string().uuid(),
149
- orderNumber: z.string(),
150
- status: z.string().nullable(),
151
- fulfillmentStatus: z.string().nullable(),
152
- paymentStatus: z.string().nullable(),
153
- customerName: z.string().nullable(),
154
- customerEntityId: z.string().uuid().nullable(),
155
- netAmount: z.string(),
156
- grossAmount: z.string(),
157
- currency: z.string().nullable(),
158
- createdAt: z.string(),
159
- })
160
-
161
- const responseSchema = z.object({
162
- items: z.array(orderItemSchema),
163
- total: z.number(),
164
- dateRange: z.object({
165
- from: z.string(),
166
- to: z.string(),
167
- }),
168
- })
169
-
170
- const widgetErrorSchema = z.object({
171
- error: z.string(),
172
- })
173
-
174
- export const openApi: OpenApiRouteDoc = {
175
- tag: 'Sales',
176
- summary: 'New orders widget',
177
- methods: {
178
- GET: {
179
- summary: 'Fetch recently created sales orders',
180
- description: 'Returns the most recent sales orders within the scoped tenant/organization.',
181
- query: querySchema,
182
- responses: [
183
- { status: 200, description: 'Widget payload', schema: responseSchema },
184
- ],
185
- errors: [
186
- { status: 400, description: 'Invalid query parameters', schema: widgetErrorSchema },
187
- { status: 401, description: 'Unauthorized', schema: widgetErrorSchema },
188
- { status: 500, description: 'Widget failed to load', schema: widgetErrorSchema },
189
- ],
190
- },
191
- },
192
- }