@open-mercato/core 0.4.2-canary-ccd610ad18 → 0.4.2-canary-cae9dafa24
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 +48 -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 +48 -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,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-
|
|
3
|
+
"version": "0.4.2-canary-cae9dafa24",
|
|
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-
|
|
210
|
+
"@open-mercato/shared": "0.4.2-canary-cae9dafa24",
|
|
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">
|
|
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.*',
|
|
@@ -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 `
|
|
75
|
+
- Generators and module registry are updated by running `yarn build:packages` followed by `yarn generate`.
|
package/src/modules/sales/acl.ts
CHANGED
|
@@ -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
|
+
})
|