@open-mercato/core 0.6.2-develop.3372.1.de9a169973 → 0.6.2-develop.3380.1.bb14509041

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.
@@ -1,9 +1,10 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
- import { ShoppingCart, ExternalLink, DollarSign, User, Calendar } from "lucide-react";
4
+ import { ShoppingCart, ExternalLink, Loader2, X } from "lucide-react";
5
5
  import { useRouter } from "next/navigation";
6
6
  import { Button } from "@open-mercato/ui/primitives/button";
7
+ import { IconButton } from "@open-mercato/ui/primitives/icon-button";
7
8
  import { cn } from "@open-mercato/shared/lib/utils";
8
9
  import { formatRelativeTime } from "@open-mercato/shared/lib/time";
9
10
  import { useT } from "@open-mercato/shared/lib/i18n/context";
@@ -44,12 +45,13 @@ function SalesOrderCreatedRenderer({
44
45
  setExecuting(false);
45
46
  }
46
47
  };
48
+ const timeAgo = formatRelativeTime(notification.createdAt, { translate: t }) ?? "";
47
49
  return /* @__PURE__ */ jsxs(
48
50
  "div",
49
51
  {
50
52
  className: cn(
51
- "group relative px-4 py-3 hover:bg-muted/50 cursor-pointer transition-colors border-l-4 border-l-status-info-border",
52
- isUnread && "bg-status-info-bg/50"
53
+ "group relative flex gap-4 items-start rounded-xl p-3 transition-colors hover:bg-muted/40 cursor-pointer",
54
+ isUnread && "bg-muted/20"
53
55
  ),
54
56
  onClick: handleView,
55
57
  onKeyDown: (e) => {
@@ -61,66 +63,86 @@ function SalesOrderCreatedRenderer({
61
63
  role: "button",
62
64
  tabIndex: 0,
63
65
  children: [
64
- isUnread && /* @__PURE__ */ jsx("div", { className: "absolute left-1.5 top-1/2 -translate-y-1/2 h-2 w-2 rounded-full bg-primary" }),
65
- /* @__PURE__ */ jsxs("div", { className: "flex gap-3", children: [
66
- /* @__PURE__ */ jsx("div", { className: "flex-shrink-0 mt-0.5", children: /* @__PURE__ */ jsx("div", { className: "h-10 w-10 rounded-lg bg-status-info-bg flex items-center justify-center", children: /* @__PURE__ */ jsx(ShoppingCart, { className: "h-5 w-5 text-status-info-icon" }) }) }),
67
- /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
68
- /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-2", children: [
69
- /* @__PURE__ */ jsxs("div", { children: [
70
- /* @__PURE__ */ jsx("h4", { className: cn("text-sm font-medium", isUnread && "font-semibold"), children: notification.title }),
71
- orderNumber && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-1 mt-0.5", children: /* @__PURE__ */ jsxs("span", { className: "text-xs font-mono text-muted-foreground bg-muted px-1.5 py-0.5 rounded", children: [
72
- "#",
73
- orderNumber
74
- ] }) })
66
+ /* @__PURE__ */ jsxs("div", { className: "relative shrink-0 flex size-10 items-center justify-center rounded-full bg-status-info-bg", children: [
67
+ /* @__PURE__ */ jsx(ShoppingCart, { className: "size-5 text-status-info-icon", "aria-hidden": "true" }),
68
+ isUnread ? /* @__PURE__ */ jsx(
69
+ "span",
70
+ {
71
+ className: "absolute -right-1 -top-1 size-3 rounded-full bg-accent-indigo ring-2 ring-background",
72
+ "aria-hidden": "true"
73
+ }
74
+ ) : null
75
+ ] }),
76
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [
77
+ /* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium leading-5 tracking-tight text-foreground", children: notification.title }),
78
+ /* @__PURE__ */ jsxs("div", { className: "text-xs leading-4 text-muted-foreground", children: [
79
+ timeAgo ? /* @__PURE__ */ jsxs(Fragment, { children: [
80
+ /* @__PURE__ */ jsx("span", { className: "whitespace-nowrap", children: timeAgo }),
81
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "mx-1 text-text-disabled", children: "\xB7" })
82
+ ] }) : null,
83
+ orderNumber ? /* @__PURE__ */ jsxs(Fragment, { children: [
84
+ /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center rounded bg-muted px-1.5 py-0.5 font-mono text-overline text-foreground", children: [
85
+ "#",
86
+ orderNumber
75
87
  ] }),
76
- /* @__PURE__ */ jsxs("span", { className: "flex-shrink-0 text-xs text-muted-foreground flex items-center gap-1", children: [
77
- /* @__PURE__ */ jsx(Calendar, { className: "h-3 w-3" }),
78
- formatRelativeTime(notification.createdAt, { translate: t }) ?? ""
79
- ] })
80
- ] }),
81
- /* @__PURE__ */ jsxs("div", { className: "mt-2 flex items-center gap-4 text-xs text-muted-foreground", children: [
82
- currentTotal && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
83
- /* @__PURE__ */ jsx(DollarSign, { className: "h-3 w-3" }),
84
- /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: currentTotal })
85
- ] }),
86
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
87
- /* @__PURE__ */ jsx(User, { className: "h-3 w-3" }),
88
- /* @__PURE__ */ jsx("span", { children: t("sales.notifications.renderer.assignedToYou", "Assigned to you") })
89
- ] })
90
- ] }),
91
- /* @__PURE__ */ jsxs("div", { className: "mt-3 flex gap-2", children: [
92
- /* @__PURE__ */ jsxs(
93
- Button,
94
- {
95
- variant: "default",
96
- size: "sm",
97
- onClick: (e) => {
98
- e.stopPropagation();
99
- handleView();
100
- },
101
- disabled: executing || !viewAction && !notification.linkHref,
102
- className: "gap-1",
103
- children: [
104
- /* @__PURE__ */ jsx(ExternalLink, { className: "h-3 w-3" }),
105
- t("sales.notifications.renderer.viewOrder", "View Order")
106
- ]
107
- }
108
- ),
109
- /* @__PURE__ */ jsx(
110
- Button,
111
- {
112
- variant: "ghost",
113
- size: "sm",
114
- onClick: (e) => {
115
- e.stopPropagation();
116
- onDismiss();
117
- },
118
- children: t("notifications.actions.dismiss", "Dismiss")
119
- }
120
- )
121
- ] })
88
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "mx-1 text-text-disabled", children: "\xB7" })
89
+ ] }) : null,
90
+ currentTotal ? /* @__PURE__ */ jsxs(Fragment, { children: [
91
+ /* @__PURE__ */ jsx("span", { className: "whitespace-nowrap font-medium text-foreground", children: currentTotal }),
92
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "mx-1 text-text-disabled", children: "\xB7" })
93
+ ] }) : null,
94
+ /* @__PURE__ */ jsx("span", { className: "whitespace-nowrap", children: t("sales.notifications.renderer.assignedToYou", "Assigned to you") })
95
+ ] }),
96
+ /* @__PURE__ */ jsxs("div", { className: "mt-2 flex items-center gap-2", children: [
97
+ /* @__PURE__ */ jsxs(
98
+ Button,
99
+ {
100
+ type: "button",
101
+ size: "sm",
102
+ className: "h-8 rounded-md px-2.5 bg-accent-indigo text-accent-indigo-foreground hover:bg-accent-indigo/90",
103
+ onClick: (e) => {
104
+ e.stopPropagation();
105
+ handleView();
106
+ },
107
+ disabled: executing || !viewAction && !notification.linkHref,
108
+ children: [
109
+ /* @__PURE__ */ jsx(ExternalLink, { className: "size-3.5", "aria-hidden": "true" }),
110
+ t("sales.notifications.renderer.viewOrder", "View Order"),
111
+ executing ? /* @__PURE__ */ jsx(Loader2, { className: "ml-1 size-3 animate-spin" }) : null
112
+ ]
113
+ }
114
+ ),
115
+ /* @__PURE__ */ jsx(
116
+ Button,
117
+ {
118
+ type: "button",
119
+ variant: "outline",
120
+ size: "sm",
121
+ className: "h-8 rounded-md px-2.5",
122
+ onClick: (e) => {
123
+ e.stopPropagation();
124
+ onDismiss();
125
+ },
126
+ children: t("notifications.actions.dismiss", "Dismiss")
127
+ }
128
+ )
122
129
  ] })
123
- ] })
130
+ ] }),
131
+ /* @__PURE__ */ jsx(
132
+ IconButton,
133
+ {
134
+ type: "button",
135
+ variant: "ghost",
136
+ size: "xs",
137
+ className: "absolute right-2 top-2 opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100",
138
+ onClick: (e) => {
139
+ e.stopPropagation();
140
+ onDismiss();
141
+ },
142
+ "aria-label": t("notifications.actions.dismiss", "Dismiss"),
143
+ children: /* @__PURE__ */ jsx(X, { className: "size-3.5" })
144
+ }
145
+ )
124
146
  ]
125
147
  }
126
148
  );
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/sales/widgets/notifications/SalesOrderCreatedRenderer.tsx"],
4
- "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { ShoppingCart, ExternalLink, DollarSign, User, Calendar } from 'lucide-react'\nimport { useRouter } from 'next/navigation'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { formatRelativeTime } from '@open-mercato/shared/lib/time'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { NotificationRendererProps } from '@open-mercato/shared/modules/notifications/types'\nimport { formatMoney } from '../../components/documents/lineItemUtils'\nimport { useSalesDocumentTotals } from './useSalesDocumentTotals'\n\nfunction normalizeTotal(value?: string | null): string | null {\n if (!value) return null\n let trimmed = value.trim()\n if (trimmed.startsWith('(') && trimmed.endsWith(')')) {\n trimmed = trimmed.slice(1, -1).trim()\n }\n return trimmed.length ? trimmed : null\n}\n\nexport function SalesOrderCreatedRenderer({\n notification,\n onAction,\n onDismiss,\n actions = [],\n}: NotificationRendererProps) {\n const t = useT()\n const router = useRouter()\n const [executing, setExecuting] = React.useState(false)\n const isUnread = notification.status === 'unread'\n const orderNumber = notification.bodyVariables?.orderNumber ?? notification.titleVariables?.orderNumber\n const fallbackTotal =\n normalizeTotal(notification.bodyVariables?.totalAmount ?? null) ??\n normalizeTotal(notification.bodyVariables?.total ?? null)\n const { totals } = useSalesDocumentTotals('order', notification.sourceEntityId)\n\n const currentTotal =\n totals && typeof totals.grandTotalGrossAmount === 'number'\n ? formatMoney(totals.grandTotalGrossAmount, totals.currencyCode)\n : fallbackTotal\n\n const viewAction = actions.find((action) => action.id === 'view') ?? actions[0] ?? null\n\n const handleView = async () => {\n if (!viewAction) {\n if (notification.linkHref) router.push(notification.linkHref)\n return\n }\n setExecuting(true)\n try {\n await onAction(viewAction.id)\n } finally {\n setExecuting(false)\n }\n }\n\n return (\n <div\n className={cn(\n 'group relative px-4 py-3 hover:bg-muted/50 cursor-pointer transition-colors border-l-4 border-l-status-info-border',\n isUnread && 'bg-status-info-bg/50'\n )}\n onClick={handleView}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n handleView()\n }\n }}\n role=\"button\"\n tabIndex={0}\n >\n {isUnread && (\n <div className=\"absolute left-1.5 top-1/2 -translate-y-1/2 h-2 w-2 rounded-full bg-primary\" />\n )}\n\n <div className=\"flex gap-3\">\n <div className=\"flex-shrink-0 mt-0.5\">\n <div className=\"h-10 w-10 rounded-lg bg-status-info-bg flex items-center justify-center\">\n <ShoppingCart className=\"h-5 w-5 text-status-info-icon\" />\n </div>\n </div>\n\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-start justify-between gap-2\">\n <div>\n <h4 className={cn('text-sm font-medium', isUnread && 'font-semibold')}>\n {notification.title}\n </h4>\n {orderNumber && (\n <div className=\"flex items-center gap-1 mt-0.5\">\n <span className=\"text-xs font-mono text-muted-foreground bg-muted px-1.5 py-0.5 rounded\">\n #{orderNumber}\n </span>\n </div>\n )}\n </div>\n <span className=\"flex-shrink-0 text-xs text-muted-foreground flex items-center gap-1\">\n <Calendar className=\"h-3 w-3\" />\n {formatRelativeTime(notification.createdAt, { translate: t }) ?? ''}\n </span>\n </div>\n\n <div className=\"mt-2 flex items-center gap-4 text-xs text-muted-foreground\">\n {currentTotal && (\n <div className=\"flex items-center gap-1\">\n <DollarSign className=\"h-3 w-3\" />\n <span className=\"font-medium text-foreground\">{currentTotal}</span>\n </div>\n )}\n <div className=\"flex items-center gap-1\">\n <User className=\"h-3 w-3\" />\n <span>{t('sales.notifications.renderer.assignedToYou', 'Assigned to you')}</span>\n </div>\n </div>\n\n <div className=\"mt-3 flex gap-2\">\n <Button\n variant=\"default\"\n size=\"sm\"\n onClick={(e) => {\n e.stopPropagation()\n handleView()\n }}\n disabled={executing || (!viewAction && !notification.linkHref)}\n className=\"gap-1\"\n >\n <ExternalLink className=\"h-3 w-3\" />\n {t('sales.notifications.renderer.viewOrder', 'View Order')}\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={(e) => {\n e.stopPropagation()\n onDismiss()\n }}\n >\n {t('notifications.actions.dismiss', 'Dismiss')}\n </Button>\n </div>\n </div>\n </div>\n </div>\n )\n}\n\nexport default SalesOrderCreatedRenderer\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { ShoppingCart, ExternalLink, Loader2, X } from 'lucide-react'\nimport { useRouter } from 'next/navigation'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { formatRelativeTime } from '@open-mercato/shared/lib/time'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { NotificationRendererProps } from '@open-mercato/shared/modules/notifications/types'\nimport { formatMoney } from '../../components/documents/lineItemUtils'\nimport { useSalesDocumentTotals } from './useSalesDocumentTotals'\n\nfunction normalizeTotal(value?: string | null): string | null {\n if (!value) return null\n let trimmed = value.trim()\n if (trimmed.startsWith('(') && trimmed.endsWith(')')) {\n trimmed = trimmed.slice(1, -1).trim()\n }\n return trimmed.length ? trimmed : null\n}\n\nexport function SalesOrderCreatedRenderer({\n notification,\n onAction,\n onDismiss,\n actions = [],\n}: NotificationRendererProps) {\n const t = useT()\n const router = useRouter()\n const [executing, setExecuting] = React.useState(false)\n const isUnread = notification.status === 'unread'\n const orderNumber = notification.bodyVariables?.orderNumber ?? notification.titleVariables?.orderNumber\n const fallbackTotal =\n normalizeTotal(notification.bodyVariables?.totalAmount ?? null) ??\n normalizeTotal(notification.bodyVariables?.total ?? null)\n const { totals } = useSalesDocumentTotals('order', notification.sourceEntityId)\n\n const currentTotal =\n totals && typeof totals.grandTotalGrossAmount === 'number'\n ? formatMoney(totals.grandTotalGrossAmount, totals.currencyCode)\n : fallbackTotal\n\n const viewAction = actions.find((action) => action.id === 'view') ?? actions[0] ?? null\n\n const handleView = async () => {\n if (!viewAction) {\n if (notification.linkHref) router.push(notification.linkHref)\n return\n }\n setExecuting(true)\n try {\n await onAction(viewAction.id)\n } finally {\n setExecuting(false)\n }\n }\n\n const timeAgo = formatRelativeTime(notification.createdAt, { translate: t }) ?? ''\n\n return (\n <div\n className={cn(\n 'group relative flex gap-4 items-start rounded-xl p-3 transition-colors hover:bg-muted/40 cursor-pointer',\n isUnread && 'bg-muted/20',\n )}\n onClick={handleView}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n handleView()\n }\n }}\n role=\"button\"\n tabIndex={0}\n >\n {/* Avatar: indigo-tinted circle with shopping-cart icon */}\n <div className=\"relative shrink-0 flex size-10 items-center justify-center rounded-full bg-status-info-bg\">\n <ShoppingCart className=\"size-5 text-status-info-icon\" aria-hidden=\"true\" />\n {isUnread ? (\n <span\n className=\"absolute -right-1 -top-1 size-3 rounded-full bg-accent-indigo ring-2 ring-background\"\n aria-hidden=\"true\"\n />\n ) : null}\n </div>\n\n <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n {/* Title row */}\n <p className=\"truncate text-sm font-medium leading-5 tracking-tight text-foreground\">\n {notification.title}\n </p>\n\n {/* Description row: time \u00B7 order# \u00B7 total \u00B7 assigned \u2014 inline so it wraps naturally per line */}\n <div className=\"text-xs leading-4 text-muted-foreground\">\n {timeAgo ? (\n <>\n <span className=\"whitespace-nowrap\">{timeAgo}</span>\n <span aria-hidden=\"true\" className=\"mx-1 text-text-disabled\">\u00B7</span>\n </>\n ) : null}\n {orderNumber ? (\n <>\n <span className=\"inline-flex items-center rounded bg-muted px-1.5 py-0.5 font-mono text-overline text-foreground\">\n #{orderNumber}\n </span>\n <span aria-hidden=\"true\" className=\"mx-1 text-text-disabled\">\u00B7</span>\n </>\n ) : null}\n {currentTotal ? (\n <>\n <span className=\"whitespace-nowrap font-medium text-foreground\">{currentTotal}</span>\n <span aria-hidden=\"true\" className=\"mx-1 text-text-disabled\">\u00B7</span>\n </>\n ) : null}\n <span className=\"whitespace-nowrap\">{t('sales.notifications.renderer.assignedToYou', 'Assigned to you')}</span>\n </div>\n\n {/* Actions row */}\n <div className=\"mt-2 flex items-center gap-2\">\n <Button\n type=\"button\"\n size=\"sm\"\n className=\"h-8 rounded-md px-2.5 bg-accent-indigo text-accent-indigo-foreground hover:bg-accent-indigo/90\"\n onClick={(e) => {\n e.stopPropagation()\n handleView()\n }}\n disabled={executing || (!viewAction && !notification.linkHref)}\n >\n <ExternalLink className=\"size-3.5\" aria-hidden=\"true\" />\n {t('sales.notifications.renderer.viewOrder', 'View Order')}\n {executing ? <Loader2 className=\"ml-1 size-3 animate-spin\" /> : null}\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-8 rounded-md px-2.5\"\n onClick={(e) => {\n e.stopPropagation()\n onDismiss()\n }}\n >\n {t('notifications.actions.dismiss', 'Dismiss')}\n </Button>\n </div>\n </div>\n\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n className=\"absolute right-2 top-2 opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100\"\n onClick={(e) => {\n e.stopPropagation()\n onDismiss()\n }}\n aria-label={t('notifications.actions.dismiss', 'Dismiss')}\n >\n <X className=\"size-3.5\" />\n </IconButton>\n </div>\n )\n}\n\nexport default SalesOrderCreatedRenderer\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.2-develop.3372.1.de9a169973",
3
+ "version": "0.6.2-develop.3380.1.bb14509041",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -238,16 +238,16 @@
238
238
  "zod": "^4.3.6"
239
239
  },
240
240
  "peerDependencies": {
241
- "@open-mercato/ai-assistant": "0.6.2-develop.3372.1.de9a169973",
242
- "@open-mercato/shared": "0.6.2-develop.3372.1.de9a169973",
243
- "@open-mercato/ui": "0.6.2-develop.3372.1.de9a169973",
241
+ "@open-mercato/ai-assistant": "0.6.2-develop.3380.1.bb14509041",
242
+ "@open-mercato/shared": "0.6.2-develop.3380.1.bb14509041",
243
+ "@open-mercato/ui": "0.6.2-develop.3380.1.bb14509041",
244
244
  "react": "^19.0.0",
245
245
  "react-dom": "^19.0.0"
246
246
  },
247
247
  "devDependencies": {
248
- "@open-mercato/ai-assistant": "0.6.2-develop.3372.1.de9a169973",
249
- "@open-mercato/shared": "0.6.2-develop.3372.1.de9a169973",
250
- "@open-mercato/ui": "0.6.2-develop.3372.1.de9a169973",
248
+ "@open-mercato/ai-assistant": "0.6.2-develop.3380.1.bb14509041",
249
+ "@open-mercato/shared": "0.6.2-develop.3380.1.bb14509041",
250
+ "@open-mercato/ui": "0.6.2-develop.3380.1.bb14509041",
251
251
  "@testing-library/dom": "^10.4.1",
252
252
  "@testing-library/jest-dom": "^6.9.1",
253
253
  "@testing-library/react": "^16.3.1",
@@ -4,6 +4,7 @@
4
4
  "notifications.actions.markRead": "Als gelesen markieren",
5
5
  "notifications.actions.undo": "Rückgängig",
6
6
  "notifications.badge.unread": "{count} ungelesene Benachrichtigungen",
7
+ "notifications.close": "Benachrichtigungen schließen",
7
8
  "notifications.delivery.email.actionNotice": "Aktionen sind in Open Mercato verfügbar und in dieser E-Mail schreibgeschützt.",
8
9
  "notifications.delivery.email.bodyIntro": "Prüfen Sie die Benachrichtigung und führen Sie bei Bedarf Aktionen aus.",
9
10
  "notifications.delivery.email.footer": "Open Mercato Benachrichtigungen",
@@ -17,6 +18,9 @@
17
18
  "notifications.filters.actionRequired": "Aktion erforderlich",
18
19
  "notifications.filters.all": "Alle",
19
20
  "notifications.filters.unread": "Ungelesen",
21
+ "notifications.footer.manage": "Benachrichtigungen verwalten",
22
+ "notifications.footer.toNavigate": "zum Navigieren",
23
+ "notifications.footer.useHint": "Verwende",
20
24
  "notifications.markAllRead": "Alle als gelesen markieren",
21
25
  "notifications.nav.title": "Benachrichtigungen",
22
26
  "notifications.settings.core.appUrl": "Anwendungs-URL",
@@ -46,5 +50,6 @@
46
50
  "notifications.toast.actionError": "Aktion konnte nicht ausgeführt werden",
47
51
  "notifications.toast.actionSuccess": "Aktion erfolgreich ausgeführt",
48
52
  "notifications.toast.dismissed": "Benachrichtigung verworfen",
49
- "notifications.toast.new": "Neue Benachrichtigung"
53
+ "notifications.toast.new": "Neue Benachrichtigung",
54
+ "notifications.unreadSummary": "{count} ungelesen"
50
55
  }
@@ -4,6 +4,7 @@
4
4
  "notifications.actions.markRead": "Mark as read",
5
5
  "notifications.actions.undo": "Undo",
6
6
  "notifications.badge.unread": "{count} unread notifications",
7
+ "notifications.close": "Close notifications",
7
8
  "notifications.delivery.email.actionNotice": "Actions are available in Open Mercato and are read-only in this email.",
8
9
  "notifications.delivery.email.bodyIntro": "Review the notification details and take any required actions.",
9
10
  "notifications.delivery.email.footer": "Open Mercato notifications",
@@ -17,6 +18,9 @@
17
18
  "notifications.filters.actionRequired": "Action Required",
18
19
  "notifications.filters.all": "All",
19
20
  "notifications.filters.unread": "Unread",
21
+ "notifications.footer.manage": "Manage notifications",
22
+ "notifications.footer.toNavigate": "to navigate",
23
+ "notifications.footer.useHint": "Use",
20
24
  "notifications.markAllRead": "Mark all read",
21
25
  "notifications.nav.title": "Notifications",
22
26
  "notifications.settings.core.appUrl": "Application URL",
@@ -46,5 +50,6 @@
46
50
  "notifications.toast.actionError": "Failed to execute action",
47
51
  "notifications.toast.actionSuccess": "Action completed successfully",
48
52
  "notifications.toast.dismissed": "Notification dismissed",
49
- "notifications.toast.new": "New notification"
53
+ "notifications.toast.new": "New notification",
54
+ "notifications.unreadSummary": "{count} unread"
50
55
  }
@@ -4,6 +4,7 @@
4
4
  "notifications.actions.markRead": "Marcar como leída",
5
5
  "notifications.actions.undo": "Deshacer",
6
6
  "notifications.badge.unread": "{count} notificaciones no leídas",
7
+ "notifications.close": "Cerrar notificaciones",
7
8
  "notifications.delivery.email.actionNotice": "Las acciones están disponibles en Open Mercato y son de solo lectura en este correo.",
8
9
  "notifications.delivery.email.bodyIntro": "Revisa los detalles de la notificación y realiza las acciones necesarias.",
9
10
  "notifications.delivery.email.footer": "Notificaciones de Open Mercato",
@@ -17,6 +18,9 @@
17
18
  "notifications.filters.actionRequired": "Acción requerida",
18
19
  "notifications.filters.all": "Todas",
19
20
  "notifications.filters.unread": "No leídas",
21
+ "notifications.footer.manage": "Gestionar notificaciones",
22
+ "notifications.footer.toNavigate": "para navegar",
23
+ "notifications.footer.useHint": "Usa",
20
24
  "notifications.markAllRead": "Marcar todo como leído",
21
25
  "notifications.nav.title": "Notificaciones",
22
26
  "notifications.settings.core.appUrl": "URL de la aplicación",
@@ -46,5 +50,6 @@
46
50
  "notifications.toast.actionError": "No se pudo ejecutar la acción",
47
51
  "notifications.toast.actionSuccess": "Acción completada correctamente",
48
52
  "notifications.toast.dismissed": "Notificación descartada",
49
- "notifications.toast.new": "Nueva notificación"
53
+ "notifications.toast.new": "Nueva notificación",
54
+ "notifications.unreadSummary": "{count} sin leer"
50
55
  }
@@ -4,6 +4,7 @@
4
4
  "notifications.actions.markRead": "Oznacz jako przeczytane",
5
5
  "notifications.actions.undo": "Cofnij",
6
6
  "notifications.badge.unread": "{count} nieprzeczytanych powiadomień",
7
+ "notifications.close": "Zamknij powiadomienia",
7
8
  "notifications.delivery.email.actionNotice": "Akcje są dostępne w Open Mercato i w tym e-mailu są tylko do odczytu.",
8
9
  "notifications.delivery.email.bodyIntro": "Sprawdź szczegóły powiadomienia i wykonaj wymagane działania.",
9
10
  "notifications.delivery.email.footer": "Powiadomienia Open Mercato",
@@ -17,6 +18,9 @@
17
18
  "notifications.filters.actionRequired": "Wymagana akcja",
18
19
  "notifications.filters.all": "Wszystkie",
19
20
  "notifications.filters.unread": "Nieprzeczytane",
21
+ "notifications.footer.manage": "Zarządzaj powiadomieniami",
22
+ "notifications.footer.toNavigate": "do nawigacji",
23
+ "notifications.footer.useHint": "Użyj",
20
24
  "notifications.markAllRead": "Oznacz wszystkie jako przeczytane",
21
25
  "notifications.nav.title": "Powiadomienia",
22
26
  "notifications.settings.core.appUrl": "URL aplikacji",
@@ -46,5 +50,6 @@
46
50
  "notifications.toast.actionError": "Nie udało się wykonać akcji",
47
51
  "notifications.toast.actionSuccess": "Akcja wykonana pomyślnie",
48
52
  "notifications.toast.dismissed": "Powiadomienie odrzucone",
49
- "notifications.toast.new": "Nowe powiadomienie"
53
+ "notifications.toast.new": "Nowe powiadomienie",
54
+ "notifications.unreadSummary": "{count} nieprzeczytanych"
50
55
  }
@@ -4,7 +4,7 @@ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
4
4
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
5
5
  import { SalesOrderAdjustment } from '../../data/entities'
6
6
  import {
7
- enforceReturnAdjustmentSign,
7
+ enforceAdjustmentSign,
8
8
  orderAdjustmentCreateSchema,
9
9
  } from '../../data/validators'
10
10
  import { createPagedListResponseSchema, createSalesCrudOpenApi, defaultOkResponseSchema } from '../openapi'
@@ -34,7 +34,7 @@ const routeMetadata = {
34
34
 
35
35
  const upsertSchema = orderAdjustmentCreateSchema
36
36
  .extend({ id: z.string().uuid().optional() })
37
- .superRefine(enforceReturnAdjustmentSign)
37
+ .superRefine(enforceAdjustmentSign)
38
38
 
39
39
  const deleteSchema = z.object({
40
40
  id: z.string().uuid(),
@@ -4,7 +4,7 @@ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
4
4
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
5
5
  import { SalesQuoteAdjustment } from '../../data/entities'
6
6
  import {
7
- enforceReturnAdjustmentSign,
7
+ enforceAdjustmentSign,
8
8
  quoteAdjustmentCreateSchema,
9
9
  } from '../../data/validators'
10
10
  import { createPagedListResponseSchema, createSalesCrudOpenApi, defaultOkResponseSchema } from '../openapi'
@@ -34,7 +34,7 @@ const routeMetadata = {
34
34
 
35
35
  const upsertSchema = quoteAdjustmentCreateSchema
36
36
  .extend({ id: z.string().uuid().optional() })
37
- .superRefine(enforceReturnAdjustmentSign)
37
+ .superRefine(enforceAdjustmentSign)
38
38
 
39
39
  const deleteSchema = z.object({
40
40
  id: z.string().uuid(),
@@ -66,7 +66,7 @@ import {
66
66
  CustomerPersonProfile,
67
67
  } from "../../customers/data/entities";
68
68
  import {
69
- enforceReturnAdjustmentSign,
69
+ enforceAdjustmentSign,
70
70
  quoteCreateSchema,
71
71
  quoteLineCreateSchema,
72
72
  quoteAdjustmentCreateSchema,
@@ -6304,7 +6304,7 @@ const orderAdjustmentUpsertSchema = orderAdjustmentCreateSchema
6304
6304
  .extend({
6305
6305
  id: z.string().uuid().optional(),
6306
6306
  })
6307
- .superRefine(enforceReturnAdjustmentSign);
6307
+ .superRefine(enforceAdjustmentSign);
6308
6308
 
6309
6309
  const orderAdjustmentDeleteSchema = z.object({
6310
6310
  id: z.string().uuid(),
@@ -6315,7 +6315,7 @@ const quoteAdjustmentUpsertSchema = quoteAdjustmentCreateSchema
6315
6315
  .extend({
6316
6316
  id: z.string().uuid().optional(),
6317
6317
  })
6318
- .superRefine(enforceReturnAdjustmentSign);
6318
+ .superRefine(enforceAdjustmentSign);
6319
6319
 
6320
6320
  const quoteAdjustmentDeleteSchema = z.object({
6321
6321
  id: z.string().uuid(),
@@ -419,13 +419,37 @@ export const quoteLineUpdateSchema = z
419
419
  })
420
420
  .merge(quoteLineCreateSchema.partial())
421
421
 
422
- // A `return` adjustment must never increase the grand total. The calculation
423
- // engine is defensively normalized to negative, but the API edge also rejects
424
- // positive amounts so bad input is caught before it reaches storage. See #1705.
422
+ // Each adjustment kind has an intrinsic sign convention so the grand total
423
+ // stays semantically correct regardless of operator input. The calculation
424
+ // engine normalizes signs defensively, but the API edge rejects values that
425
+ // would invert the kind's meaning so bad data never reaches storage.
426
+ //
427
+ // - `return`: non-positive — returns reduce the total (see #1705).
428
+ // - `discount`: non-negative — discounts reduce the total.
429
+ // - `surcharge`/`shipping`/`tax`: non-negative — they add to the total.
430
+ // - `custom` and unknown kinds: unconstrained (operator-controlled).
431
+ //
432
+ // See #1905.
425
433
  export const RETURN_ADJUSTMENT_POSITIVE_NET_MESSAGE =
426
434
  'Return adjustments must use a non-positive amountNet (returns reduce the total).'
427
435
  export const RETURN_ADJUSTMENT_POSITIVE_GROSS_MESSAGE =
428
436
  'Return adjustments must use a non-positive amountGross (returns reduce the total).'
437
+ export const DISCOUNT_ADJUSTMENT_NEGATIVE_NET_MESSAGE =
438
+ 'Discount adjustments must use a non-negative amountNet (discounts reduce the total).'
439
+ export const DISCOUNT_ADJUSTMENT_NEGATIVE_GROSS_MESSAGE =
440
+ 'Discount adjustments must use a non-negative amountGross (discounts reduce the total).'
441
+ export const SURCHARGE_ADJUSTMENT_NEGATIVE_NET_MESSAGE =
442
+ 'Surcharge adjustments must use a non-negative amountNet (surcharges add to the total).'
443
+ export const SURCHARGE_ADJUSTMENT_NEGATIVE_GROSS_MESSAGE =
444
+ 'Surcharge adjustments must use a non-negative amountGross (surcharges add to the total).'
445
+ export const SHIPPING_ADJUSTMENT_NEGATIVE_NET_MESSAGE =
446
+ 'Shipping adjustments must use a non-negative amountNet (shipping adds to the total).'
447
+ export const SHIPPING_ADJUSTMENT_NEGATIVE_GROSS_MESSAGE =
448
+ 'Shipping adjustments must use a non-negative amountGross (shipping adds to the total).'
449
+ export const TAX_ADJUSTMENT_NEGATIVE_NET_MESSAGE =
450
+ 'Tax adjustments must use a non-negative amountNet (taxes add to the total).'
451
+ export const TAX_ADJUSTMENT_NEGATIVE_GROSS_MESSAGE =
452
+ 'Tax adjustments must use a non-negative amountGross (taxes add to the total).'
429
453
 
430
454
  type AdjustmentSignInput = {
431
455
  kind?: string
@@ -433,27 +457,72 @@ type AdjustmentSignInput = {
433
457
  amountGross?: number
434
458
  }
435
459
 
436
- export const enforceReturnAdjustmentSign = (
460
+ const NON_NEGATIVE_SIGN_MESSAGES: Record<string, { net: string; gross: string }> = {
461
+ discount: {
462
+ net: DISCOUNT_ADJUSTMENT_NEGATIVE_NET_MESSAGE,
463
+ gross: DISCOUNT_ADJUSTMENT_NEGATIVE_GROSS_MESSAGE,
464
+ },
465
+ surcharge: {
466
+ net: SURCHARGE_ADJUSTMENT_NEGATIVE_NET_MESSAGE,
467
+ gross: SURCHARGE_ADJUSTMENT_NEGATIVE_GROSS_MESSAGE,
468
+ },
469
+ shipping: {
470
+ net: SHIPPING_ADJUSTMENT_NEGATIVE_NET_MESSAGE,
471
+ gross: SHIPPING_ADJUSTMENT_NEGATIVE_GROSS_MESSAGE,
472
+ },
473
+ tax: {
474
+ net: TAX_ADJUSTMENT_NEGATIVE_NET_MESSAGE,
475
+ gross: TAX_ADJUSTMENT_NEGATIVE_GROSS_MESSAGE,
476
+ },
477
+ }
478
+
479
+ export const enforceAdjustmentSign = (
437
480
  value: AdjustmentSignInput,
438
481
  ctx: z.RefinementCtx
439
482
  ) => {
440
- if (value.kind !== 'return') return
441
- if (typeof value.amountNet === 'number' && value.amountNet > 0) {
483
+ if (!value.kind) return
484
+ if (value.kind === 'return') {
485
+ if (typeof value.amountNet === 'number' && value.amountNet > 0) {
486
+ ctx.addIssue({
487
+ code: z.ZodIssueCode.custom,
488
+ message: RETURN_ADJUSTMENT_POSITIVE_NET_MESSAGE,
489
+ path: ['amountNet'],
490
+ })
491
+ }
492
+ if (typeof value.amountGross === 'number' && value.amountGross > 0) {
493
+ ctx.addIssue({
494
+ code: z.ZodIssueCode.custom,
495
+ message: RETURN_ADJUSTMENT_POSITIVE_GROSS_MESSAGE,
496
+ path: ['amountGross'],
497
+ })
498
+ }
499
+ return
500
+ }
501
+ const messages = NON_NEGATIVE_SIGN_MESSAGES[value.kind]
502
+ if (!messages) return
503
+ if (typeof value.amountNet === 'number' && value.amountNet < 0) {
442
504
  ctx.addIssue({
443
505
  code: z.ZodIssueCode.custom,
444
- message: RETURN_ADJUSTMENT_POSITIVE_NET_MESSAGE,
506
+ message: messages.net,
445
507
  path: ['amountNet'],
446
508
  })
447
509
  }
448
- if (typeof value.amountGross === 'number' && value.amountGross > 0) {
510
+ if (typeof value.amountGross === 'number' && value.amountGross < 0) {
449
511
  ctx.addIssue({
450
512
  code: z.ZodIssueCode.custom,
451
- message: RETURN_ADJUSTMENT_POSITIVE_GROSS_MESSAGE,
513
+ message: messages.gross,
452
514
  path: ['amountGross'],
453
515
  })
454
516
  }
455
517
  }
456
518
 
519
+ /**
520
+ * @deprecated Use `enforceAdjustmentSign` — it enforces the sign convention
521
+ * for every adjustment kind, not just `return`. Kept for backward compatibility
522
+ * with third-party code that imported the original helper.
523
+ */
524
+ export const enforceReturnAdjustmentSign = enforceAdjustmentSign
525
+
457
526
  export const orderAdjustmentCreateSchema = scoped.extend({
458
527
  orderId: uuid(),
459
528
  orderLineId: uuid().optional(),
@@ -151,13 +151,26 @@ function buildBaseDocumentResult(params: {
151
151
  )
152
152
 
153
153
  for (const adj of scopedAdjustments) {
154
- const net = toNumber(adj.amountNet, toNumber(adj.amountGross))
155
- const gross = toNumber(adj.amountGross, net)
154
+ const rawNet = toNumber(adj.amountNet, toNumber(adj.amountGross))
155
+ const rawGross = toNumber(adj.amountGross, rawNet)
156
+ // Each adjustment kind has an intrinsic sign convention. The API edge
157
+ // (enforceAdjustmentSign) rejects values that would invert the kind's
158
+ // semantic effect, but the calculation engine normalizes defensively so
159
+ // direct DB writes or seeded data can't inflate the grand total either.
160
+ // See #1905 (mirrors the existing return normalization a few lines below
161
+ // introduced for #1705).
162
+ const isNonNegativeKind =
163
+ adj.kind === 'discount' ||
164
+ adj.kind === 'surcharge' ||
165
+ adj.kind === 'shipping' ||
166
+ adj.kind === 'tax'
167
+ const net = isNonNegativeKind ? Math.abs(rawNet) : rawNet
168
+ const gross = isNonNegativeKind ? Math.abs(rawGross) : rawGross
156
169
  const taxRate = extractAdjustmentTaxRate(adj)
157
170
  const taxPortion = taxRate !== null ? round(gross - net) : 0
158
171
  switch (adj.kind) {
159
172
  case 'discount':
160
- discountTotal += Math.abs(net)
173
+ discountTotal += net
161
174
  subtotalNet = Math.max(subtotalNet - net, 0)
162
175
  subtotalGross = Math.max(subtotalGross - gross, 0)
163
176
  if (taxPortion) {