@open-mercato/core 0.6.3-develop.3901.1.ddad60693a → 0.6.3

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 (98) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/global.d.js +1 -0
  3. package/dist/global.d.js.map +7 -0
  4. package/dist/modules/catalog/commands/variants.js +11 -5
  5. package/dist/modules/catalog/commands/variants.js.map +2 -2
  6. package/dist/modules/customers/backend/customers/deals/create/page.js +3 -61
  7. package/dist/modules/customers/backend/customers/deals/create/page.js.map +2 -2
  8. package/dist/modules/customers/components/detail/DealForm.js +2 -0
  9. package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
  10. package/dist/modules/customers/components/detail/create/CreateDealForm.js +233 -0
  11. package/dist/modules/customers/components/detail/create/CreateDealForm.js.map +7 -0
  12. package/dist/modules/customers/components/detail/create/DealAssociationsField.js +209 -0
  13. package/dist/modules/customers/components/detail/create/DealAssociationsField.js.map +7 -0
  14. package/dist/modules/customers/components/detail/create/DealAssociationsSection.js +67 -0
  15. package/dist/modules/customers/components/detail/create/DealAssociationsSection.js.map +7 -0
  16. package/dist/modules/customers/components/detail/create/DealCreateSidebar.js +73 -0
  17. package/dist/modules/customers/components/detail/create/DealCreateSidebar.js.map +7 -0
  18. package/dist/modules/customers/components/detail/create/DealCurrencyField.js +92 -0
  19. package/dist/modules/customers/components/detail/create/DealCurrencyField.js.map +7 -0
  20. package/dist/modules/customers/components/detail/create/DealCustomAttributes.js +81 -0
  21. package/dist/modules/customers/components/detail/create/DealCustomAttributes.js.map +7 -0
  22. package/dist/modules/customers/components/detail/create/DealDetailsFields.js +171 -0
  23. package/dist/modules/customers/components/detail/create/DealDetailsFields.js.map +7 -0
  24. package/dist/modules/customers/components/detail/create/DealFormField.js +24 -0
  25. package/dist/modules/customers/components/detail/create/DealFormField.js.map +7 -0
  26. package/dist/modules/customers/components/detail/create/DealSectionCard.js +29 -0
  27. package/dist/modules/customers/components/detail/create/DealSectionCard.js.map +7 -0
  28. package/dist/modules/customers/components/detail/create/DealTipsCard.js +19 -0
  29. package/dist/modules/customers/components/detail/create/DealTipsCard.js.map +7 -0
  30. package/dist/modules/customers/components/detail/create/PipelineSelect.js +41 -0
  31. package/dist/modules/customers/components/detail/create/PipelineSelect.js.map +7 -0
  32. package/dist/modules/customers/components/detail/create/PipelineStageSelect.js +49 -0
  33. package/dist/modules/customers/components/detail/create/PipelineStageSelect.js.map +7 -0
  34. package/dist/modules/customers/components/detail/create/SuffixInput.js +21 -0
  35. package/dist/modules/customers/components/detail/create/SuffixInput.js.map +7 -0
  36. package/dist/modules/customers/components/detail/create/dealCustomFieldControl.js +270 -0
  37. package/dist/modules/customers/components/detail/create/dealCustomFieldControl.js.map +7 -0
  38. package/dist/modules/customers/components/detail/create/dealFormTypes.js +17 -0
  39. package/dist/modules/customers/components/detail/create/dealFormTypes.js.map +7 -0
  40. package/dist/modules/customers/components/detail/create/dealNumericInput.js +16 -0
  41. package/dist/modules/customers/components/detail/create/dealNumericInput.js.map +7 -0
  42. package/dist/modules/customers/components/detail/create/useDealCustomFields.js +93 -0
  43. package/dist/modules/customers/components/detail/create/useDealCustomFields.js.map +7 -0
  44. package/dist/modules/customers/components/detail/create/useDealPipelines.js +59 -0
  45. package/dist/modules/customers/components/detail/create/useDealPipelines.js.map +7 -0
  46. package/dist/modules/customers/components/formConfig.js +4 -2
  47. package/dist/modules/customers/components/formConfig.js.map +2 -2
  48. package/dist/modules/dictionaries/components/DictionaryEntrySelect.js +5 -2
  49. package/dist/modules/dictionaries/components/DictionaryEntrySelect.js.map +2 -2
  50. package/dist/modules/feature_toggles/lib/feature-flag-check.js +13 -5
  51. package/dist/modules/feature_toggles/lib/feature-flag-check.js.map +2 -2
  52. package/dist/modules/query_index/subscribers/coverage_refresh.js +6 -1
  53. package/dist/modules/query_index/subscribers/coverage_refresh.js.map +2 -2
  54. package/dist/modules/workflows/components/WorkflowGraph.js +29 -186
  55. package/dist/modules/workflows/components/WorkflowGraph.js.map +2 -2
  56. package/dist/modules/workflows/components/WorkflowGraphImpl.js +196 -0
  57. package/dist/modules/workflows/components/WorkflowGraphImpl.js.map +7 -0
  58. package/package.json +8 -9
  59. package/src/global.d.ts +9 -0
  60. package/src/modules/catalog/commands/variants.ts +14 -5
  61. package/src/modules/customers/backend/customers/deals/create/page.tsx +3 -64
  62. package/src/modules/customers/components/detail/DealForm.tsx +2 -0
  63. package/src/modules/customers/components/detail/create/CreateDealForm.tsx +254 -0
  64. package/src/modules/customers/components/detail/create/DealAssociationsField.tsx +253 -0
  65. package/src/modules/customers/components/detail/create/DealAssociationsSection.tsx +72 -0
  66. package/src/modules/customers/components/detail/create/DealCreateSidebar.tsx +79 -0
  67. package/src/modules/customers/components/detail/create/DealCurrencyField.tsx +108 -0
  68. package/src/modules/customers/components/detail/create/DealCustomAttributes.tsx +118 -0
  69. package/src/modules/customers/components/detail/create/DealDetailsFields.tsx +171 -0
  70. package/src/modules/customers/components/detail/create/DealFormField.tsx +39 -0
  71. package/src/modules/customers/components/detail/create/DealSectionCard.tsx +40 -0
  72. package/src/modules/customers/components/detail/create/DealTipsCard.tsx +26 -0
  73. package/src/modules/customers/components/detail/create/PipelineSelect.tsx +55 -0
  74. package/src/modules/customers/components/detail/create/PipelineStageSelect.tsx +70 -0
  75. package/src/modules/customers/components/detail/create/SuffixInput.tsx +20 -0
  76. package/src/modules/customers/components/detail/create/dealCustomFieldControl.tsx +310 -0
  77. package/src/modules/customers/components/detail/create/dealFormTypes.ts +29 -0
  78. package/src/modules/customers/components/detail/create/dealNumericInput.ts +20 -0
  79. package/src/modules/customers/components/detail/create/useDealCustomFields.ts +118 -0
  80. package/src/modules/customers/components/detail/create/useDealPipelines.ts +80 -0
  81. package/src/modules/customers/components/formConfig.tsx +3 -0
  82. package/src/modules/customers/i18n/de.json +26 -0
  83. package/src/modules/customers/i18n/en.json +26 -0
  84. package/src/modules/customers/i18n/es.json +26 -0
  85. package/src/modules/customers/i18n/pl.json +26 -0
  86. package/src/modules/dictionaries/components/DictionaryEntrySelect.tsx +12 -1
  87. package/src/modules/feature_toggles/lib/feature-flag-check.ts +14 -4
  88. package/src/modules/query_index/subscribers/coverage_refresh.ts +7 -1
  89. package/src/modules/resources/i18n/de.json +1 -0
  90. package/src/modules/resources/i18n/en.json +1 -0
  91. package/src/modules/resources/i18n/es.json +1 -0
  92. package/src/modules/resources/i18n/pl.json +1 -0
  93. package/src/modules/sales/i18n/de.json +2 -0
  94. package/src/modules/sales/i18n/en.json +2 -0
  95. package/src/modules/sales/i18n/es.json +2 -0
  96. package/src/modules/sales/i18n/pl.json +2 -0
  97. package/src/modules/workflows/components/WorkflowGraph.tsx +39 -235
  98. package/src/modules/workflows/components/WorkflowGraphImpl.tsx +233 -0
@@ -0,0 +1,196 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import "@xyflow/react/dist/style.css";
4
+ import { useCallback, useMemo, useEffect, useState } from "react";
5
+ import {
6
+ ReactFlow,
7
+ Controls,
8
+ Background,
9
+ BackgroundVariant,
10
+ MiniMap,
11
+ Panel,
12
+ useNodesState,
13
+ useEdgesState,
14
+ addEdge,
15
+ ConnectionMode,
16
+ MarkerType
17
+ } from "@xyflow/react";
18
+ import { StartNode, EndNode, UserTaskNode, AutomatedNode, SubWorkflowNode, WaitForSignalNode, WaitForTimerNode } from "./nodes/index.js";
19
+ import { WorkflowTransitionEdge } from "./WorkflowTransitionEdge.js";
20
+ import { STATUS_COLORS } from "../lib/status-colors.js";
21
+ import { Alert, AlertDescription } from "@open-mercato/ui/primitives/alert";
22
+ import { Edit3 } from "lucide-react";
23
+ import { useTheme } from "@open-mercato/ui/theme";
24
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
25
+ function WorkflowGraphImpl({
26
+ initialNodes = [],
27
+ initialEdges = [],
28
+ onNodesChange: onNodesChangeProp,
29
+ onEdgesChange: onEdgesChangeProp,
30
+ onNodeClick: onNodeClickProp,
31
+ onEdgeClick: onEdgeClickProp,
32
+ onConnect: onConnectProp,
33
+ editable = false,
34
+ className = "",
35
+ height = "600px"
36
+ }) {
37
+ const t = useT();
38
+ const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
39
+ const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
40
+ const { resolvedTheme } = useTheme();
41
+ const isDark = resolvedTheme === "dark";
42
+ const backgroundDotColor = isDark ? "#374151" : "#e5e7eb";
43
+ const [isCompactViewport, setIsCompactViewport] = useState(false);
44
+ useEffect(() => {
45
+ if (typeof window === "undefined") return;
46
+ const mediaQuery = window.matchMedia("(max-width: 1279px)");
47
+ const updateViewportMode = () => setIsCompactViewport(mediaQuery.matches);
48
+ updateViewportMode();
49
+ mediaQuery.addEventListener("change", updateViewportMode);
50
+ return () => {
51
+ mediaQuery.removeEventListener("change", updateViewportMode);
52
+ };
53
+ }, []);
54
+ useEffect(() => {
55
+ setNodes(initialNodes);
56
+ }, [initialNodes, setNodes]);
57
+ useEffect(() => {
58
+ setEdges(initialEdges);
59
+ }, [initialEdges, setEdges]);
60
+ const onConnect = useCallback(
61
+ (connection) => {
62
+ if (onConnectProp) {
63
+ onConnectProp(connection);
64
+ } else {
65
+ const newEdge = {
66
+ ...connection,
67
+ type: "workflowTransition",
68
+ animated: false,
69
+ markerEnd: {
70
+ type: MarkerType.ArrowClosed,
71
+ width: 16,
72
+ height: 16,
73
+ color: "#9ca3af"
74
+ }
75
+ };
76
+ setEdges((eds) => addEdge(newEdge, eds));
77
+ }
78
+ },
79
+ [setEdges, onConnectProp]
80
+ );
81
+ const handleNodesChange = useCallback(
82
+ (changes) => {
83
+ onNodesChange(changes);
84
+ if (onNodesChangeProp) {
85
+ onNodesChangeProp(changes);
86
+ }
87
+ },
88
+ [onNodesChange, onNodesChangeProp]
89
+ );
90
+ const handleEdgesChange = useCallback(
91
+ (changes) => {
92
+ onEdgesChange(changes);
93
+ if (onEdgesChangeProp) {
94
+ onEdgesChangeProp(changes);
95
+ }
96
+ },
97
+ [onEdgesChange, onEdgesChangeProp]
98
+ );
99
+ const nodeTypes = useMemo(
100
+ () => ({
101
+ start: StartNode,
102
+ end: EndNode,
103
+ userTask: UserTaskNode,
104
+ automated: AutomatedNode,
105
+ subWorkflow: SubWorkflowNode,
106
+ waitForSignal: WaitForSignalNode,
107
+ waitForTimer: WaitForTimerNode
108
+ }),
109
+ []
110
+ );
111
+ const edgeTypes = useMemo(
112
+ () => ({
113
+ workflowTransition: WorkflowTransitionEdge
114
+ }),
115
+ []
116
+ );
117
+ return /* @__PURE__ */ jsx("div", { className: `workflow-graph-container ${className}`, style: { height }, children: /* @__PURE__ */ jsxs(
118
+ ReactFlow,
119
+ {
120
+ nodes,
121
+ edges,
122
+ nodeTypes,
123
+ edgeTypes,
124
+ onNodesChange: handleNodesChange,
125
+ onEdgesChange: handleEdgesChange,
126
+ onConnect: editable ? onConnect : void 0,
127
+ onNodeClick: onNodeClickProp,
128
+ onEdgeClick: onEdgeClickProp,
129
+ connectionMode: ConnectionMode.Loose,
130
+ fitView: true,
131
+ fitViewOptions: {
132
+ padding: 0.2,
133
+ maxZoom: isCompactViewport ? 0.9 : 1
134
+ },
135
+ minZoom: 0.1,
136
+ maxZoom: 2,
137
+ defaultEdgeOptions: {
138
+ type: "workflowTransition",
139
+ animated: false,
140
+ markerEnd: {
141
+ type: MarkerType.ArrowClosed,
142
+ width: 16,
143
+ height: 16,
144
+ color: "#9ca3af"
145
+ }
146
+ },
147
+ nodesDraggable: editable,
148
+ nodesConnectable: editable,
149
+ elementsSelectable: editable,
150
+ proOptions: { hideAttribution: true },
151
+ children: [
152
+ /* @__PURE__ */ jsx(
153
+ Background,
154
+ {
155
+ variant: BackgroundVariant.Dots,
156
+ gap: 16,
157
+ size: 1,
158
+ color: backgroundDotColor
159
+ }
160
+ ),
161
+ /* @__PURE__ */ jsx(
162
+ Controls,
163
+ {
164
+ showZoom: true,
165
+ showFitView: true,
166
+ showInteractive: false,
167
+ position: isCompactViewport ? "bottom-right" : "top-right",
168
+ className: `!bg-card !border-border !shadow-md [&>button]:!bg-card [&>button]:!border-border [&>button]:!fill-foreground [&>button:hover]:!bg-muted ${isCompactViewport ? "scale-90 origin-bottom-right" : ""}`
169
+ }
170
+ ),
171
+ !isCompactViewport && /* @__PURE__ */ jsx(
172
+ MiniMap,
173
+ {
174
+ nodeStrokeWidth: 3,
175
+ nodeColor: (node) => {
176
+ const status = node.data?.status || "not_started";
177
+ return STATUS_COLORS[status]?.hex || STATUS_COLORS.not_started.hex;
178
+ },
179
+ maskColor: "rgba(0, 0, 0, 0.1)",
180
+ position: "bottom-left",
181
+ className: "!bg-card !border !border-border !rounded-lg"
182
+ }
183
+ ),
184
+ !editable && !isCompactViewport && /* @__PURE__ */ jsx(Panel, { position: "top-left", style: { margin: 10 }, children: /* @__PURE__ */ jsx("div", { className: "bg-card rounded-lg shadow-sm border border-border px-4 py-2", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground font-medium", children: t("workflows.graph.visualization") }) }) }),
185
+ editable && !isCompactViewport && /* @__PURE__ */ jsx(Panel, { position: "top-left", style: { margin: 10 }, children: /* @__PURE__ */ jsxs(Alert, { variant: "info", className: "max-w-sm", children: [
186
+ /* @__PURE__ */ jsx(Edit3, { className: "size-4" }),
187
+ /* @__PURE__ */ jsx(AlertDescription, { className: "font-medium", children: t("workflows.graph.editModeInfo") })
188
+ ] }) })
189
+ ]
190
+ }
191
+ ) });
192
+ }
193
+ export {
194
+ WorkflowGraphImpl as default
195
+ };
196
+ //# sourceMappingURL=WorkflowGraphImpl.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/workflows/components/WorkflowGraphImpl.tsx"],
4
+ "sourcesContent": ["'use client'\n\nimport '@xyflow/react/dist/style.css'\n\nimport { useCallback, useMemo, useEffect, useState } from 'react'\nimport {\n ReactFlow,\n Node,\n Edge,\n Controls,\n Background,\n BackgroundVariant,\n MiniMap,\n Panel,\n useNodesState,\n useEdgesState,\n addEdge,\n Connection,\n ConnectionMode,\n MarkerType,\n} from '@xyflow/react'\nimport {StartNode, EndNode, UserTaskNode, AutomatedNode, SubWorkflowNode, WaitForSignalNode, WaitForTimerNode} from './nodes'\nimport { WorkflowTransitionEdge } from './WorkflowTransitionEdge'\nimport { STATUS_COLORS } from '../lib/status-colors'\nimport { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport { Edit3 } from 'lucide-react'\nimport { useTheme } from '@open-mercato/ui/theme'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\nexport interface WorkflowGraphImplProps {\n initialNodes?: Node[]\n initialEdges?: Edge[]\n onNodesChange?: (changes: any[]) => void\n onEdgesChange?: (changes: any[]) => void\n onNodeClick?: (event: React.MouseEvent, node: Node) => void\n onEdgeClick?: (event: React.MouseEvent, edge: Edge) => void\n onConnect?: (connection: Connection) => void\n editable?: boolean\n className?: string\n height?: string\n}\n\nexport default function WorkflowGraphImpl({\n initialNodes = [],\n initialEdges = [],\n onNodesChange: onNodesChangeProp,\n onEdgesChange: onEdgesChangeProp,\n onNodeClick: onNodeClickProp,\n onEdgeClick: onEdgeClickProp,\n onConnect: onConnectProp,\n editable = false,\n className = '',\n height = '600px',\n}: WorkflowGraphImplProps) {\n const t = useT()\n const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes)\n const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges)\n\n const { resolvedTheme } = useTheme()\n const isDark = resolvedTheme === 'dark'\n const backgroundDotColor = isDark ? '#374151' : '#e5e7eb'\n const [isCompactViewport, setIsCompactViewport] = useState(false)\n\n useEffect(() => {\n if (typeof window === 'undefined') return\n const mediaQuery = window.matchMedia('(max-width: 1279px)')\n const updateViewportMode = () => setIsCompactViewport(mediaQuery.matches)\n\n updateViewportMode()\n mediaQuery.addEventListener('change', updateViewportMode)\n\n return () => {\n mediaQuery.removeEventListener('change', updateViewportMode)\n }\n }, [])\n\n useEffect(() => {\n setNodes(initialNodes)\n }, [initialNodes, setNodes])\n\n useEffect(() => {\n setEdges(initialEdges)\n }, [initialEdges, setEdges])\n\n const onConnect = useCallback(\n (connection: Connection) => {\n if (onConnectProp) {\n onConnectProp(connection)\n } else {\n const newEdge = {\n ...connection,\n type: 'workflowTransition',\n animated: false,\n markerEnd: {\n type: MarkerType.ArrowClosed,\n width: 16,\n height: 16,\n color: '#9ca3af',\n },\n }\n setEdges((eds) => addEdge(newEdge, eds))\n }\n },\n [setEdges, onConnectProp]\n )\n\n const handleNodesChange = useCallback(\n (changes: any) => {\n onNodesChange(changes)\n if (onNodesChangeProp) {\n onNodesChangeProp(changes)\n }\n },\n [onNodesChange, onNodesChangeProp]\n )\n\n const handleEdgesChange = useCallback(\n (changes: any) => {\n onEdgesChange(changes)\n if (onEdgesChangeProp) {\n onEdgesChangeProp(changes)\n }\n },\n [onEdgesChange, onEdgesChangeProp]\n )\n\n const nodeTypes = useMemo(\n () => ({\n start: StartNode,\n end: EndNode,\n userTask: UserTaskNode,\n automated: AutomatedNode,\n subWorkflow: SubWorkflowNode,\n waitForSignal: WaitForSignalNode,\n waitForTimer: WaitForTimerNode,\n }),\n []\n )\n\n const edgeTypes = useMemo(\n () => ({\n workflowTransition: WorkflowTransitionEdge,\n }),\n []\n )\n\n return (\n <div className={`workflow-graph-container ${className}`} style={{ height }}>\n <ReactFlow\n nodes={nodes}\n edges={edges}\n nodeTypes={nodeTypes}\n edgeTypes={edgeTypes}\n onNodesChange={handleNodesChange}\n onEdgesChange={handleEdgesChange}\n onConnect={editable ? onConnect : undefined}\n onNodeClick={onNodeClickProp}\n onEdgeClick={onEdgeClickProp}\n connectionMode={ConnectionMode.Loose}\n fitView\n fitViewOptions={{\n padding: 0.2,\n maxZoom: isCompactViewport ? 0.9 : 1,\n }}\n minZoom={0.1}\n maxZoom={2}\n defaultEdgeOptions={{\n type: 'workflowTransition',\n animated: false,\n markerEnd: {\n type: MarkerType.ArrowClosed,\n width: 16,\n height: 16,\n color: '#9ca3af',\n },\n }}\n nodesDraggable={editable}\n nodesConnectable={editable}\n elementsSelectable={editable}\n proOptions={{ hideAttribution: true }}\n >\n <Background\n variant={BackgroundVariant.Dots}\n gap={16}\n size={1}\n color={backgroundDotColor}\n />\n\n <Controls\n showZoom={true}\n showFitView={true}\n showInteractive={false}\n position={isCompactViewport ? 'bottom-right' : 'top-right'}\n className={`!bg-card !border-border !shadow-md [&>button]:!bg-card [&>button]:!border-border [&>button]:!fill-foreground [&>button:hover]:!bg-muted ${isCompactViewport ? 'scale-90 origin-bottom-right' : ''}`}\n />\n\n {!isCompactViewport && (\n <MiniMap\n nodeStrokeWidth={3}\n nodeColor={(node) => {\n const status = (node.data?.status || 'not_started') as keyof typeof STATUS_COLORS\n return STATUS_COLORS[status]?.hex || STATUS_COLORS.not_started.hex\n }}\n maskColor=\"rgba(0, 0, 0, 0.1)\"\n position=\"bottom-left\"\n className=\"!bg-card !border !border-border !rounded-lg\"\n />\n )}\n\n {!editable && !isCompactViewport && (\n <Panel position=\"top-left\" style={{ margin: 10 }}>\n <div className=\"bg-card rounded-lg shadow-sm border border-border px-4 py-2\">\n <p className=\"text-sm text-muted-foreground font-medium\">\n {t('workflows.graph.visualization')}\n </p>\n </div>\n </Panel>\n )}\n\n {editable && !isCompactViewport && (\n <Panel position=\"top-left\" style={{ margin: 10 }}>\n <Alert variant=\"info\" className=\"max-w-sm\">\n <Edit3 className=\"size-4\" />\n <AlertDescription className=\"font-medium\">\n {t('workflows.graph.editModeInfo')}\n </AlertDescription>\n </Alert>\n </Panel>\n )}\n </ReactFlow>\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.3-develop.3901.1.ddad60693a",
3
+ "version": "0.6.3",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -243,16 +243,16 @@
243
243
  "zod": "^4.4.3"
244
244
  },
245
245
  "peerDependencies": {
246
- "@open-mercato/ai-assistant": "0.6.3-develop.3901.1.ddad60693a",
247
- "@open-mercato/shared": "0.6.3-develop.3901.1.ddad60693a",
248
- "@open-mercato/ui": "0.6.3-develop.3901.1.ddad60693a",
246
+ "@open-mercato/ai-assistant": "0.6.3",
247
+ "@open-mercato/shared": "0.6.3",
248
+ "@open-mercato/ui": "0.6.3",
249
249
  "react": "^19.0.0",
250
250
  "react-dom": "^19.0.0"
251
251
  },
252
252
  "devDependencies": {
253
- "@open-mercato/ai-assistant": "0.6.3-develop.3901.1.ddad60693a",
254
- "@open-mercato/shared": "0.6.3-develop.3901.1.ddad60693a",
255
- "@open-mercato/ui": "0.6.3-develop.3901.1.ddad60693a",
253
+ "@open-mercato/ai-assistant": "0.6.3",
254
+ "@open-mercato/shared": "0.6.3",
255
+ "@open-mercato/ui": "0.6.3",
256
256
  "@testing-library/dom": "^10.4.1",
257
257
  "@testing-library/jest-dom": "^6.9.1",
258
258
  "@testing-library/react": "^16.3.1",
@@ -276,6 +276,5 @@
276
276
  "type": "git",
277
277
  "url": "https://github.com/open-mercato/open-mercato",
278
278
  "directory": "packages/core"
279
- },
280
- "stableVersion": "0.6.2"
279
+ }
281
280
  }
@@ -0,0 +1,9 @@
1
+ // Ambient module declarations for the @open-mercato/core TypeScript context.
2
+ //
3
+ // Next.js apps get these declarations via `next-env.d.ts`, but workspace
4
+ // packages need them locally because they may import CSS side-effects from
5
+ // .tsx files (e.g. `import 'pkg/dist/style.css'` inside a lazy-loaded
6
+ // client component).
7
+
8
+ declare module '*.css'
9
+ declare module '*.scss'
@@ -103,7 +103,12 @@ async function loadVariantSnapshot(
103
103
  ): Promise<VariantSnapshot | null> {
104
104
  const record = await em.findOne(CatalogProductVariant, { id, deletedAt: null })
105
105
  if (!record) return null
106
- const prices = options.includePrices ? await loadVariantPriceSnapshots(em, record.id) : null
106
+ const prices = options.includePrices
107
+ ? await loadVariantPriceSnapshots(em, record.id, {
108
+ tenantId: record.tenantId,
109
+ organizationId: record.organizationId,
110
+ })
111
+ : null
107
112
  const custom = await loadCustomFieldSnapshot(em, {
108
113
  entityId: E.catalog.catalog_product_variant,
109
114
  recordId: record.id,
@@ -223,14 +228,15 @@ type VariantPriceSnapshot = {
223
228
 
224
229
  async function loadVariantPriceSnapshots(
225
230
  em: EntityManager,
226
- variantId: string
231
+ variantId: string,
232
+ scope: { tenantId: string; organizationId: string }
227
233
  ): Promise<VariantPriceSnapshot[]> {
228
234
  const prices = await findWithDecryption(
229
235
  em,
230
236
  CatalogProductPrice,
231
- { variant: variantId },
237
+ { variant: variantId, tenantId: scope.tenantId, organizationId: scope.organizationId },
232
238
  { populate: ['priceKind', 'product', 'offer'] },
233
- { tenantId: null, organizationId: null },
239
+ { tenantId: scope.tenantId, organizationId: scope.organizationId },
234
240
  )
235
241
  const snapshots: VariantPriceSnapshot[] = []
236
242
  for (const price of prices) {
@@ -904,7 +910,10 @@ const deleteVariantCommand: CommandHandler<
904
910
  const priceSnapshots =
905
911
  snapshot?.prices && snapshot.prices.length
906
912
  ? snapshot.prices
907
- : await loadVariantPriceSnapshots(baseEm, id)
913
+ : await loadVariantPriceSnapshots(baseEm, id, {
914
+ tenantId: record.tenantId,
915
+ organizationId: record.organizationId,
916
+ })
908
917
 
909
918
  if (priceSnapshots.length) {
910
919
  await em.nativeDelete(CatalogProductPrice, { id: { $in: priceSnapshots.map((price) => price.id) } })
@@ -1,13 +1,9 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from 'react'
4
- import { useRouter, useSearchParams } from 'next/navigation'
4
+ import { useSearchParams } from 'next/navigation'
5
5
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
6
- import { flash } from '@open-mercato/ui/backend/FlashMessages'
7
- import { createCrud } from '@open-mercato/ui/backend/utils/crud'
8
- import { useT } from '@open-mercato/shared/lib/i18n/context'
9
- import { DealForm, type DealFormSubmitPayload } from '../../../../components/detail/DealForm'
10
- import { useCurrencyDictionary } from '../../../../components/detail/hooks/useCurrencyDictionary'
6
+ import { CreateDealForm } from '../../../../components/detail/create/CreateDealForm'
11
7
 
12
8
  const DEFAULT_RETURN_TO = '/backend/customers/deals'
13
9
 
@@ -24,73 +20,16 @@ function resolveReturnTo(value: string | null | undefined): string {
24
20
  }
25
21
 
26
22
  export default function CreateDealPage() {
27
- const t = useT()
28
- const router = useRouter()
29
23
  const searchParams = useSearchParams()
30
24
  const returnTo = React.useMemo(
31
25
  () => resolveReturnTo(searchParams?.get('returnTo') ?? null),
32
26
  [searchParams],
33
27
  )
34
- const [isSubmitting, setIsSubmitting] = React.useState(false)
35
- useCurrencyDictionary()
36
-
37
- const handleCancel = React.useCallback(() => {
38
- router.push(returnTo)
39
- }, [router, returnTo])
40
-
41
- const handleSubmit = React.useCallback(
42
- async ({ base, custom }: DealFormSubmitPayload) => {
43
- if (isSubmitting) return
44
- setIsSubmitting(true)
45
- try {
46
- const payload: Record<string, unknown> = {
47
- title: base.title,
48
- status: base.status ?? undefined,
49
- pipelineStage: base.pipelineStage ?? undefined,
50
- pipelineId: base.pipelineId ?? undefined,
51
- pipelineStageId: base.pipelineStageId ?? undefined,
52
- valueAmount: typeof base.valueAmount === 'number' ? base.valueAmount : undefined,
53
- valueCurrency: base.valueCurrency ?? undefined,
54
- probability: typeof base.probability === 'number' ? base.probability : undefined,
55
- expectedCloseAt: base.expectedCloseAt ?? undefined,
56
- description: base.description ?? undefined,
57
- personIds: Array.isArray(base.personIds) && base.personIds.length ? base.personIds : undefined,
58
- companyIds: Array.isArray(base.companyIds) && base.companyIds.length ? base.companyIds : undefined,
59
- }
60
- if (Object.keys(custom).length) payload.customFields = custom
61
-
62
- await createCrud('customers/deals', payload, {
63
- errorMessage: t('customers.deals.create.error', 'Failed to create deal.'),
64
- })
65
- flash(t('customers.people.detail.deals.success', 'Deal created.'), 'success')
66
- router.push(returnTo)
67
- } catch (err) {
68
- const message =
69
- err instanceof Error
70
- ? err.message
71
- : t('customers.deals.create.error', 'Failed to create deal.')
72
- flash(message, 'error')
73
- throw err instanceof Error ? err : new Error(message)
74
- } finally {
75
- setIsSubmitting(false)
76
- }
77
- },
78
- [isSubmitting, returnTo, router, t],
79
- )
80
28
 
81
29
  return (
82
30
  <Page>
83
31
  <PageBody>
84
- <DealForm
85
- mode="create"
86
- onSubmit={handleSubmit}
87
- onCancel={handleCancel}
88
- isSubmitting={isSubmitting}
89
- submitLabel={t('customers.deals.create.submit', 'Create deal')}
90
- embedded={false}
91
- title={t('customers.deals.create.title', 'Create deal')}
92
- backHref={returnTo}
93
- />
32
+ <CreateDealForm returnTo={returnTo} />
94
33
  </PageBody>
95
34
  </Page>
96
35
  )
@@ -177,6 +177,8 @@ const schema = z.object({
177
177
  companyIds: z.array(z.string().trim().min(1)).optional(),
178
178
  }).passthrough()
179
179
 
180
+ export const dealFormSchema = schema
181
+
180
182
  import { toDateInputValue as toDateInputValueOrNull } from '@open-mercato/shared/lib/date/format'
181
183
 
182
184
  function toDateInputValue(value: string | null | undefined): string {
@@ -0,0 +1,254 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { useRouter } from 'next/navigation'
5
+ import { Briefcase, Save } from 'lucide-react'
6
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
7
+ import { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'
8
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
9
+ import { createCrud } from '@open-mercato/ui/backend/utils/crud'
10
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
11
+ import { FormHeader } from '@open-mercato/ui/backend/forms'
12
+ import { Button } from '@open-mercato/ui/primitives/button'
13
+ import { Spinner } from '@open-mercato/ui/primitives/spinner'
14
+ import { dealFormSchema } from '../DealForm'
15
+ import { createDictionarySelectLabels } from '../utils'
16
+ import { DealSectionCard } from './DealSectionCard'
17
+ import { DealDetailsFields } from './DealDetailsFields'
18
+ import { DealAssociationsSection } from './DealAssociationsSection'
19
+ import { DealCreateSidebar } from './DealCreateSidebar'
20
+ import { useDealPipelines } from './useDealPipelines'
21
+ import { useDealCustomFields } from './useDealCustomFields'
22
+ import { EMPTY_VALUES, type BaseValues } from './dealFormTypes'
23
+
24
+ const CONTEXT_ID = 'customers.deals.create'
25
+ const DEAL_ENTITY_ID = 'customers:customer_deal'
26
+ const CUSTOM_FIELDS_MANAGE_HREF = `/backend/entities/system/${encodeURIComponent(DEAL_ENTITY_ID)}`
27
+
28
+ export type CreateDealFormProps = {
29
+ returnTo: string
30
+ }
31
+
32
+ export function CreateDealForm({ returnTo }: CreateDealFormProps) {
33
+ const t = useT()
34
+ const router = useRouter()
35
+ const tr = React.useCallback(
36
+ (key: string, fallback: string, params?: Record<string, string | number>) =>
37
+ translateWithFallback(t, key, fallback, params),
38
+ [t],
39
+ )
40
+
41
+ const [values, setValues] = React.useState<BaseValues>(EMPTY_VALUES)
42
+ const [errors, setErrors] = React.useState<Record<string, string>>({})
43
+ const [isSubmitting, setIsSubmitting] = React.useState(false)
44
+
45
+ const { pipelines, stages, loadStages } = useDealPipelines()
46
+ const {
47
+ customValues,
48
+ customFieldsLoaded,
49
+ customCount,
50
+ handleCustomChange,
51
+ handleCustomAttributesLoaded,
52
+ validateCustomFields,
53
+ collectNormalizedCustomValues,
54
+ } = useDealCustomFields(tr)
55
+
56
+ const { runMutation, retryLastMutation } = useGuardedMutation<{
57
+ formId: string
58
+ resourceKind: string
59
+ retryLastMutation: () => Promise<boolean>
60
+ }>({
61
+ contextId: CONTEXT_ID,
62
+ blockedMessage: tr('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
63
+ })
64
+
65
+ const statusLabels = React.useMemo(
66
+ () => createDictionarySelectLabels('deal-statuses', (key, fallback) => tr(key, fallback ?? key)),
67
+ [tr],
68
+ )
69
+
70
+ const patch = React.useCallback((partial: Partial<BaseValues>) => {
71
+ setValues((current) => ({ ...current, ...partial }))
72
+ }, [])
73
+
74
+ const handlePipelineChange = React.useCallback(
75
+ (id: string) => {
76
+ patch({ pipelineId: id, pipelineStageId: '' })
77
+ // loadStages resets stages to [] on failure; the rejection is intentionally ignored here.
78
+ loadStages(id).catch(() => {})
79
+ },
80
+ [loadStages, patch],
81
+ )
82
+
83
+ const handleCancel = React.useCallback(() => {
84
+ router.push(returnTo)
85
+ }, [returnTo, router])
86
+
87
+ const handleSubmit = React.useCallback(async () => {
88
+ if (isSubmitting) return
89
+ if (!customFieldsLoaded) {
90
+ flash(tr('customers.deals.create.sections.custom.loading', 'Loading custom fields...'), 'error')
91
+ return
92
+ }
93
+ const merged = { ...values, ...customValues }
94
+ const parsed = dealFormSchema.safeParse(merged)
95
+ if (!parsed.success) {
96
+ const fieldErrors: Record<string, string> = {}
97
+ for (const issue of parsed.error.issues) {
98
+ const key = typeof issue.path[0] === 'string' ? issue.path[0] : undefined
99
+ if (key && !fieldErrors[key]) fieldErrors[key] = tr(issue.message, issue.message)
100
+ }
101
+ setErrors(fieldErrors)
102
+ const firstMessage = Object.values(fieldErrors)[0]
103
+ if (firstMessage) flash(firstMessage, 'error')
104
+ return
105
+ }
106
+
107
+ const customFieldErrors = validateCustomFields(merged)
108
+ if (Object.keys(customFieldErrors).length) {
109
+ setErrors(customFieldErrors)
110
+ const firstMessage = Object.values(customFieldErrors)[0]
111
+ if (firstMessage) flash(firstMessage, 'error')
112
+ return
113
+ }
114
+
115
+ setErrors({})
116
+ setIsSubmitting(true)
117
+ try {
118
+ const data = parsed.data
119
+ const expectedCloseAt =
120
+ data.expectedCloseAt && data.expectedCloseAt.length
121
+ ? new Date(data.expectedCloseAt).toISOString()
122
+ : undefined
123
+ const payload: Record<string, unknown> = {
124
+ title: data.title,
125
+ status: data.status || undefined,
126
+ pipelineId: data.pipelineId || undefined,
127
+ pipelineStageId: data.pipelineStageId || undefined,
128
+ valueAmount: typeof data.valueAmount === 'number' ? data.valueAmount : undefined,
129
+ valueCurrency: data.valueCurrency || undefined,
130
+ probability: typeof data.probability === 'number' ? data.probability : undefined,
131
+ expectedCloseAt,
132
+ description: data.description && data.description.length ? data.description : undefined,
133
+ personIds: values.personIds.length ? values.personIds : undefined,
134
+ companyIds: values.companyIds.length ? values.companyIds : undefined,
135
+ }
136
+ const custom = collectNormalizedCustomValues(merged)
137
+ if (Object.keys(custom).length) payload.customFields = custom
138
+
139
+ await runMutation({
140
+ operation: () =>
141
+ createCrud('customers/deals', payload, {
142
+ errorMessage: tr('customers.deals.create.error', 'Failed to create deal.'),
143
+ }),
144
+ context: { formId: CONTEXT_ID, resourceKind: 'customers.deal', retryLastMutation },
145
+ mutationPayload: payload,
146
+ })
147
+ flash(tr('customers.people.detail.deals.success', 'Deal created.'), 'success')
148
+ router.push(returnTo)
149
+ } catch (err) {
150
+ const message = err instanceof Error ? err.message : tr('customers.deals.create.error', 'Failed to create deal.')
151
+ flash(message, 'error')
152
+ } finally {
153
+ setIsSubmitting(false)
154
+ }
155
+ }, [
156
+ collectNormalizedCustomValues,
157
+ customFieldsLoaded,
158
+ customValues,
159
+ isSubmitting,
160
+ retryLastMutation,
161
+ returnTo,
162
+ router,
163
+ runMutation,
164
+ tr,
165
+ validateCustomFields,
166
+ values,
167
+ ])
168
+
169
+ const onKeyDown = React.useCallback(
170
+ (event: React.KeyboardEvent<HTMLFormElement>) => {
171
+ if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
172
+ event.preventDefault()
173
+ handleSubmit()
174
+ }
175
+ },
176
+ [handleSubmit],
177
+ )
178
+
179
+ const onFormSubmit = React.useCallback(
180
+ (event: React.FormEvent<HTMLFormElement>) => {
181
+ event.preventDefault()
182
+ handleSubmit()
183
+ },
184
+ [handleSubmit],
185
+ )
186
+
187
+ const cancelLabel = tr('customers.deals.create.cancel', 'Cancel')
188
+ const submitLabel = tr('customers.deals.create.submit', 'Create deal')
189
+ const submitDisabled = !customFieldsLoaded
190
+
191
+ return (
192
+ <form className="mx-auto max-w-screen-2xl" onKeyDown={onKeyDown} onSubmit={onFormSubmit} noValidate>
193
+ <FormHeader
194
+ backHref={returnTo}
195
+ backLabel={tr('customers.deals.create.back', 'Back to deals')}
196
+ />
197
+
198
+ <div className="mt-6 grid grid-cols-1 gap-5 lg:grid-cols-[minmax(0,1fr)_330px]">
199
+ <div className="space-y-4">
200
+ <DealSectionCard
201
+ icon={Briefcase}
202
+ title={tr('customers.deals.create.title', 'Create deal')}
203
+ subtitle={tr('customers.deals.create.sections.details.subtitle', 'Core opportunity info')}
204
+ actions={
205
+ <>
206
+ <Button type="button" variant="outline" onClick={handleCancel} disabled={isSubmitting}>
207
+ {cancelLabel}
208
+ </Button>
209
+ <Button type="button" onClick={handleSubmit} disabled={isSubmitting || submitDisabled}>
210
+ {isSubmitting ? <Spinner className="size-4" /> : <Save className="size-4" />}
211
+ {submitLabel}
212
+ </Button>
213
+ </>
214
+ }
215
+ >
216
+ <DealDetailsFields
217
+ values={values}
218
+ errors={errors}
219
+ isSubmitting={isSubmitting}
220
+ patch={patch}
221
+ onPipelineChange={handlePipelineChange}
222
+ pipelines={pipelines}
223
+ stages={stages}
224
+ statusLabels={statusLabels}
225
+ tr={tr}
226
+ />
227
+ </DealSectionCard>
228
+
229
+ <DealAssociationsSection
230
+ tr={tr}
231
+ personIds={values.personIds}
232
+ companyIds={values.companyIds}
233
+ onPeopleChange={(next) => patch({ personIds: next })}
234
+ onCompaniesChange={(next) => patch({ companyIds: next })}
235
+ disabled={isSubmitting}
236
+ />
237
+ </div>
238
+
239
+ <DealCreateSidebar
240
+ tr={tr}
241
+ customValues={customValues}
242
+ onCustomChange={handleCustomChange}
243
+ errors={errors}
244
+ disabled={isSubmitting}
245
+ customCount={customCount}
246
+ manageHref={CUSTOM_FIELDS_MANAGE_HREF}
247
+ onCustomLoaded={handleCustomAttributesLoaded}
248
+ />
249
+ </div>
250
+ </form>
251
+ )
252
+ }
253
+
254
+ export default CreateDealForm