@open-mercato/core 0.6.3-develop.3651.1.d3a548a5f1 → 0.6.3-develop.3660.1.53b73d5aa4

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 (160) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/customer_settings/index.js +2 -0
  3. package/dist/generated/entities/customer_settings/index.js.map +2 -2
  4. package/dist/generated/entity-fields-registry.js +1 -0
  5. package/dist/generated/entity-fields-registry.js.map +2 -2
  6. package/dist/modules/customers/api/deals/aggregate/route.js +332 -0
  7. package/dist/modules/customers/api/deals/aggregate/route.js.map +7 -0
  8. package/dist/modules/customers/api/deals/bulk-update-owner/route.js +188 -0
  9. package/dist/modules/customers/api/deals/bulk-update-owner/route.js.map +7 -0
  10. package/dist/modules/customers/api/deals/bulk-update-stage/route.js +188 -0
  11. package/dist/modules/customers/api/deals/bulk-update-stage/route.js.map +7 -0
  12. package/dist/modules/customers/api/deals/route.js +255 -122
  13. package/dist/modules/customers/api/deals/route.js.map +2 -2
  14. package/dist/modules/customers/api/settings/stuck-threshold/route.js +169 -0
  15. package/dist/modules/customers/api/settings/stuck-threshold/route.js.map +7 -0
  16. package/dist/modules/customers/backend/config/customers/deals/page.js +181 -0
  17. package/dist/modules/customers/backend/config/customers/deals/page.js.map +7 -0
  18. package/dist/modules/customers/backend/config/customers/deals/page.meta.js +38 -0
  19. package/dist/modules/customers/backend/config/customers/deals/page.meta.js.map +7 -0
  20. package/dist/modules/customers/backend/customers/deals/create/page.js +17 -6
  21. package/dist/modules/customers/backend/customers/deals/create/page.js.map +2 -2
  22. package/dist/modules/customers/backend/customers/deals/page.js +2 -0
  23. package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
  24. package/dist/modules/customers/backend/customers/deals/pipeline/components/ActivityComposerDialog.js +217 -0
  25. package/dist/modules/customers/backend/customers/deals/pipeline/components/ActivityComposerDialog.js.map +7 -0
  26. package/dist/modules/customers/backend/customers/deals/pipeline/components/AddStageDialog.js +335 -0
  27. package/dist/modules/customers/backend/customers/deals/pipeline/components/AddStageDialog.js.map +7 -0
  28. package/dist/modules/customers/backend/customers/deals/pipeline/components/AddStageLane.js +29 -0
  29. package/dist/modules/customers/backend/customers/deals/pipeline/components/AddStageLane.js.map +7 -0
  30. package/dist/modules/customers/backend/customers/deals/pipeline/components/BulkActionsBar.js +160 -0
  31. package/dist/modules/customers/backend/customers/deals/pipeline/components/BulkActionsBar.js.map +7 -0
  32. package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.js +157 -0
  33. package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.js.map +7 -0
  34. package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeStageDialog.js +114 -0
  35. package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeStageDialog.js.map +7 -0
  36. package/dist/modules/customers/backend/customers/deals/pipeline/components/ChipButton.js +36 -0
  37. package/dist/modules/customers/backend/customers/deals/pipeline/components/ChipButton.js.map +7 -0
  38. package/dist/modules/customers/backend/customers/deals/pipeline/components/CloseDateFilterPopover.js +199 -0
  39. package/dist/modules/customers/backend/customers/deals/pipeline/components/CloseDateFilterPopover.js.map +7 -0
  40. package/dist/modules/customers/backend/customers/deals/pipeline/components/CurrencyBreakdownTable.js +71 -0
  41. package/dist/modules/customers/backend/customers/deals/pipeline/components/CurrencyBreakdownTable.js.map +7 -0
  42. package/dist/modules/customers/backend/customers/deals/pipeline/components/CurrencyFilterPopover.js +233 -0
  43. package/dist/modules/customers/backend/customers/deals/pipeline/components/CurrencyFilterPopover.js.map +7 -0
  44. package/dist/modules/customers/backend/customers/deals/pipeline/components/CustomizeViewDialog.js +126 -0
  45. package/dist/modules/customers/backend/customers/deals/pipeline/components/CustomizeViewDialog.js.map +7 -0
  46. package/dist/modules/customers/backend/customers/deals/pipeline/components/DashedTileButton.js +31 -0
  47. package/dist/modules/customers/backend/customers/deals/pipeline/components/DashedTileButton.js.map +7 -0
  48. package/dist/modules/customers/backend/customers/deals/pipeline/components/DealCard.js +389 -0
  49. package/dist/modules/customers/backend/customers/deals/pipeline/components/DealCard.js.map +7 -0
  50. package/dist/modules/customers/backend/customers/deals/pipeline/components/DealCardMenu.js +181 -0
  51. package/dist/modules/customers/backend/customers/deals/pipeline/components/DealCardMenu.js.map +7 -0
  52. package/dist/modules/customers/backend/customers/deals/pipeline/components/EntityFilterPopover.js +199 -0
  53. package/dist/modules/customers/backend/customers/deals/pipeline/components/EntityFilterPopover.js.map +7 -0
  54. package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterBarRow.js +44 -0
  55. package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterBarRow.js.map +7 -0
  56. package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterPopoverShell.js +70 -0
  57. package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterPopoverShell.js.map +7 -0
  58. package/dist/modules/customers/backend/customers/deals/pipeline/components/Lane.js +278 -0
  59. package/dist/modules/customers/backend/customers/deals/pipeline/components/Lane.js.map +7 -0
  60. package/dist/modules/customers/backend/customers/deals/pipeline/components/LaneCurrencyBreakdown.js +135 -0
  61. package/dist/modules/customers/backend/customers/deals/pipeline/components/LaneCurrencyBreakdown.js.map +7 -0
  62. package/dist/modules/customers/backend/customers/deals/pipeline/components/LaneResizeHandle.js +83 -0
  63. package/dist/modules/customers/backend/customers/deals/pipeline/components/LaneResizeHandle.js.map +7 -0
  64. package/dist/modules/customers/backend/customers/deals/pipeline/components/PipelineFilterPopover.js +155 -0
  65. package/dist/modules/customers/backend/customers/deals/pipeline/components/PipelineFilterPopover.js.map +7 -0
  66. package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js +318 -0
  67. package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js.map +7 -0
  68. package/dist/modules/customers/backend/customers/deals/pipeline/components/SortByPopover.js +138 -0
  69. package/dist/modules/customers/backend/customers/deals/pipeline/components/SortByPopover.js.map +7 -0
  70. package/dist/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.js +126 -0
  71. package/dist/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.js.map +7 -0
  72. package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js +71 -0
  73. package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js.map +7 -0
  74. package/dist/modules/customers/backend/customers/deals/pipeline/components/constants.js +7 -0
  75. package/dist/modules/customers/backend/customers/deals/pipeline/components/constants.js.map +7 -0
  76. package/dist/modules/customers/backend/customers/deals/pipeline/page.js +2330 -415
  77. package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +3 -3
  78. package/dist/modules/customers/commands/deals.js +6 -0
  79. package/dist/modules/customers/commands/deals.js.map +2 -2
  80. package/dist/modules/customers/commands/pipeline-stages.js +48 -13
  81. package/dist/modules/customers/commands/pipeline-stages.js.map +3 -3
  82. package/dist/modules/customers/commands/settings.js +45 -5
  83. package/dist/modules/customers/commands/settings.js.map +2 -2
  84. package/dist/modules/customers/data/enrichers.js +130 -1
  85. package/dist/modules/customers/data/enrichers.js.map +2 -2
  86. package/dist/modules/customers/data/entities.js +4 -0
  87. package/dist/modules/customers/data/entities.js.map +2 -2
  88. package/dist/modules/customers/data/validators.js +29 -2
  89. package/dist/modules/customers/data/validators.js.map +2 -2
  90. package/dist/modules/customers/lib/bulkDeals.js +150 -0
  91. package/dist/modules/customers/lib/bulkDeals.js.map +7 -0
  92. package/dist/modules/customers/lib/kysely.js +12 -0
  93. package/dist/modules/customers/lib/kysely.js.map +7 -0
  94. package/dist/modules/customers/lib/stuckDeals.js +32 -0
  95. package/dist/modules/customers/lib/stuckDeals.js.map +7 -0
  96. package/dist/modules/customers/migrations/Migration20260513203311_customers.js +13 -0
  97. package/dist/modules/customers/migrations/Migration20260513203311_customers.js.map +7 -0
  98. package/dist/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.js +52 -0
  99. package/dist/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.js.map +7 -0
  100. package/dist/modules/customers/workers/deals-bulk-update-owner.js +41 -0
  101. package/dist/modules/customers/workers/deals-bulk-update-owner.js.map +7 -0
  102. package/dist/modules/customers/workers/deals-bulk-update-stage.js +41 -0
  103. package/dist/modules/customers/workers/deals-bulk-update-stage.js.map +7 -0
  104. package/generated/entities/customer_settings/index.ts +1 -0
  105. package/generated/entity-fields-registry.ts +1 -0
  106. package/package.json +7 -7
  107. package/src/modules/customers/api/deals/aggregate/route.ts +438 -0
  108. package/src/modules/customers/api/deals/bulk-update-owner/route.ts +216 -0
  109. package/src/modules/customers/api/deals/bulk-update-stage/route.ts +228 -0
  110. package/src/modules/customers/api/deals/route.ts +330 -135
  111. package/src/modules/customers/api/settings/stuck-threshold/route.ts +195 -0
  112. package/src/modules/customers/backend/config/customers/deals/page.meta.ts +36 -0
  113. package/src/modules/customers/backend/config/customers/deals/page.tsx +205 -0
  114. package/src/modules/customers/backend/customers/deals/create/page.tsx +25 -6
  115. package/src/modules/customers/backend/customers/deals/page.tsx +2 -0
  116. package/src/modules/customers/backend/customers/deals/pipeline/components/ActivityComposerDialog.tsx +277 -0
  117. package/src/modules/customers/backend/customers/deals/pipeline/components/AddStageDialog.tsx +425 -0
  118. package/src/modules/customers/backend/customers/deals/pipeline/components/AddStageLane.tsx +31 -0
  119. package/src/modules/customers/backend/customers/deals/pipeline/components/BulkActionsBar.tsx +171 -0
  120. package/src/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.tsx +196 -0
  121. package/src/modules/customers/backend/customers/deals/pipeline/components/ChangeStageDialog.tsx +138 -0
  122. package/src/modules/customers/backend/customers/deals/pipeline/components/ChipButton.tsx +47 -0
  123. package/src/modules/customers/backend/customers/deals/pipeline/components/CloseDateFilterPopover.tsx +232 -0
  124. package/src/modules/customers/backend/customers/deals/pipeline/components/CurrencyBreakdownTable.tsx +110 -0
  125. package/src/modules/customers/backend/customers/deals/pipeline/components/CurrencyFilterPopover.tsx +310 -0
  126. package/src/modules/customers/backend/customers/deals/pipeline/components/CustomizeViewDialog.tsx +169 -0
  127. package/src/modules/customers/backend/customers/deals/pipeline/components/DashedTileButton.tsx +54 -0
  128. package/src/modules/customers/backend/customers/deals/pipeline/components/DealCard.tsx +562 -0
  129. package/src/modules/customers/backend/customers/deals/pipeline/components/DealCardMenu.tsx +214 -0
  130. package/src/modules/customers/backend/customers/deals/pipeline/components/EntityFilterPopover.tsx +257 -0
  131. package/src/modules/customers/backend/customers/deals/pipeline/components/FilterBarRow.tsx +62 -0
  132. package/src/modules/customers/backend/customers/deals/pipeline/components/FilterPopoverShell.tsx +101 -0
  133. package/src/modules/customers/backend/customers/deals/pipeline/components/Lane.tsx +378 -0
  134. package/src/modules/customers/backend/customers/deals/pipeline/components/LaneCurrencyBreakdown.tsx +192 -0
  135. package/src/modules/customers/backend/customers/deals/pipeline/components/LaneResizeHandle.tsx +107 -0
  136. package/src/modules/customers/backend/customers/deals/pipeline/components/PipelineFilterPopover.tsx +196 -0
  137. package/src/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.tsx +408 -0
  138. package/src/modules/customers/backend/customers/deals/pipeline/components/SortByPopover.tsx +164 -0
  139. package/src/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.tsx +171 -0
  140. package/src/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.tsx +84 -0
  141. package/src/modules/customers/backend/customers/deals/pipeline/components/constants.ts +16 -0
  142. package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +2841 -550
  143. package/src/modules/customers/commands/deals.ts +19 -0
  144. package/src/modules/customers/commands/pipeline-stages.ts +68 -13
  145. package/src/modules/customers/commands/settings.ts +64 -5
  146. package/src/modules/customers/data/enrichers.ts +209 -2
  147. package/src/modules/customers/data/entities.ts +4 -1
  148. package/src/modules/customers/data/validators.ts +36 -2
  149. package/src/modules/customers/i18n/de.json +244 -0
  150. package/src/modules/customers/i18n/en.json +244 -0
  151. package/src/modules/customers/i18n/es.json +244 -0
  152. package/src/modules/customers/i18n/pl.json +244 -0
  153. package/src/modules/customers/lib/bulkDeals.ts +231 -0
  154. package/src/modules/customers/lib/kysely.ts +60 -0
  155. package/src/modules/customers/lib/stuckDeals.ts +88 -0
  156. package/src/modules/customers/migrations/.snapshot-open-mercato.json +16 -0
  157. package/src/modules/customers/migrations/Migration20260513203311_customers.ts +13 -0
  158. package/src/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.ts +71 -0
  159. package/src/modules/customers/workers/deals-bulk-update-owner.ts +46 -0
  160. package/src/modules/customers/workers/deals-bulk-update-stage.ts +46 -0
@@ -0,0 +1,217 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { Mail, Phone, StickyNote } from "lucide-react";
5
+ import { z } from "zod";
6
+ import {
7
+ Dialog,
8
+ DialogContent,
9
+ DialogDescription,
10
+ DialogHeader,
11
+ DialogTitle
12
+ } from "@open-mercato/ui/primitives/dialog";
13
+ import { CrudForm } from "@open-mercato/ui/backend/CrudForm";
14
+ import { createCrud } from "@open-mercato/ui/backend/utils/crud";
15
+ import { createCrudFormError } from "@open-mercato/ui/backend/utils/serverErrors";
16
+ import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
17
+ import { flash } from "@open-mercato/ui/backend/FlashMessages";
18
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
19
+ import { translateWithFallback } from "@open-mercato/shared/lib/i18n/translate";
20
+ const ACTIVITY_CONTEXT_ID = "customers-deals-kanban:activity-composer";
21
+ const TYPE_META = {
22
+ call: {
23
+ titleKey: "customers.deals.kanban.activityComposer.call",
24
+ titleFallback: "Log a call",
25
+ interactionType: "call",
26
+ icon: Phone
27
+ },
28
+ email: {
29
+ titleKey: "customers.deals.kanban.activityComposer.email",
30
+ titleFallback: "Send email",
31
+ interactionType: "email",
32
+ icon: Mail
33
+ },
34
+ note: {
35
+ titleKey: "customers.deals.kanban.activityComposer.note",
36
+ titleFallback: "Add a note",
37
+ interactionType: "note",
38
+ icon: StickyNote
39
+ }
40
+ };
41
+ function ActivityComposerDialog({
42
+ open,
43
+ context,
44
+ onClose,
45
+ onCreated
46
+ }) {
47
+ const t = useT();
48
+ const [formInstanceKey, setFormInstanceKey] = React.useState(0);
49
+ React.useEffect(() => {
50
+ if (open) setFormInstanceKey((current) => current + 1);
51
+ }, [open]);
52
+ const { runMutation, retryLastMutation } = useGuardedMutation({
53
+ contextId: ACTIVITY_CONTEXT_ID,
54
+ blockedMessage: translateWithFallback(t, "ui.forms.flash.saveBlocked", "Save blocked by validation")
55
+ });
56
+ const eitherRequiredMessage = React.useMemo(
57
+ () => context?.type === "email" ? translateWithFallback(
58
+ t,
59
+ "customers.deals.kanban.activityComposer.required.email",
60
+ "Add a subject or body before saving."
61
+ ) : translateWithFallback(
62
+ t,
63
+ "customers.deals.kanban.activityComposer.required.titleOrBody",
64
+ "Add a title or body before saving."
65
+ ),
66
+ [context?.type, t]
67
+ );
68
+ const formSchema = React.useMemo(
69
+ () => z.object({
70
+ title: z.string().optional(),
71
+ phoneNumber: z.string().optional(),
72
+ body: z.string().optional()
73
+ }).refine(
74
+ (values) => values.title?.trim().length || values.body?.trim().length ? true : false,
75
+ {
76
+ message: eitherRequiredMessage,
77
+ path: ["title"]
78
+ }
79
+ ),
80
+ [eitherRequiredMessage]
81
+ );
82
+ const initialValues = React.useMemo(
83
+ () => ({ title: "", phoneNumber: "", body: "" }),
84
+ []
85
+ );
86
+ const isCall = context?.type === "call";
87
+ const fields = React.useMemo(() => {
88
+ const list = [
89
+ {
90
+ // CrudForm auto-focuses the first field on mount; explicit `autoFocus` is rejected
91
+ // by CrudBuiltinField's type and would shadow CrudForm's own focus management.
92
+ id: "title",
93
+ label: translateWithFallback(t, "customers.deals.kanban.activityComposer.title", "Subject"),
94
+ type: "text",
95
+ placeholder: translateWithFallback(
96
+ t,
97
+ "customers.deals.kanban.activityComposer.title.placeholder",
98
+ "Short summary (optional)"
99
+ )
100
+ }
101
+ ];
102
+ if (isCall) {
103
+ list.push({
104
+ id: "phoneNumber",
105
+ label: translateWithFallback(t, "customers.deals.kanban.activityComposer.phone", "Phone number"),
106
+ type: "text",
107
+ placeholder: "+48 ..."
108
+ });
109
+ }
110
+ list.push({
111
+ id: "body",
112
+ label: translateWithFallback(t, "customers.deals.kanban.activityComposer.body", "Notes"),
113
+ type: "textarea",
114
+ rows: 4,
115
+ placeholder: translateWithFallback(
116
+ t,
117
+ "customers.deals.kanban.activityComposer.body.placeholder",
118
+ "What happened? Markdown supported."
119
+ )
120
+ });
121
+ return list;
122
+ }, [isCall, t]);
123
+ const handleSubmit = React.useCallback(
124
+ async (values) => {
125
+ if (!context) return;
126
+ const trimmedTitle = (values.title ?? "").trim();
127
+ const trimmedBody = (values.body ?? "").trim();
128
+ if (!trimmedTitle.length && !trimmedBody.length) {
129
+ throw createCrudFormError(eitherRequiredMessage, { title: eitherRequiredMessage });
130
+ }
131
+ const meta2 = TYPE_META[context.type];
132
+ const payload = {
133
+ entityId: context.entityId,
134
+ interactionType: meta2.interactionType,
135
+ dealId: context.dealId,
136
+ status: "planned"
137
+ };
138
+ if (trimmedTitle.length) payload.title = trimmedTitle;
139
+ if (trimmedBody.length) payload.body = trimmedBody;
140
+ if (context.type === "call" && (values.phoneNumber ?? "").trim().length) {
141
+ payload.phoneNumber = values.phoneNumber.trim();
142
+ }
143
+ const operation = () => createCrud("customers/interactions", payload, {
144
+ errorMessage: translateWithFallback(
145
+ t,
146
+ "customers.deals.kanban.activityComposer.error",
147
+ "Failed to save activity."
148
+ )
149
+ });
150
+ await runMutation({
151
+ operation,
152
+ context: {
153
+ formId: ACTIVITY_CONTEXT_ID,
154
+ resourceKind: "customers.interaction",
155
+ resourceId: context.dealId,
156
+ retryLastMutation
157
+ }
158
+ });
159
+ flash(
160
+ translateWithFallback(t, "customers.deals.kanban.activityComposer.success", "Activity saved."),
161
+ "success"
162
+ );
163
+ onCreated();
164
+ onClose();
165
+ },
166
+ [context, eitherRequiredMessage, onClose, onCreated, retryLastMutation, runMutation, t]
167
+ );
168
+ if (!context) return null;
169
+ const meta = TYPE_META[context.type];
170
+ const Icon = meta.icon;
171
+ const dialogTitle = translateWithFallback(t, meta.titleKey, meta.titleFallback);
172
+ return /* @__PURE__ */ jsx(
173
+ Dialog,
174
+ {
175
+ open,
176
+ onOpenChange: (next) => {
177
+ if (!next) onClose();
178
+ },
179
+ children: /* @__PURE__ */ jsxs(DialogContent, { children: [
180
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [
181
+ /* @__PURE__ */ jsxs(DialogTitle, { className: "flex items-center gap-2", children: [
182
+ /* @__PURE__ */ jsx(Icon, { className: "size-4", "aria-hidden": "true" }),
183
+ dialogTitle
184
+ ] }),
185
+ /* @__PURE__ */ jsx(DialogDescription, { children: translateWithFallback(
186
+ t,
187
+ "customers.deals.kanban.activityComposer.context",
188
+ "Deal: {title}",
189
+ { title: context.dealTitle }
190
+ ) })
191
+ ] }),
192
+ /* @__PURE__ */ jsx(
193
+ CrudForm,
194
+ {
195
+ embedded: true,
196
+ fields,
197
+ initialValues,
198
+ schema: formSchema,
199
+ submitLabel: translateWithFallback(
200
+ t,
201
+ "customers.deals.kanban.activityComposer.submit",
202
+ "Save activity"
203
+ ),
204
+ onSubmit: handleSubmit
205
+ },
206
+ `${context.dealId}:${context.type}:${formInstanceKey}`
207
+ )
208
+ ] })
209
+ }
210
+ );
211
+ }
212
+ var ActivityComposerDialog_default = ActivityComposerDialog;
213
+ export {
214
+ ActivityComposerDialog,
215
+ ActivityComposerDialog_default as default
216
+ };
217
+ //# sourceMappingURL=ActivityComposerDialog.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../../../src/modules/customers/backend/customers/deals/pipeline/components/ActivityComposerDialog.tsx"],
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Mail, Phone, StickyNote, type LucideIcon } from 'lucide-react'\nimport { z } from 'zod'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'\nimport { createCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'\n\nexport type ActivityComposerType = 'call' | 'email' | 'note'\n\nexport type ActivityComposerContext = {\n dealId: string\n dealTitle: string\n type: ActivityComposerType\n entityId: string\n}\n\ntype ActivityComposerDialogProps = {\n open: boolean\n context: ActivityComposerContext | null\n onClose: () => void\n onCreated: () => void\n}\n\ntype ActivityComposerFormValues = {\n title: string\n phoneNumber: string\n body: string\n}\n\nconst ACTIVITY_CONTEXT_ID = 'customers-deals-kanban:activity-composer'\n\nconst TYPE_META: Record<\n ActivityComposerType,\n {\n titleKey: string\n titleFallback: string\n interactionType: string\n icon: LucideIcon\n }\n> = {\n call: {\n titleKey: 'customers.deals.kanban.activityComposer.call',\n titleFallback: 'Log a call',\n interactionType: 'call',\n icon: Phone,\n },\n email: {\n titleKey: 'customers.deals.kanban.activityComposer.email',\n titleFallback: 'Send email',\n interactionType: 'email',\n icon: Mail,\n },\n note: {\n titleKey: 'customers.deals.kanban.activityComposer.note',\n titleFallback: 'Add a note',\n interactionType: 'note',\n icon: StickyNote,\n },\n}\n\nexport function ActivityComposerDialog({\n open,\n context,\n onClose,\n onCreated,\n}: ActivityComposerDialogProps): React.ReactElement | null {\n const t = useT()\n // Re-mount CrudForm whenever the dialog opens, or when the deal/type changes \u2014 these\n // identify what we're logging activity FOR, and CrudForm's internal state must reset.\n const [formInstanceKey, setFormInstanceKey] = React.useState(0)\n React.useEffect(() => {\n if (open) setFormInstanceKey((current) => current + 1)\n }, [open])\n\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n resourceId: string\n retryLastMutation: () => Promise<boolean>\n }>({\n contextId: ACTIVITY_CONTEXT_ID,\n blockedMessage: translateWithFallback(t, 'ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n // Email mode shows the field as \"Subject\" (see TYPE_META + the field config below), so\n // the refine error must reference \"subject\" instead of \"title\" to stay coherent. Call\n // and Note modes keep the \"title or body\" copy that matches their Subject-labelled field.\n const eitherRequiredMessage = React.useMemo(\n () =>\n context?.type === 'email'\n ? translateWithFallback(\n t,\n 'customers.deals.kanban.activityComposer.required.email',\n 'Add a subject or body before saving.',\n )\n : translateWithFallback(\n t,\n 'customers.deals.kanban.activityComposer.required.titleOrBody',\n 'Add a title or body before saving.',\n ),\n [context?.type, t],\n )\n\n const formSchema = React.useMemo(\n () =>\n z\n .object({\n title: z.string().optional(),\n phoneNumber: z.string().optional(),\n body: z.string().optional(),\n })\n // Activity must carry at least a title OR a body \u2014 purely empty saves are\n // useless and they previously surfaced as a confusing inline error.\n .refine(\n (values) => (values.title?.trim().length || values.body?.trim().length) ? true : false,\n {\n message: eitherRequiredMessage,\n path: ['title'],\n },\n ),\n [eitherRequiredMessage],\n ) as unknown as z.ZodType<ActivityComposerFormValues>\n\n const initialValues = React.useMemo<Partial<ActivityComposerFormValues>>(\n () => ({ title: '', phoneNumber: '', body: '' }),\n [],\n )\n\n const isCall = context?.type === 'call'\n\n const fields = React.useMemo<CrudField[]>(() => {\n const list: CrudField[] = [\n {\n // CrudForm auto-focuses the first field on mount; explicit `autoFocus` is rejected\n // by CrudBuiltinField's type and would shadow CrudForm's own focus management.\n id: 'title',\n label: translateWithFallback(t, 'customers.deals.kanban.activityComposer.title', 'Subject'),\n type: 'text',\n placeholder: translateWithFallback(\n t,\n 'customers.deals.kanban.activityComposer.title.placeholder',\n 'Short summary (optional)',\n ),\n },\n ]\n if (isCall) {\n list.push({\n id: 'phoneNumber',\n label: translateWithFallback(t, 'customers.deals.kanban.activityComposer.phone', 'Phone number'),\n type: 'text',\n placeholder: '+48 ...',\n })\n }\n list.push({\n id: 'body',\n label: translateWithFallback(t, 'customers.deals.kanban.activityComposer.body', 'Notes'),\n type: 'textarea',\n rows: 4,\n placeholder: translateWithFallback(\n t,\n 'customers.deals.kanban.activityComposer.body.placeholder',\n 'What happened? Markdown supported.',\n ),\n })\n return list\n }, [isCall, t])\n\n const handleSubmit = React.useCallback(\n async (values: ActivityComposerFormValues) => {\n if (!context) return\n const trimmedTitle = (values.title ?? '').trim()\n const trimmedBody = (values.body ?? '').trim()\n if (!trimmedTitle.length && !trimmedBody.length) {\n // Zod refine already handles this, but we surface a field-level error too so the\n // inline indicator stays anchored on the Subject input.\n throw createCrudFormError(eitherRequiredMessage, { title: eitherRequiredMessage })\n }\n const meta = TYPE_META[context.type]\n const payload: Record<string, unknown> = {\n entityId: context.entityId,\n interactionType: meta.interactionType,\n dealId: context.dealId,\n status: 'planned',\n }\n if (trimmedTitle.length) payload.title = trimmedTitle\n if (trimmedBody.length) payload.body = trimmedBody\n if (context.type === 'call' && (values.phoneNumber ?? '').trim().length) {\n payload.phoneNumber = values.phoneNumber!.trim()\n }\n const operation = () =>\n createCrud('customers/interactions', payload, {\n errorMessage: translateWithFallback(\n t,\n 'customers.deals.kanban.activityComposer.error',\n 'Failed to save activity.',\n ),\n })\n await runMutation({\n operation,\n context: {\n formId: ACTIVITY_CONTEXT_ID,\n resourceKind: 'customers.interaction',\n resourceId: context.dealId,\n retryLastMutation,\n },\n })\n flash(\n translateWithFallback(t, 'customers.deals.kanban.activityComposer.success', 'Activity saved.'),\n 'success',\n )\n onCreated()\n onClose()\n },\n [context, eitherRequiredMessage, onClose, onCreated, retryLastMutation, runMutation, t],\n )\n\n if (!context) return null\n const meta = TYPE_META[context.type]\n const Icon = meta.icon\n const dialogTitle = translateWithFallback(t, meta.titleKey, meta.titleFallback)\n\n return (\n <Dialog\n open={open}\n onOpenChange={(next) => {\n if (!next) onClose()\n }}\n >\n <DialogContent>\n <DialogHeader>\n <DialogTitle className=\"flex items-center gap-2\">\n <Icon className=\"size-4\" aria-hidden=\"true\" />\n {dialogTitle}\n </DialogTitle>\n <DialogDescription>\n {translateWithFallback(\n t,\n 'customers.deals.kanban.activityComposer.context',\n 'Deal: {title}',\n { title: context.dealTitle },\n )}\n </DialogDescription>\n </DialogHeader>\n\n <CrudForm<ActivityComposerFormValues>\n key={`${context.dealId}:${context.type}:${formInstanceKey}`}\n embedded\n fields={fields}\n initialValues={initialValues}\n schema={formSchema}\n submitLabel={translateWithFallback(\n t,\n 'customers.deals.kanban.activityComposer.submit',\n 'Save activity',\n )}\n onSubmit={handleSubmit}\n />\n </DialogContent>\n </Dialog>\n )\n}\n\nexport default ActivityComposerDialog\n"],
5
+ "mappings": "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",
6
+ "names": ["meta"]
7
+ }
@@ -0,0 +1,335 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { z } from "zod";
5
+ import {
6
+ Dialog,
7
+ DialogContent,
8
+ DialogDescription,
9
+ DialogHeader,
10
+ DialogTitle
11
+ } from "@open-mercato/ui/primitives/dialog";
12
+ import { Button } from "@open-mercato/ui/primitives/button";
13
+ import { CrudForm } from "@open-mercato/ui/backend/CrudForm";
14
+ import { createCrud } from "@open-mercato/ui/backend/utils/crud";
15
+ import { createCrudFormError } from "@open-mercato/ui/backend/utils/serverErrors";
16
+ import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
17
+ import { flash } from "@open-mercato/ui/backend/FlashMessages";
18
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
19
+ import { translateWithFallback } from "@open-mercato/shared/lib/i18n/translate";
20
+ const ADD_STAGE_CONTEXT_ID = "customers-deals-kanban:add-stage";
21
+ const TONE_OPTIONS = [
22
+ {
23
+ value: "",
24
+ swatchClass: "border border-dashed border-border",
25
+ labelKey: "customers.deals.kanban.addStage.color.none",
26
+ labelFallback: "No color"
27
+ },
28
+ {
29
+ value: "success",
30
+ swatchClass: "bg-status-success-icon",
31
+ labelKey: "customers.deals.kanban.addStage.color.success",
32
+ labelFallback: "Green"
33
+ },
34
+ {
35
+ value: "warning",
36
+ swatchClass: "bg-status-warning-icon",
37
+ labelKey: "customers.deals.kanban.addStage.color.warning",
38
+ labelFallback: "Amber"
39
+ },
40
+ {
41
+ value: "info",
42
+ swatchClass: "bg-status-info-icon",
43
+ labelKey: "customers.deals.kanban.addStage.color.info",
44
+ labelFallback: "Blue"
45
+ },
46
+ {
47
+ value: "error",
48
+ swatchClass: "bg-status-error-icon",
49
+ labelKey: "customers.deals.kanban.addStage.color.error",
50
+ labelFallback: "Red"
51
+ },
52
+ {
53
+ value: "neutral",
54
+ swatchClass: "bg-status-neutral-icon",
55
+ labelKey: "customers.deals.kanban.addStage.color.neutral",
56
+ labelFallback: "Gray"
57
+ },
58
+ {
59
+ value: "brand",
60
+ swatchClass: "bg-brand-violet",
61
+ labelKey: "customers.deals.kanban.addStage.color.brand",
62
+ labelFallback: "Violet"
63
+ },
64
+ {
65
+ value: "pink",
66
+ swatchClass: "bg-status-pink-icon",
67
+ labelKey: "customers.deals.kanban.addStage.color.pink",
68
+ labelFallback: "Pink"
69
+ }
70
+ ];
71
+ const POSITION_END = "end";
72
+ const POSITION_START = "start";
73
+ const POSITION_AFTER_PREFIX = "after:";
74
+ function resolveInsertOrder(position, existingStages) {
75
+ if (position === POSITION_END || position === "") return void 0;
76
+ if (position === POSITION_START) return 0;
77
+ if (position.startsWith(POSITION_AFTER_PREFIX)) {
78
+ const afterId = position.slice(POSITION_AFTER_PREFIX.length);
79
+ const idx = existingStages.findIndex((stage) => stage.id === afterId);
80
+ if (idx < 0) return void 0;
81
+ return existingStages[idx].order + 1;
82
+ }
83
+ return void 0;
84
+ }
85
+ function AddStageDialog({
86
+ open,
87
+ context,
88
+ onClose,
89
+ onCreated
90
+ }) {
91
+ const t = useT();
92
+ const [formInstanceKey, setFormInstanceKey] = React.useState(0);
93
+ React.useEffect(() => {
94
+ if (open) setFormInstanceKey((current) => current + 1);
95
+ }, [open]);
96
+ const { runMutation, retryLastMutation } = useGuardedMutation({
97
+ contextId: ADD_STAGE_CONTEXT_ID,
98
+ blockedMessage: translateWithFallback(t, "ui.forms.flash.saveBlocked", "Save blocked by validation")
99
+ });
100
+ const labelRequiredMessage = translateWithFallback(
101
+ t,
102
+ "customers.deals.kanban.addStage.label.required",
103
+ "Stage name is required."
104
+ );
105
+ const formSchema = React.useMemo(
106
+ () => z.object({
107
+ label: z.string().trim().min(1, labelRequiredMessage),
108
+ color: z.string().optional(),
109
+ position: z.string().optional()
110
+ }),
111
+ [labelRequiredMessage]
112
+ );
113
+ const positionOptions = React.useMemo(() => {
114
+ const existingStages = context?.existingStages ?? [];
115
+ const opts = [
116
+ {
117
+ value: POSITION_END,
118
+ label: translateWithFallback(
119
+ t,
120
+ "customers.deals.kanban.addStage.position.end",
121
+ "At the end (default)"
122
+ )
123
+ },
124
+ {
125
+ value: POSITION_START,
126
+ label: translateWithFallback(
127
+ t,
128
+ "customers.deals.kanban.addStage.position.start",
129
+ "At the start"
130
+ )
131
+ }
132
+ ];
133
+ for (const stage of existingStages) {
134
+ opts.push({
135
+ value: `${POSITION_AFTER_PREFIX}${stage.id}`,
136
+ label: translateWithFallback(
137
+ t,
138
+ "customers.deals.kanban.addStage.position.after",
139
+ "After {label}",
140
+ { label: stage.label }
141
+ )
142
+ });
143
+ }
144
+ return opts;
145
+ }, [context?.existingStages, t]);
146
+ const fields = React.useMemo(
147
+ () => [
148
+ {
149
+ // CrudForm auto-focuses the first field of the first group on mount, so we don't
150
+ // need an explicit `autoFocus: true` here — and adding one would be a type error
151
+ // because `autoFocus` is only valid on CrudCustomFieldRenderProps.
152
+ id: "label",
153
+ label: translateWithFallback(t, "customers.deals.kanban.addStage.label", "Stage name"),
154
+ type: "text",
155
+ required: true,
156
+ placeholder: translateWithFallback(
157
+ t,
158
+ "customers.deals.kanban.addStage.label.placeholder",
159
+ "e.g. Discovery"
160
+ )
161
+ },
162
+ {
163
+ // Custom inline radio-style list instead of a Radix Select — when the dialog sits
164
+ // near the bottom of the viewport, Radix's Select dropdown clips to the available
165
+ // vertical space (`--radix-select-content-available-height`), which can leave only
166
+ // 1–2 visible options and hide the rest behind a small scroll affordance the
167
+ // operator doesn't notice. Rendering the picker inline inside the dialog body
168
+ // gives it a deterministic max-height + clearly scrollable area, and "After {label}"
169
+ // entries are always visible without portal clipping.
170
+ id: "position",
171
+ label: translateWithFallback(
172
+ t,
173
+ "customers.deals.kanban.addStage.position",
174
+ "Position in pipeline"
175
+ ),
176
+ type: "custom",
177
+ description: translateWithFallback(
178
+ t,
179
+ "customers.deals.kanban.addStage.position.help",
180
+ "Choose where the new stage appears on the kanban. Existing stages after the chosen position shift one step to the right."
181
+ ),
182
+ component: ({ value, setValue }) => {
183
+ const current = typeof value === "string" && value.length > 0 ? value : POSITION_END;
184
+ return /* @__PURE__ */ jsx("div", { className: "flex max-h-52 flex-col gap-1 overflow-y-auto rounded-md border border-input bg-card p-1", children: positionOptions.map((option) => {
185
+ const selected = current === option.value;
186
+ return /* @__PURE__ */ jsxs(
187
+ Button,
188
+ {
189
+ variant: "ghost",
190
+ size: "sm",
191
+ type: "button",
192
+ onClick: () => setValue(option.value),
193
+ "aria-pressed": selected,
194
+ className: `flex items-center justify-between gap-2 rounded-sm px-3 py-2 text-left text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${selected ? "bg-muted font-medium text-foreground" : "text-muted-foreground hover:bg-muted/60 hover:text-foreground"}`,
195
+ children: [
196
+ /* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: option.label }),
197
+ /* @__PURE__ */ jsx(
198
+ "span",
199
+ {
200
+ className: `inline-flex size-4 shrink-0 items-center justify-center rounded-full border ${selected ? "border-primary bg-primary" : "border-input"}`,
201
+ "aria-hidden": "true",
202
+ children: selected ? /* @__PURE__ */ jsx("span", { className: "size-1.5 rounded-full bg-primary-foreground" }) : null
203
+ }
204
+ )
205
+ ]
206
+ },
207
+ option.value
208
+ );
209
+ }) });
210
+ }
211
+ },
212
+ {
213
+ // Custom render — built-in `select` can't render a swatch alongside each label.
214
+ // Each swatch uses the same DS class the lane bar renders, so the picker is a
215
+ // true preview of the kanban appearance instead of a separate hex translation.
216
+ id: "color",
217
+ label: translateWithFallback(t, "customers.deals.kanban.addStage.color", "Color"),
218
+ type: "custom",
219
+ component: ({ value, setValue }) => {
220
+ const current = typeof value === "string" ? value : "";
221
+ return /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: TONE_OPTIONS.map((option) => {
222
+ const selected = current === option.value;
223
+ const label = translateWithFallback(t, option.labelKey, option.labelFallback);
224
+ return /* @__PURE__ */ jsxs(
225
+ Button,
226
+ {
227
+ variant: "outline",
228
+ size: "sm",
229
+ type: "button",
230
+ onClick: () => setValue(option.value),
231
+ "aria-pressed": selected,
232
+ title: label,
233
+ className: `inline-flex items-center gap-2 rounded-md border px-3 py-1.5 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${selected ? "border-primary bg-muted text-foreground" : "border-input bg-card text-muted-foreground hover:bg-muted hover:text-foreground"}`,
234
+ children: [
235
+ /* @__PURE__ */ jsx(
236
+ "span",
237
+ {
238
+ className: `size-3 rounded-sm ${option.swatchClass}`,
239
+ "aria-hidden": "true"
240
+ }
241
+ ),
242
+ /* @__PURE__ */ jsx("span", { children: label })
243
+ ]
244
+ },
245
+ option.value || "none"
246
+ );
247
+ }) });
248
+ }
249
+ }
250
+ ],
251
+ [positionOptions, t]
252
+ );
253
+ const initialValues = React.useMemo(
254
+ () => ({ label: "", color: "", position: POSITION_END }),
255
+ []
256
+ );
257
+ const handleSubmit = React.useCallback(
258
+ async (values) => {
259
+ if (!context) return;
260
+ const trimmedLabel = values.label.trim();
261
+ if (!trimmedLabel.length) {
262
+ throw createCrudFormError(labelRequiredMessage, { label: labelRequiredMessage });
263
+ }
264
+ const payload = {
265
+ pipelineId: context.pipelineId,
266
+ label: trimmedLabel
267
+ };
268
+ if (values.color) payload.color = values.color;
269
+ const insertOrder = resolveInsertOrder(values.position ?? POSITION_END, context.existingStages);
270
+ if (insertOrder !== void 0) payload.order = insertOrder;
271
+ const operation = () => createCrud("customers/pipeline-stages", payload, {
272
+ errorMessage: translateWithFallback(
273
+ t,
274
+ "customers.deals.kanban.addStage.error",
275
+ "Failed to add stage."
276
+ )
277
+ });
278
+ await runMutation({
279
+ operation,
280
+ context: {
281
+ formId: ADD_STAGE_CONTEXT_ID,
282
+ resourceKind: "customers.pipeline_stage",
283
+ retryLastMutation
284
+ }
285
+ });
286
+ flash(translateWithFallback(t, "customers.deals.kanban.addStage.success", "Stage added."), "success");
287
+ onCreated();
288
+ onClose();
289
+ },
290
+ [context, labelRequiredMessage, onClose, onCreated, retryLastMutation, runMutation, t]
291
+ );
292
+ if (!context) return null;
293
+ return /* @__PURE__ */ jsx(
294
+ Dialog,
295
+ {
296
+ open,
297
+ onOpenChange: (next) => {
298
+ if (!next) onClose();
299
+ },
300
+ children: /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-md", children: [
301
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [
302
+ /* @__PURE__ */ jsx(DialogTitle, { children: translateWithFallback(t, "customers.deals.kanban.addStage.title", "New stage") }),
303
+ /* @__PURE__ */ jsx(DialogDescription, { children: translateWithFallback(
304
+ t,
305
+ "customers.deals.kanban.addStage.context",
306
+ "Pipeline: {pipeline}",
307
+ { pipeline: context.pipelineName }
308
+ ) })
309
+ ] }),
310
+ /* @__PURE__ */ jsx(
311
+ CrudForm,
312
+ {
313
+ embedded: true,
314
+ fields,
315
+ initialValues,
316
+ schema: formSchema,
317
+ submitLabel: translateWithFallback(
318
+ t,
319
+ "customers.deals.kanban.addStage.submit",
320
+ "Add stage"
321
+ ),
322
+ onSubmit: handleSubmit
323
+ },
324
+ `${context.pipelineId}:${formInstanceKey}`
325
+ )
326
+ ] })
327
+ }
328
+ );
329
+ }
330
+ var AddStageDialog_default = AddStageDialog;
331
+ export {
332
+ AddStageDialog,
333
+ AddStageDialog_default as default
334
+ };
335
+ //# sourceMappingURL=AddStageDialog.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../../../src/modules/customers/backend/customers/deals/pipeline/components/AddStageDialog.tsx"],
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { z } from 'zod'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'\nimport { createCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'\n\nconst ADD_STAGE_CONTEXT_ID = 'customers-deals-kanban:add-stage'\n\n// Stage color palette \u2014 semantic tone identifiers matching Lane.tsx's ACCENT_TONE_CLASS\n// map. We switched from arbitrary hex values to canonical tones (success / warning / info\n// / error / neutral / brand / pink) so the picker can't silently fall through to gray\n// when the implicit hex\u2192tone mapping doesn't recognise a value. Empty string = no color\n// (lane falls back to the position-based rotation). Forward-only migration 2026-05-19\n// backfills existing hex values to their nearest tone.\nconst TONE_OPTIONS = [\n {\n value: '',\n swatchClass: 'border border-dashed border-border',\n labelKey: 'customers.deals.kanban.addStage.color.none',\n labelFallback: 'No color',\n },\n {\n value: 'success',\n swatchClass: 'bg-status-success-icon',\n labelKey: 'customers.deals.kanban.addStage.color.success',\n labelFallback: 'Green',\n },\n {\n value: 'warning',\n swatchClass: 'bg-status-warning-icon',\n labelKey: 'customers.deals.kanban.addStage.color.warning',\n labelFallback: 'Amber',\n },\n {\n value: 'info',\n swatchClass: 'bg-status-info-icon',\n labelKey: 'customers.deals.kanban.addStage.color.info',\n labelFallback: 'Blue',\n },\n {\n value: 'error',\n swatchClass: 'bg-status-error-icon',\n labelKey: 'customers.deals.kanban.addStage.color.error',\n labelFallback: 'Red',\n },\n {\n value: 'neutral',\n swatchClass: 'bg-status-neutral-icon',\n labelKey: 'customers.deals.kanban.addStage.color.neutral',\n labelFallback: 'Gray',\n },\n {\n value: 'brand',\n swatchClass: 'bg-brand-violet',\n labelKey: 'customers.deals.kanban.addStage.color.brand',\n labelFallback: 'Violet',\n },\n {\n value: 'pink',\n swatchClass: 'bg-status-pink-icon',\n labelKey: 'customers.deals.kanban.addStage.color.pink',\n labelFallback: 'Pink',\n },\n] as const\n\n/**\n * Identifier values for the \"Position\" picker:\n * - `end` \u2014 append after every existing stage (the default; matches the previous\n * always-at-end behavior so the dialog stays familiar)\n * - `start` \u2014 insert at order 0; existing stages shift right\n * - `after:<stageId>` \u2014 insert directly after the given stage; everything to its right shifts\n *\n * We use a single-string discriminated value rather than a separate \"mode\" + \"afterId\"\n * pair because CrudForm's built-in `select` field type expects scalar option values, and\n * the backend payload computation is trivial off this single string.\n */\nconst POSITION_END = 'end' as const\nconst POSITION_START = 'start' as const\nconst POSITION_AFTER_PREFIX = 'after:'\n\nexport type AddStageContext = {\n pipelineId: string\n pipelineName: string\n /**\n * Snapshot of every stage already in the pipeline, ordered by their `order` field.\n * Drives the \"Position\" picker. `order` is the server-side position used to build\n * the create payload (we send `existingOrder + 1` for \"after this stage\").\n */\n existingStages: ReadonlyArray<{ id: string; label: string; order: number }>\n}\n\ntype AddStageDialogProps = {\n open: boolean\n context: AddStageContext | null\n onClose: () => void\n onCreated: () => void\n}\n\ntype AddStageFormValues = {\n label: string\n color: string\n position: string\n}\n\n/**\n * Resolve the picker's discriminated string to the integer `order` we send to the API.\n * Returns `undefined` when the operator picked \"At end\" \u2014 the backend treats undefined\n * as \"append\" so the legacy default behavior is preserved bit-for-bit.\n */\nfunction resolveInsertOrder(\n position: string,\n existingStages: ReadonlyArray<{ id: string; label: string; order: number }>,\n): number | undefined {\n if (position === POSITION_END || position === '') return undefined\n if (position === POSITION_START) return 0\n if (position.startsWith(POSITION_AFTER_PREFIX)) {\n const afterId = position.slice(POSITION_AFTER_PREFIX.length)\n const idx = existingStages.findIndex((stage) => stage.id === afterId)\n if (idx < 0) return undefined\n // Insert position is the existing stage's order + 1; the backend will shift the\n // remainder of the stages by +1 to keep ordering dense and unique.\n return existingStages[idx].order + 1\n }\n return undefined\n}\n\nexport function AddStageDialog({\n open,\n context,\n onClose,\n onCreated,\n}: AddStageDialogProps): React.ReactElement | null {\n const t = useT()\n // Re-mount CrudForm whenever the dialog opens so cleared state is consistently fresh and\n // any stale field errors from a previous open don't carry over.\n const [formInstanceKey, setFormInstanceKey] = React.useState(0)\n React.useEffect(() => {\n if (open) setFormInstanceKey((current) => current + 1)\n }, [open])\n\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n retryLastMutation: () => Promise<boolean>\n }>({\n contextId: ADD_STAGE_CONTEXT_ID,\n blockedMessage: translateWithFallback(t, 'ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n const labelRequiredMessage = translateWithFallback(\n t,\n 'customers.deals.kanban.addStage.label.required',\n 'Stage name is required.',\n )\n\n // Cast widens to AddStageFormValues so CrudForm's TValues generic stays exact \u2014 Zod's\n // inferred type is structurally identical but the generic parameters carry extra\n // metadata that doesn't match `z.ZodType<TValues>` literally.\n const formSchema = React.useMemo(\n () =>\n z.object({\n label: z.string().trim().min(1, labelRequiredMessage),\n color: z.string().optional(),\n position: z.string().optional(),\n }) as unknown as z.ZodType<AddStageFormValues>,\n [labelRequiredMessage],\n )\n\n const positionOptions = React.useMemo(() => {\n const existingStages = context?.existingStages ?? []\n const opts: Array<{ value: string; label: string }> = [\n {\n value: POSITION_END,\n label: translateWithFallback(\n t,\n 'customers.deals.kanban.addStage.position.end',\n 'At the end (default)',\n ),\n },\n {\n value: POSITION_START,\n label: translateWithFallback(\n t,\n 'customers.deals.kanban.addStage.position.start',\n 'At the start',\n ),\n },\n ]\n for (const stage of existingStages) {\n opts.push({\n value: `${POSITION_AFTER_PREFIX}${stage.id}`,\n label: translateWithFallback(\n t,\n 'customers.deals.kanban.addStage.position.after',\n 'After {label}',\n { label: stage.label },\n ),\n })\n }\n return opts\n }, [context?.existingStages, t])\n\n const fields = React.useMemo<CrudField[]>(\n () => [\n {\n // CrudForm auto-focuses the first field of the first group on mount, so we don't\n // need an explicit `autoFocus: true` here \u2014 and adding one would be a type error\n // because `autoFocus` is only valid on CrudCustomFieldRenderProps.\n id: 'label',\n label: translateWithFallback(t, 'customers.deals.kanban.addStage.label', 'Stage name'),\n type: 'text',\n required: true,\n placeholder: translateWithFallback(\n t,\n 'customers.deals.kanban.addStage.label.placeholder',\n 'e.g. Discovery',\n ),\n },\n {\n // Custom inline radio-style list instead of a Radix Select \u2014 when the dialog sits\n // near the bottom of the viewport, Radix's Select dropdown clips to the available\n // vertical space (`--radix-select-content-available-height`), which can leave only\n // 1\u20132 visible options and hide the rest behind a small scroll affordance the\n // operator doesn't notice. Rendering the picker inline inside the dialog body\n // gives it a deterministic max-height + clearly scrollable area, and \"After {label}\"\n // entries are always visible without portal clipping.\n id: 'position',\n label: translateWithFallback(\n t,\n 'customers.deals.kanban.addStage.position',\n 'Position in pipeline',\n ),\n type: 'custom',\n description: translateWithFallback(\n t,\n 'customers.deals.kanban.addStage.position.help',\n 'Choose where the new stage appears on the kanban. Existing stages after the chosen position shift one step to the right.',\n ),\n component: ({ value, setValue }) => {\n const current = typeof value === 'string' && value.length > 0 ? value : POSITION_END\n return (\n <div className=\"flex max-h-52 flex-col gap-1 overflow-y-auto rounded-md border border-input bg-card p-1\">\n {positionOptions.map((option) => {\n const selected = current === option.value\n return (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n key={option.value}\n type=\"button\"\n onClick={() => setValue(option.value)}\n aria-pressed={selected}\n className={`flex items-center justify-between gap-2 rounded-sm px-3 py-2 text-left text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${\n selected\n ? 'bg-muted font-medium text-foreground'\n : 'text-muted-foreground hover:bg-muted/60 hover:text-foreground'\n }`}\n >\n <span className=\"flex-1 truncate\">{option.label}</span>\n <span\n className={`inline-flex size-4 shrink-0 items-center justify-center rounded-full border ${\n selected ? 'border-primary bg-primary' : 'border-input'\n }`}\n aria-hidden=\"true\"\n >\n {selected ? (\n <span className=\"size-1.5 rounded-full bg-primary-foreground\" />\n ) : null}\n </span>\n </Button>\n )\n })}\n </div>\n )\n },\n },\n {\n // Custom render \u2014 built-in `select` can't render a swatch alongside each label.\n // Each swatch uses the same DS class the lane bar renders, so the picker is a\n // true preview of the kanban appearance instead of a separate hex translation.\n id: 'color',\n label: translateWithFallback(t, 'customers.deals.kanban.addStage.color', 'Color'),\n type: 'custom',\n component: ({ value, setValue }) => {\n const current = typeof value === 'string' ? value : ''\n return (\n <div className=\"flex flex-wrap gap-2\">\n {TONE_OPTIONS.map((option) => {\n const selected = current === option.value\n const label = translateWithFallback(t, option.labelKey, option.labelFallback)\n return (\n <Button\n variant=\"outline\"\n size=\"sm\"\n key={option.value || 'none'}\n type=\"button\"\n onClick={() => setValue(option.value)}\n aria-pressed={selected}\n title={label}\n className={`inline-flex items-center gap-2 rounded-md border px-3 py-1.5 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${\n selected\n ? 'border-primary bg-muted text-foreground'\n : 'border-input bg-card text-muted-foreground hover:bg-muted hover:text-foreground'\n }`}\n >\n <span\n className={`size-3 rounded-sm ${option.swatchClass}`}\n aria-hidden=\"true\"\n />\n <span>{label}</span>\n </Button>\n )\n })}\n </div>\n )\n },\n },\n ],\n [positionOptions, t],\n )\n\n const initialValues = React.useMemo<Partial<AddStageFormValues>>(\n () => ({ label: '', color: '', position: POSITION_END }),\n [],\n )\n\n const handleSubmit = React.useCallback(\n async (values: AddStageFormValues) => {\n if (!context) return\n const trimmedLabel = values.label.trim()\n if (!trimmedLabel.length) {\n // Zod schema should already catch this \u2014 but we re-verify defensively because the\n // Zod check uses `trim().min(1)` and the field renders trimmed values consistently.\n throw createCrudFormError(labelRequiredMessage, { label: labelRequiredMessage })\n }\n const payload: Record<string, unknown> = {\n pipelineId: context.pipelineId,\n label: trimmedLabel,\n }\n if (values.color) payload.color = values.color\n const insertOrder = resolveInsertOrder(values.position ?? POSITION_END, context.existingStages)\n // Only send `order` when the operator explicitly chose a non-default position. Letting\n // it stay undefined preserves the legacy \"append to end\" behavior on the backend\n // (and keeps payloads clean / debugger-friendly for typical add flows).\n if (insertOrder !== undefined) payload.order = insertOrder\n const operation = () =>\n createCrud('customers/pipeline-stages', payload, {\n errorMessage: translateWithFallback(\n t,\n 'customers.deals.kanban.addStage.error',\n 'Failed to add stage.',\n ),\n })\n await runMutation({\n operation,\n context: {\n formId: ADD_STAGE_CONTEXT_ID,\n resourceKind: 'customers.pipeline_stage',\n retryLastMutation,\n },\n })\n flash(translateWithFallback(t, 'customers.deals.kanban.addStage.success', 'Stage added.'), 'success')\n onCreated()\n onClose()\n },\n [context, labelRequiredMessage, onClose, onCreated, retryLastMutation, runMutation, t],\n )\n\n if (!context) return null\n\n return (\n <Dialog\n open={open}\n onOpenChange={(next) => {\n if (!next) onClose()\n }}\n >\n <DialogContent className=\"sm:max-w-md\">\n <DialogHeader>\n <DialogTitle>\n {translateWithFallback(t, 'customers.deals.kanban.addStage.title', 'New stage')}\n </DialogTitle>\n <DialogDescription>\n {translateWithFallback(\n t,\n 'customers.deals.kanban.addStage.context',\n 'Pipeline: {pipeline}',\n { pipeline: context.pipelineName },\n )}\n </DialogDescription>\n </DialogHeader>\n\n <CrudForm<AddStageFormValues>\n key={`${context.pipelineId}:${formInstanceKey}`}\n embedded\n fields={fields}\n initialValues={initialValues}\n schema={formSchema}\n submitLabel={translateWithFallback(\n t,\n 'customers.deals.kanban.addStage.submit',\n 'Add stage',\n )}\n onSubmit={handleSubmit}\n />\n </DialogContent>\n </Dialog>\n )\n}\n\nexport default AddStageDialog\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }