@open-mercato/core 0.4.2-canary-ccd610ad18 → 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.
Files changed (54) hide show
  1. package/dist/modules/api_docs/backend/docs/page.js +4 -1
  2. package/dist/modules/api_docs/backend/docs/page.js.map +2 -2
  3. package/dist/modules/auth/lib/setup-app.js +4 -0
  4. package/dist/modules/auth/lib/setup-app.js.map +2 -2
  5. package/dist/modules/sales/acl.js +3 -1
  6. package/dist/modules/sales/acl.js.map +2 -2
  7. package/dist/modules/sales/api/dashboard/widgets/new-orders/route.js +163 -0
  8. package/dist/modules/sales/api/dashboard/widgets/new-orders/route.js.map +7 -0
  9. package/dist/modules/sales/api/dashboard/widgets/new-quotes/route.js +165 -0
  10. package/dist/modules/sales/api/dashboard/widgets/new-quotes/route.js.map +7 -0
  11. package/dist/modules/sales/api/dashboard/widgets/utils.js +38 -0
  12. package/dist/modules/sales/api/dashboard/widgets/utils.js.map +7 -0
  13. package/dist/modules/sales/lib/customerSnapshot.js +21 -0
  14. package/dist/modules/sales/lib/customerSnapshot.js.map +7 -0
  15. package/dist/modules/sales/lib/dateRange.js +39 -0
  16. package/dist/modules/sales/lib/dateRange.js.map +7 -0
  17. package/dist/modules/sales/widgets/dashboard/new-orders/config.js +32 -0
  18. package/dist/modules/sales/widgets/dashboard/new-orders/config.js.map +7 -0
  19. package/dist/modules/sales/widgets/dashboard/new-orders/widget.client.js +252 -0
  20. package/dist/modules/sales/widgets/dashboard/new-orders/widget.client.js.map +7 -0
  21. package/dist/modules/sales/widgets/dashboard/new-orders/widget.js +33 -0
  22. package/dist/modules/sales/widgets/dashboard/new-orders/widget.js.map +7 -0
  23. package/dist/modules/sales/widgets/dashboard/new-quotes/config.js +32 -0
  24. package/dist/modules/sales/widgets/dashboard/new-quotes/config.js.map +7 -0
  25. package/dist/modules/sales/widgets/dashboard/new-quotes/widget.client.js +272 -0
  26. package/dist/modules/sales/widgets/dashboard/new-quotes/widget.client.js.map +7 -0
  27. package/dist/modules/sales/widgets/dashboard/new-quotes/widget.js +33 -0
  28. package/dist/modules/sales/widgets/dashboard/new-quotes/widget.js.map +7 -0
  29. package/package.json +2 -2
  30. package/src/modules/api_docs/backend/docs/page.tsx +2 -1
  31. package/src/modules/auth/lib/setup-app.ts +4 -0
  32. package/src/modules/customers/README.md +2 -1
  33. package/src/modules/entities/README.md +1 -1
  34. package/src/modules/sales/acl.ts +2 -0
  35. package/src/modules/sales/api/dashboard/widgets/new-orders/__tests__/route.test.ts +60 -0
  36. package/src/modules/sales/api/dashboard/widgets/new-orders/route.ts +192 -0
  37. package/src/modules/sales/api/dashboard/widgets/new-quotes/__tests__/route.test.ts +61 -0
  38. package/src/modules/sales/api/dashboard/widgets/new-quotes/route.ts +194 -0
  39. package/src/modules/sales/api/dashboard/widgets/utils.ts +53 -0
  40. package/src/modules/sales/i18n/de.json +32 -1
  41. package/src/modules/sales/i18n/en.json +32 -1
  42. package/src/modules/sales/i18n/es.json +32 -1
  43. package/src/modules/sales/i18n/pl.json +32 -1
  44. package/src/modules/sales/lib/__tests__/dateRange.test.ts +26 -0
  45. package/src/modules/sales/lib/customerSnapshot.ts +17 -0
  46. package/src/modules/sales/lib/dateRange.ts +42 -0
  47. package/src/modules/sales/widgets/dashboard/new-orders/__tests__/config.test.ts +28 -0
  48. package/src/modules/sales/widgets/dashboard/new-orders/config.ts +49 -0
  49. package/src/modules/sales/widgets/dashboard/new-orders/widget.client.tsx +295 -0
  50. package/src/modules/sales/widgets/dashboard/new-orders/widget.ts +33 -0
  51. package/src/modules/sales/widgets/dashboard/new-quotes/__tests__/config.test.ts +28 -0
  52. package/src/modules/sales/widgets/dashboard/new-quotes/config.ts +49 -0
  53. package/src/modules/sales/widgets/dashboard/new-quotes/widget.client.tsx +322 -0
  54. package/src/modules/sales/widgets/dashboard/new-quotes/widget.ts +33 -0
@@ -0,0 +1,272 @@
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
@@ -0,0 +1,7 @@
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
+ }
@@ -0,0 +1,33 @@
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
@@ -0,0 +1,7 @@
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.4.2-canary-ccd610ad18",
3
+ "version": "0.4.2-canary-92bc12ea91",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -207,7 +207,7 @@
207
207
  }
208
208
  },
209
209
  "dependencies": {
210
- "@open-mercato/shared": "0.4.2-canary-ccd610ad18",
210
+ "@open-mercato/shared": "0.4.2-canary-92bc12ea91",
211
211
  "@xyflow/react": "^12.6.0",
212
212
  "date-fns": "^4.1.0",
213
213
  "date-fns-tz": "^3.2.0"
@@ -38,7 +38,8 @@ export default async function ApiDocsPage() {
38
38
  <code className="rounded bg-background px-2 py-0.5 text-xs text-foreground">{baseUrl}</code>
39
39
  </p>
40
40
  <p>
41
- Run <code className="rounded bg-background px-2 py-0.5 text-xs text-foreground">npm run modules:prepare</code>{' '}
41
+ Run <code className="rounded bg-background px-2 py-0.5 text-xs text-foreground">yarn build:packages</code>{' '}
42
+ then <code className="rounded bg-background px-2 py-0.5 text-xs text-foreground">yarn generate</code>{' '}
42
43
  whenever APIs change to refresh the generated registry.
43
44
  </p>
44
45
  </div>
@@ -379,6 +379,8 @@ async function ensureDefaultRoleAcls(
379
379
  'catalog.variants.manage',
380
380
  'catalog.pricing.manage',
381
381
  'sales.*',
382
+ 'sales.widgets.new-orders',
383
+ 'sales.widgets.new-quotes',
382
384
  'audit_logs.*',
383
385
  'directory.organizations.view',
384
386
  'directory.organizations.manage',
@@ -419,6 +421,8 @@ async function ensureDefaultRoleAcls(
419
421
  'catalog.variants.manage',
420
422
  'catalog.pricing.manage',
421
423
  'sales.*',
424
+ 'sales.widgets.new-orders',
425
+ 'sales.widgets.new-quotes',
422
426
  'dictionaries.view',
423
427
  'example.*',
424
428
  'example.widgets.*',
@@ -45,5 +45,6 @@ npm run db:generate
45
45
  ```
46
46
  - Re-run code generators to sync metadata:
47
47
  ```bash
48
- npm run modules:prepare
48
+ yarn build:packages
49
+ yarn generate
49
50
  ```
@@ -72,4 +72,4 @@ UI integration tips
72
72
 
73
73
  Migrations
74
74
  - Run `npm run db:migrate` after enabling modules or changing this module.
75
- - Generators and module registry are updated by `npm run modules:prepare`.
75
+ - Generators and module registry are updated by running `yarn build:packages` followed by `yarn generate`.
@@ -9,6 +9,8 @@ export const features = [
9
9
  { id: 'sales.credit_memos.manage', title: 'Manage credit memos', module: 'sales' },
10
10
  { id: 'sales.channels.manage', title: 'Manage sales channels', module: 'sales' },
11
11
  { id: 'sales.settings.manage', title: 'Manage sales configuration', module: 'sales' },
12
+ { id: 'sales.widgets.new-orders', title: 'View new orders widget', module: 'sales' },
13
+ { id: 'sales.widgets.new-quotes', title: 'View new quotes widget', module: 'sales' },
12
14
  ]
13
15
 
14
16
  export default features
@@ -0,0 +1,60 @@
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
+ })