@open-mercato/core 0.6.5-develop.5382.1.f542de69af → 0.6.6-develop.5412.1.e2a52b14f0

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 (138) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/helpers/integration/crmFixtures.js +4 -0
  3. package/dist/helpers/integration/crmFixtures.js.map +2 -2
  4. package/dist/modules/attachments/api/route.js +2 -0
  5. package/dist/modules/attachments/api/route.js.map +2 -2
  6. package/dist/modules/attachments/lib/access.js +18 -0
  7. package/dist/modules/attachments/lib/access.js.map +2 -2
  8. package/dist/modules/auth/services/rbacService.js +3 -2
  9. package/dist/modules/auth/services/rbacService.js.map +2 -2
  10. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js +0 -3
  11. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js.map +2 -2
  12. package/dist/modules/customers/api/deals/route.js +43 -2
  13. package/dist/modules/customers/api/deals/route.js.map +2 -2
  14. package/dist/modules/customers/api/deals/summary/route.js +402 -0
  15. package/dist/modules/customers/api/deals/summary/route.js.map +7 -0
  16. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.js +16 -5
  17. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.js.map +2 -2
  18. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealData.js +22 -5
  19. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealData.js.map +2 -2
  20. package/dist/modules/customers/backend/customers/deals/[id]/page.js +12 -2
  21. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  22. package/dist/modules/customers/backend/customers/deals/page.js +221 -56
  23. package/dist/modules/customers/backend/customers/deals/page.js.map +3 -3
  24. package/dist/modules/customers/backend/customers/deals/pipeline/page.js +1 -1
  25. package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
  26. package/dist/modules/customers/cli.js +15 -9
  27. package/dist/modules/customers/cli.js.map +2 -2
  28. package/dist/modules/customers/components/DealsKpiStrip.js +282 -0
  29. package/dist/modules/customers/components/DealsKpiStrip.js.map +7 -0
  30. package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js +0 -1
  31. package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js.map +2 -2
  32. package/dist/modules/customers/components/detail/DealForm.js +100 -17
  33. package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
  34. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +1 -2
  35. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
  36. package/dist/modules/customers/components/kpi/PipelineStageBar.js +63 -0
  37. package/dist/modules/customers/components/kpi/PipelineStageBar.js.map +7 -0
  38. package/dist/modules/customers/lib/dealsMetrics.js +82 -0
  39. package/dist/modules/customers/lib/dealsMetrics.js.map +7 -0
  40. package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js +2 -1
  41. package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js.map +2 -2
  42. package/dist/modules/directory/utils/organizationScope.js +59 -27
  43. package/dist/modules/directory/utils/organizationScope.js.map +2 -2
  44. package/dist/modules/entities/api/entities.js +7 -0
  45. package/dist/modules/entities/api/entities.js.map +2 -2
  46. package/dist/modules/entities/api/records.js +26 -15
  47. package/dist/modules/entities/api/records.js.map +2 -2
  48. package/dist/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.js +14 -0
  49. package/dist/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.js.map +2 -2
  50. package/dist/modules/entities/backend/entities/user/[entityId]/records/create/page.js +14 -0
  51. package/dist/modules/entities/backend/entities/user/[entityId]/records/create/page.js.map +2 -2
  52. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +12 -0
  53. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
  54. package/dist/modules/entities/components/useRecordsEntityGuard.js +30 -0
  55. package/dist/modules/entities/components/useRecordsEntityGuard.js.map +7 -0
  56. package/dist/modules/query_index/lib/engine.js +4 -2
  57. package/dist/modules/query_index/lib/engine.js.map +2 -2
  58. package/dist/modules/staff/api/team-members.js +9 -2
  59. package/dist/modules/staff/api/team-members.js.map +2 -2
  60. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js +24 -1
  61. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js.map +2 -2
  62. package/dist/modules/staff/backend/staff/team-members/[id]/page.js +11 -6
  63. package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
  64. package/dist/modules/staff/commands/team-members.js +1 -1
  65. package/dist/modules/staff/commands/team-members.js.map +2 -2
  66. package/dist/modules/staff/components/TeamMemberForm.js +1 -1
  67. package/dist/modules/staff/components/TeamMemberForm.js.map +2 -2
  68. package/dist/modules/staff/lib/scheduleSwitch.js +23 -0
  69. package/dist/modules/staff/lib/scheduleSwitch.js.map +7 -0
  70. package/dist/modules/workflows/backend/definitions/create/page.js +1 -2
  71. package/dist/modules/workflows/backend/definitions/create/page.js.map +2 -2
  72. package/dist/modules/workflows/backend/definitions/visual-editor/page.js +1 -2
  73. package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
  74. package/dist/modules/workflows/components/DefinitionTriggersEditor.js +1 -2
  75. package/dist/modules/workflows/components/DefinitionTriggersEditor.js.map +2 -2
  76. package/dist/modules/workflows/components/NodeEditDialog.js +4 -13
  77. package/dist/modules/workflows/components/NodeEditDialog.js.map +2 -2
  78. package/dist/modules/workflows/components/NodeEditDialogCrudForm.js +4 -13
  79. package/dist/modules/workflows/components/NodeEditDialogCrudForm.js.map +2 -2
  80. package/dist/modules/workflows/components/WorkflowGraphImpl.js +1 -4
  81. package/dist/modules/workflows/components/WorkflowGraphImpl.js.map +2 -2
  82. package/dist/modules/workflows/components/fields/FormFieldArrayEditor.js +2 -5
  83. package/dist/modules/workflows/components/fields/FormFieldArrayEditor.js.map +2 -2
  84. package/package.json +8 -8
  85. package/src/helpers/integration/crmFixtures.ts +21 -1
  86. package/src/modules/attachments/AGENTS.md +79 -0
  87. package/src/modules/attachments/api/route.ts +2 -0
  88. package/src/modules/attachments/lib/access.ts +36 -0
  89. package/src/modules/auth/services/rbacService.ts +11 -2
  90. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.tsx +0 -3
  91. package/src/modules/customers/api/deals/route.ts +51 -2
  92. package/src/modules/customers/api/deals/summary/route.ts +496 -0
  93. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.ts +28 -6
  94. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealData.ts +33 -6
  95. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +17 -2
  96. package/src/modules/customers/backend/customers/deals/page.tsx +254 -66
  97. package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +1 -2
  98. package/src/modules/customers/cli.ts +15 -15
  99. package/src/modules/customers/components/DealsKpiStrip.tsx +389 -0
  100. package/src/modules/customers/components/detail/ConfirmDealLostDialog.tsx +0 -1
  101. package/src/modules/customers/components/detail/DealForm.tsx +121 -19
  102. package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +1 -2
  103. package/src/modules/customers/components/kpi/PipelineStageBar.tsx +77 -0
  104. package/src/modules/customers/i18n/de.json +43 -0
  105. package/src/modules/customers/i18n/en.json +43 -0
  106. package/src/modules/customers/i18n/es.json +43 -0
  107. package/src/modules/customers/i18n/pl.json +43 -0
  108. package/src/modules/customers/lib/dealsMetrics.ts +159 -0
  109. package/src/modules/directory/subscribers/invalidateOrgScopeCache.ts +3 -1
  110. package/src/modules/directory/utils/organizationScope.ts +85 -30
  111. package/src/modules/entities/api/entities.ts +11 -0
  112. package/src/modules/entities/api/records.ts +46 -25
  113. package/src/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.tsx +15 -0
  114. package/src/modules/entities/backend/entities/user/[entityId]/records/create/page.tsx +15 -0
  115. package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +23 -0
  116. package/src/modules/entities/components/useRecordsEntityGuard.ts +41 -0
  117. package/src/modules/entities/i18n/de.json +1 -0
  118. package/src/modules/entities/i18n/en.json +1 -0
  119. package/src/modules/entities/i18n/es.json +1 -0
  120. package/src/modules/entities/i18n/pl.json +1 -0
  121. package/src/modules/query_index/lib/engine.ts +11 -5
  122. package/src/modules/staff/api/team-members.ts +9 -2
  123. package/src/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.ts +31 -1
  124. package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +18 -8
  125. package/src/modules/staff/commands/team-members.ts +5 -2
  126. package/src/modules/staff/components/TeamMemberForm.tsx +4 -1
  127. package/src/modules/staff/i18n/de.json +1 -0
  128. package/src/modules/staff/i18n/en.json +1 -0
  129. package/src/modules/staff/i18n/es.json +1 -0
  130. package/src/modules/staff/i18n/pl.json +1 -0
  131. package/src/modules/staff/lib/scheduleSwitch.ts +46 -0
  132. package/src/modules/workflows/backend/definitions/create/page.tsx +1 -2
  133. package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +1 -2
  134. package/src/modules/workflows/components/DefinitionTriggersEditor.tsx +1 -2
  135. package/src/modules/workflows/components/NodeEditDialog.tsx +1 -4
  136. package/src/modules/workflows/components/NodeEditDialogCrudForm.tsx +4 -7
  137. package/src/modules/workflows/components/WorkflowGraphImpl.tsx +1 -2
  138. package/src/modules/workflows/components/fields/FormFieldArrayEditor.tsx +2 -3
@@ -0,0 +1,282 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { CheckCircle } from "lucide-react";
5
+ import { cn } from "@open-mercato/shared/lib/utils";
6
+ import { useT, useLocale } from "@open-mercato/shared/lib/i18n/context";
7
+ import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
8
+ import { KpiCard, DeltaBadge, Sparkline } from "@open-mercato/ui/backend/charts";
9
+ import { Avatar, AvatarStack } from "@open-mercato/ui/primitives/avatar";
10
+ import { Button } from "@open-mercato/ui/primitives/button";
11
+ import { Spinner } from "@open-mercato/ui/primitives/spinner";
12
+ import { PipelineStageBar } from "./kpi/PipelineStageBar.js";
13
+ const compactNumberFormatter = new Intl.NumberFormat(void 0, {
14
+ notation: "compact",
15
+ maximumFractionDigits: 1
16
+ });
17
+ function formatCompact(value) {
18
+ return compactNumberFormatter.format(value);
19
+ }
20
+ function buildCurrencySuffix(code, convertedAll) {
21
+ if (!code) return convertedAll ? "" : "\u2248";
22
+ return convertedAll ? code : `\u2248 ${code}`;
23
+ }
24
+ const KPI_TITLE_CLASS = "text-xs font-semibold uppercase tracking-wide text-muted-foreground";
25
+ function DealKpiCard(props) {
26
+ return /* @__PURE__ */ jsx(KpiCard, { titleClassName: KPI_TITLE_CLASS, ...props });
27
+ }
28
+ function KpiDeltaBadge({
29
+ direction,
30
+ value,
31
+ unit,
32
+ title
33
+ }) {
34
+ if (direction === "unchanged" && value === 0) {
35
+ return /* @__PURE__ */ jsx(
36
+ "span",
37
+ {
38
+ className: "inline-flex items-center rounded-md bg-status-neutral-bg px-2 py-0.5 text-xs font-medium text-status-neutral-text",
39
+ title,
40
+ children: "--"
41
+ }
42
+ );
43
+ }
44
+ return /* @__PURE__ */ jsx(DeltaBadge, { direction, value, unit, title });
45
+ }
46
+ function isObject(value) {
47
+ return typeof value === "object" && value !== null;
48
+ }
49
+ function isDealsSummaryResponse(value) {
50
+ if (!isObject(value)) return false;
51
+ const { pipelineValue, activeDeals, wonThisQuarter, winRate } = value;
52
+ return isObject(pipelineValue) && Array.isArray(pipelineValue.stages) && isObject(activeDeals) && Array.isArray(activeDeals.owners) && isObject(wonThisQuarter) && isObject(winRate) && Array.isArray(winRate.series);
53
+ }
54
+ function DealsKpiStrip({
55
+ ownerNames,
56
+ stageDictionary,
57
+ pipelineCount,
58
+ className,
59
+ scopeVersion,
60
+ reloadToken,
61
+ onNeedsAttentionClick
62
+ }) {
63
+ const t = useT();
64
+ const locale = useLocale();
65
+ const pluralCat = React.useCallback((count) => {
66
+ try {
67
+ return new Intl.PluralRules(locale).select(count);
68
+ } catch {
69
+ return count === 1 ? "one" : "other";
70
+ }
71
+ }, [locale]);
72
+ const pf = React.useCallback((base, count) => {
73
+ const cat = pluralCat(count);
74
+ const key = `${base}.${cat}`;
75
+ const out = t(key, { count });
76
+ return out === key ? t(`${base}.other`, { count }) : out;
77
+ }, [t, pluralCat]);
78
+ const [data, setData] = React.useState(null);
79
+ const [loading, setLoading] = React.useState(true);
80
+ const [error, setError] = React.useState(null);
81
+ const [retryToken, setRetryToken] = React.useState(0);
82
+ const previousScopeVersionRef = React.useRef(scopeVersion);
83
+ const retry = React.useCallback(() => {
84
+ setRetryToken((token) => token + 1);
85
+ }, []);
86
+ React.useEffect(() => {
87
+ let cancelled = false;
88
+ const scopeChanged = previousScopeVersionRef.current !== scopeVersion;
89
+ previousScopeVersionRef.current = scopeVersion;
90
+ if (scopeChanged) setData(null);
91
+ setLoading(true);
92
+ setError(null);
93
+ apiCall("/api/customers/deals/summary").then((call) => {
94
+ if (cancelled) return;
95
+ if (!call.ok || !isDealsSummaryResponse(call.result)) {
96
+ setError(t("customers.deals.list.kpi.error"));
97
+ return;
98
+ }
99
+ setData(call.result);
100
+ }).catch(() => {
101
+ if (cancelled) return;
102
+ setError(t("customers.deals.list.kpi.error"));
103
+ }).finally(() => {
104
+ if (!cancelled) setLoading(false);
105
+ });
106
+ return () => {
107
+ cancelled = true;
108
+ };
109
+ }, [t, scopeVersion, reloadToken, retryToken]);
110
+ const wrapperClassName = cn("space-y-2", className);
111
+ const gridClassName = "grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4";
112
+ if (loading && !data) {
113
+ return /* @__PURE__ */ jsx("div", { className: wrapperClassName, children: /* @__PURE__ */ jsxs("div", { className: gridClassName, children: [
114
+ /* @__PURE__ */ jsx(DealKpiCard, { loading: true, title: t("customers.deals.list.kpi.pipelineValue"), value: null }),
115
+ /* @__PURE__ */ jsx(DealKpiCard, { loading: true, title: t("customers.deals.list.kpi.activeDeals"), value: null }),
116
+ /* @__PURE__ */ jsx(DealKpiCard, { loading: true, title: t("customers.deals.list.kpi.wonThisQuarter"), value: null }),
117
+ /* @__PURE__ */ jsx(DealKpiCard, { loading: true, title: t("customers.deals.list.kpi.winRate"), value: null })
118
+ ] }) });
119
+ }
120
+ if (!data) {
121
+ const errorMessage = error ?? t("customers.deals.list.kpi.error");
122
+ return /* @__PURE__ */ jsx("div", { className: wrapperClassName, children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 rounded-lg border border-destructive/30 bg-destructive/5 p-4", children: [
123
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", children: errorMessage }),
124
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "destructive-outline", size: "sm", onClick: retry, children: t("customers.deals.list.kpi.retry") })
125
+ ] }) });
126
+ }
127
+ const currencySuffix = buildCurrencySuffix(data.baseCurrencyCode, data.convertedAll);
128
+ const unassignedLabel = t("customers.deals.list.kpi.unassignedStage");
129
+ const deltaTooltip = t("customers.deals.list.kpi.deltaTooltip");
130
+ const deltaUnavailableTooltip = t("customers.deals.list.kpi.deltaUnavailable");
131
+ const scopeLabel = t("customers.deals.list.kpi.scopeAllPipelinesThisQuarter");
132
+ const unknownOwner = t("customers.deals.list.unknownOwner");
133
+ const currencyHint = !data.convertedAll ? data.baseCurrencyCode ? t("customers.deals.list.kpi.currencyApproxMissing", {
134
+ currencies: data.missingRateCurrencies.length ? data.missingRateCurrencies.join(", ") : currencySuffix
135
+ }) : t("customers.deals.list.kpi.currencyApproxNoBase") : null;
136
+ const attentionLabel = pf("customers.deals.list.kpi.frag.needAttention", data.activeDeals.needAttention);
137
+ return /* @__PURE__ */ jsxs("div", { className: wrapperClassName, children: [
138
+ error ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2", children: [
139
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: error }),
140
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "destructive-outline", size: "2xs", onClick: retry, children: t("customers.deals.list.kpi.retry") })
141
+ ] }) : null,
142
+ loading ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-2 text-xs text-muted-foreground", children: [
143
+ /* @__PURE__ */ jsx(Spinner, { className: "h-3 w-3" }),
144
+ /* @__PURE__ */ jsx("span", { children: t("customers.deals.list.kpi.updating") })
145
+ ] }) : null,
146
+ /* @__PURE__ */ jsxs("div", { className: gridClassName, children: [
147
+ /* @__PURE__ */ jsx(
148
+ DealKpiCard,
149
+ {
150
+ title: t("customers.deals.list.kpi.pipelineValue"),
151
+ value: data.pipelineValue.value,
152
+ formatValue: formatCompact,
153
+ suffix: currencySuffix,
154
+ headerAction: /* @__PURE__ */ jsx(
155
+ KpiDeltaBadge,
156
+ {
157
+ direction: data.pipelineValue.delta.direction,
158
+ value: data.pipelineValue.delta.value,
159
+ title: data.pipelineValue.delta.direction === "unchanged" && data.pipelineValue.delta.value === 0 ? deltaUnavailableTooltip : deltaTooltip
160
+ }
161
+ ),
162
+ footer: /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
163
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: scopeLabel }),
164
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("customers.deals.list.kpi.activeAcrossPipelines", {
165
+ deals: pf("customers.deals.list.kpi.frag.activeDeals", data.activeDeals.value),
166
+ pipelines: pf("customers.deals.list.kpi.frag.pipelines", pipelineCount)
167
+ }) }),
168
+ currencyHint ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: currencyHint }) : null,
169
+ /* @__PURE__ */ jsx(
170
+ PipelineStageBar,
171
+ {
172
+ stages: data.pipelineValue.stages,
173
+ stageDictionary,
174
+ unassignedLabel
175
+ }
176
+ )
177
+ ] })
178
+ }
179
+ ),
180
+ /* @__PURE__ */ jsx(
181
+ DealKpiCard,
182
+ {
183
+ title: t("customers.deals.list.kpi.activeDeals"),
184
+ value: data.activeDeals.value,
185
+ formatValue: formatCompact,
186
+ headerAction: /* @__PURE__ */ jsx(
187
+ KpiDeltaBadge,
188
+ {
189
+ direction: data.activeDeals.delta.direction,
190
+ value: data.activeDeals.delta.value,
191
+ title: data.activeDeals.delta.direction === "unchanged" && data.activeDeals.delta.value === 0 ? deltaUnavailableTooltip : deltaTooltip
192
+ }
193
+ ),
194
+ footer: /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
195
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: scopeLabel }),
196
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-1.5 gap-y-1 text-xs text-muted-foreground", children: [
197
+ /* @__PURE__ */ jsx("span", { children: pf("customers.deals.list.kpi.frag.owners", data.activeDeals.ownersCount) }),
198
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
199
+ onNeedsAttentionClick && data.activeDeals.needAttention > 0 ? /* @__PURE__ */ jsx(
200
+ Button,
201
+ {
202
+ type: "button",
203
+ variant: "link",
204
+ size: "2xs",
205
+ className: "h-auto p-0 text-xs",
206
+ onClick: onNeedsAttentionClick,
207
+ children: attentionLabel
208
+ }
209
+ ) : /* @__PURE__ */ jsx("span", { children: attentionLabel })
210
+ ] }),
211
+ data.activeDeals.owners.length > 0 ? /* @__PURE__ */ jsx(AvatarStack, { max: 4, size: "sm", overflowCount: data.activeDeals.ownersOverflow, children: data.activeDeals.owners.map((owner) => {
212
+ const ownerLabel = ownerNames[owner.id]?.trim() || unknownOwner;
213
+ return /* @__PURE__ */ jsx(Avatar, { label: ownerLabel, size: "sm" }, owner.id);
214
+ }) }) : null
215
+ ] })
216
+ }
217
+ ),
218
+ /* @__PURE__ */ jsx(
219
+ DealKpiCard,
220
+ {
221
+ title: t("customers.deals.list.kpi.wonThisQuarter"),
222
+ value: data.wonThisQuarter.value,
223
+ formatValue: formatCompact,
224
+ suffix: currencySuffix,
225
+ headerAction: /* @__PURE__ */ jsx(
226
+ KpiDeltaBadge,
227
+ {
228
+ direction: data.wonThisQuarter.delta.direction,
229
+ value: data.wonThisQuarter.delta.value,
230
+ title: data.wonThisQuarter.delta.direction === "unchanged" && data.wonThisQuarter.delta.value === 0 ? deltaUnavailableTooltip : deltaTooltip
231
+ }
232
+ ),
233
+ footer: /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
234
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: scopeLabel }),
235
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [
236
+ /* @__PURE__ */ jsx(CheckCircle, { className: "h-4 w-4 text-status-success-text", "aria-hidden": true }),
237
+ /* @__PURE__ */ jsx("span", { children: pf("customers.deals.list.kpi.frag.dealsClosed", data.wonThisQuarter.dealsClosed) })
238
+ ] }),
239
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("customers.deals.list.kpi.avgDeal", {
240
+ value: `${formatCompact(data.wonThisQuarter.avgDeal)}${currencySuffix ? ` ${currencySuffix}` : ""}`
241
+ }) }),
242
+ currencyHint ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: currencyHint }) : null
243
+ ] })
244
+ }
245
+ ),
246
+ /* @__PURE__ */ jsx(
247
+ DealKpiCard,
248
+ {
249
+ title: t("customers.deals.list.kpi.winRate"),
250
+ value: data.winRate.value,
251
+ suffix: "%",
252
+ headerAction: /* @__PURE__ */ jsx(
253
+ KpiDeltaBadge,
254
+ {
255
+ direction: data.winRate.direction,
256
+ value: Math.abs(data.winRate.deltaPp),
257
+ unit: "pp",
258
+ title: data.winRate.previousValue === 0 ? deltaUnavailableTooltip : deltaTooltip
259
+ }
260
+ ),
261
+ footer: /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
262
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: scopeLabel }),
263
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("customers.deals.list.kpi.fromLastQuarter", { value: data.winRate.previousValue }) }),
264
+ /* @__PURE__ */ jsx("div", { className: "text-primary", children: /* @__PURE__ */ jsx(
265
+ Sparkline,
266
+ {
267
+ values: data.winRate.series.map((point) => point.rate),
268
+ ariaLabel: t("customers.deals.list.kpi.winRate")
269
+ }
270
+ ) })
271
+ ] })
272
+ }
273
+ )
274
+ ] })
275
+ ] });
276
+ }
277
+ var DealsKpiStrip_default = DealsKpiStrip;
278
+ export {
279
+ DealsKpiStrip,
280
+ DealsKpiStrip_default as default
281
+ };
282
+ //# sourceMappingURL=DealsKpiStrip.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/customers/components/DealsKpiStrip.tsx"],
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { CheckCircle } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT, useLocale } from '@open-mercato/shared/lib/i18n/context'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { KpiCard, DeltaBadge, Sparkline } from '@open-mercato/ui/backend/charts'\nimport { Avatar, AvatarStack } from '@open-mercato/ui/primitives/avatar'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport type { DictionaryMap } from '@open-mercato/core/modules/dictionaries/components/dictionaryAppearance'\nimport { PipelineStageBar } from './kpi/PipelineStageBar'\n\ntype DeltaDirection = 'up' | 'down' | 'unchanged'\n\ntype SummaryDelta = {\n value: number\n direction: DeltaDirection\n}\n\ntype DealsSummaryResponse = {\n baseCurrencyCode: string | null\n convertedAll: boolean\n missingRateCurrencies: string[]\n pipelineValue: {\n value: number\n delta: SummaryDelta\n stages: { stage: string | null; count: number; value: number }[]\n }\n activeDeals: {\n value: number\n delta: SummaryDelta\n ownersCount: number\n needAttention: number\n owners: { id: string; count: number }[]\n ownersOverflow: number\n }\n wonThisQuarter: {\n value: number\n delta: SummaryDelta\n dealsClosed: number\n avgDeal: number\n }\n winRate: {\n value: number\n deltaPp: number\n direction: DeltaDirection\n previousValue: number\n series: { period: string; rate: number }[]\n }\n}\n\nexport type DealsKpiStripProps = {\n ownerNames: Record<string, string>\n stageDictionary: DictionaryMap\n pipelineCount: number\n className?: string\n /** Bumped by the host when the active org scope changes \u2014 forces a KPI refetch so the cards never show another org's data. */\n scopeVersion?: number\n /** Bumped by the host on manual refresh / after mutations \u2014 forces a KPI refetch so totals stay in sync with the table. */\n reloadToken?: number\n onNeedsAttentionClick?: () => void\n}\n\nconst compactNumberFormatter = new Intl.NumberFormat(undefined, {\n notation: 'compact',\n maximumFractionDigits: 1,\n})\n\nfunction formatCompact(value: number): string {\n return compactNumberFormatter.format(value)\n}\n\nfunction buildCurrencySuffix(code: string | null, convertedAll: boolean): string {\n if (!code) return convertedAll ? '' : '\u2248'\n return convertedAll ? code : `\u2248 ${code}`\n}\n\nconst KPI_TITLE_CLASS = 'text-xs font-semibold uppercase tracking-wide text-muted-foreground'\n\nfunction DealKpiCard(props: React.ComponentProps<typeof KpiCard>) {\n return <KpiCard titleClassName={KPI_TITLE_CLASS} {...props} />\n}\n\nfunction KpiDeltaBadge({\n direction,\n value,\n unit,\n title,\n}: {\n direction: DeltaDirection\n value: number\n unit?: string\n title: string\n}) {\n if (direction === 'unchanged' && value === 0) {\n return (\n <span\n className=\"inline-flex items-center rounded-md bg-status-neutral-bg px-2 py-0.5 text-xs font-medium text-status-neutral-text\"\n title={title}\n >\n --\n </span>\n )\n }\n return <DeltaBadge direction={direction} value={value} unit={unit} title={title} />\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null\n}\n\n// Guard the summary payload before rendering: a non-conforming response (an unrelated\n// endpoint mock, an error body, or a future contract drift) must surface the error card,\n// never crash the whole deals page by dereferencing missing sections/arrays.\nfunction isDealsSummaryResponse(value: unknown): value is DealsSummaryResponse {\n if (!isObject(value)) return false\n const { pipelineValue, activeDeals, wonThisQuarter, winRate } = value\n return (\n isObject(pipelineValue) && Array.isArray(pipelineValue.stages) &&\n isObject(activeDeals) && Array.isArray(activeDeals.owners) &&\n isObject(wonThisQuarter) &&\n isObject(winRate) && Array.isArray(winRate.series)\n )\n}\n\nexport function DealsKpiStrip({\n ownerNames,\n stageDictionary,\n pipelineCount,\n className,\n scopeVersion,\n reloadToken,\n onNeedsAttentionClick,\n}: DealsKpiStripProps) {\n const t = useT()\n const locale = useLocale()\n const pluralCat = React.useCallback((count: number): string => {\n try {\n return new Intl.PluralRules(locale).select(count)\n } catch {\n return count === 1 ? 'one' : 'other'\n }\n }, [locale])\n const pf = React.useCallback((base: string, count: number): string => {\n const cat = pluralCat(count)\n const key = `${base}.${cat}`\n const out = t(key, { count })\n return out === key ? t(`${base}.other`, { count }) : out\n }, [t, pluralCat])\n const [data, setData] = React.useState<DealsSummaryResponse | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [retryToken, setRetryToken] = React.useState(0)\n const previousScopeVersionRef = React.useRef(scopeVersion)\n\n const retry = React.useCallback(() => {\n setRetryToken((token) => token + 1)\n }, [])\n\n React.useEffect(() => {\n let cancelled = false\n const scopeChanged = previousScopeVersionRef.current !== scopeVersion\n previousScopeVersionRef.current = scopeVersion\n if (scopeChanged) setData(null)\n setLoading(true)\n setError(null)\n apiCall<DealsSummaryResponse>('/api/customers/deals/summary')\n .then((call) => {\n if (cancelled) return\n if (!call.ok || !isDealsSummaryResponse(call.result)) {\n setError(t('customers.deals.list.kpi.error'))\n return\n }\n setData(call.result)\n })\n .catch(() => {\n if (cancelled) return\n setError(t('customers.deals.list.kpi.error'))\n })\n .finally(() => {\n if (!cancelled) setLoading(false)\n })\n return () => {\n cancelled = true\n }\n }, [t, scopeVersion, reloadToken, retryToken])\n\n const wrapperClassName = cn('space-y-2', className)\n const gridClassName = 'grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4'\n\n if (loading && !data) {\n return (\n <div className={wrapperClassName}>\n <div className={gridClassName}>\n <DealKpiCard loading title={t('customers.deals.list.kpi.pipelineValue')} value={null} />\n <DealKpiCard loading title={t('customers.deals.list.kpi.activeDeals')} value={null} />\n <DealKpiCard loading title={t('customers.deals.list.kpi.wonThisQuarter')} value={null} />\n <DealKpiCard loading title={t('customers.deals.list.kpi.winRate')} value={null} />\n </div>\n </div>\n )\n }\n\n if (!data) {\n const errorMessage = error ?? t('customers.deals.list.kpi.error')\n return (\n <div className={wrapperClassName}>\n <div className=\"flex items-center justify-between gap-3 rounded-lg border border-destructive/30 bg-destructive/5 p-4\">\n <p className=\"text-sm text-destructive\">{errorMessage}</p>\n <Button type=\"button\" variant=\"destructive-outline\" size=\"sm\" onClick={retry}>\n {t('customers.deals.list.kpi.retry')}\n </Button>\n </div>\n </div>\n )\n }\n\n const currencySuffix = buildCurrencySuffix(data.baseCurrencyCode, data.convertedAll)\n const unassignedLabel = t('customers.deals.list.kpi.unassignedStage')\n const deltaTooltip = t('customers.deals.list.kpi.deltaTooltip')\n const deltaUnavailableTooltip = t('customers.deals.list.kpi.deltaUnavailable')\n const scopeLabel = t('customers.deals.list.kpi.scopeAllPipelinesThisQuarter')\n const unknownOwner = t('customers.deals.list.unknownOwner')\n const currencyHint = !data.convertedAll\n ? data.baseCurrencyCode\n ? t('customers.deals.list.kpi.currencyApproxMissing', {\n currencies: data.missingRateCurrencies.length ? data.missingRateCurrencies.join(', ') : currencySuffix,\n })\n : t('customers.deals.list.kpi.currencyApproxNoBase')\n : null\n const attentionLabel = pf('customers.deals.list.kpi.frag.needAttention', data.activeDeals.needAttention)\n\n return (\n <div className={wrapperClassName}>\n {error ? (\n <div className=\"flex items-center justify-between gap-3 rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2\">\n <p className=\"text-xs text-destructive\">{error}</p>\n <Button type=\"button\" variant=\"destructive-outline\" size=\"2xs\" onClick={retry}>\n {t('customers.deals.list.kpi.retry')}\n </Button>\n </div>\n ) : null}\n {loading ? (\n <div className=\"flex items-center justify-end gap-2 text-xs text-muted-foreground\">\n <Spinner className=\"h-3 w-3\" />\n <span>{t('customers.deals.list.kpi.updating')}</span>\n </div>\n ) : null}\n <div className={gridClassName}>\n <DealKpiCard\n title={t('customers.deals.list.kpi.pipelineValue')}\n value={data.pipelineValue.value}\n formatValue={formatCompact}\n suffix={currencySuffix}\n headerAction={\n <KpiDeltaBadge\n direction={data.pipelineValue.delta.direction}\n value={data.pipelineValue.delta.value}\n title={data.pipelineValue.delta.direction === 'unchanged' && data.pipelineValue.delta.value === 0 ? deltaUnavailableTooltip : deltaTooltip}\n />\n }\n footer={\n <div className=\"space-y-2\">\n <p className=\"text-xs text-muted-foreground\">{scopeLabel}</p>\n <p className=\"text-xs text-muted-foreground\">\n {t('customers.deals.list.kpi.activeAcrossPipelines', {\n deals: pf('customers.deals.list.kpi.frag.activeDeals', data.activeDeals.value),\n pipelines: pf('customers.deals.list.kpi.frag.pipelines', pipelineCount),\n })}\n </p>\n {currencyHint ? <p className=\"text-xs text-muted-foreground\">{currencyHint}</p> : null}\n <PipelineStageBar\n stages={data.pipelineValue.stages}\n stageDictionary={stageDictionary}\n unassignedLabel={unassignedLabel}\n />\n </div>\n }\n />\n\n <DealKpiCard\n title={t('customers.deals.list.kpi.activeDeals')}\n value={data.activeDeals.value}\n formatValue={formatCompact}\n headerAction={\n <KpiDeltaBadge\n direction={data.activeDeals.delta.direction}\n value={data.activeDeals.delta.value}\n title={data.activeDeals.delta.direction === 'unchanged' && data.activeDeals.delta.value === 0 ? deltaUnavailableTooltip : deltaTooltip}\n />\n }\n footer={\n <div className=\"space-y-2\">\n <p className=\"text-xs text-muted-foreground\">{scopeLabel}</p>\n <div className=\"flex flex-wrap items-center gap-x-1.5 gap-y-1 text-xs text-muted-foreground\">\n <span>{pf('customers.deals.list.kpi.frag.owners', data.activeDeals.ownersCount)}</span>\n <span aria-hidden=\"true\">\u00B7</span>\n {onNeedsAttentionClick && data.activeDeals.needAttention > 0 ? (\n <Button\n type=\"button\"\n variant=\"link\"\n size=\"2xs\"\n className=\"h-auto p-0 text-xs\"\n onClick={onNeedsAttentionClick}\n >\n {attentionLabel}\n </Button>\n ) : (\n <span>{attentionLabel}</span>\n )}\n </div>\n {data.activeDeals.owners.length > 0 ? (\n <AvatarStack max={4} size=\"sm\" overflowCount={data.activeDeals.ownersOverflow}>\n {data.activeDeals.owners.map((owner) => {\n const ownerLabel = ownerNames[owner.id]?.trim() || unknownOwner\n return <Avatar key={owner.id} label={ownerLabel} size=\"sm\" />\n })}\n </AvatarStack>\n ) : null}\n </div>\n }\n />\n\n <DealKpiCard\n title={t('customers.deals.list.kpi.wonThisQuarter')}\n value={data.wonThisQuarter.value}\n formatValue={formatCompact}\n suffix={currencySuffix}\n headerAction={\n <KpiDeltaBadge\n direction={data.wonThisQuarter.delta.direction}\n value={data.wonThisQuarter.delta.value}\n title={data.wonThisQuarter.delta.direction === 'unchanged' && data.wonThisQuarter.delta.value === 0 ? deltaUnavailableTooltip : deltaTooltip}\n />\n }\n footer={\n <div className=\"space-y-1\">\n <p className=\"text-xs text-muted-foreground\">{scopeLabel}</p>\n <div className=\"flex items-center gap-1.5 text-xs text-muted-foreground\">\n <CheckCircle className=\"h-4 w-4 text-status-success-text\" aria-hidden />\n <span>\n {pf('customers.deals.list.kpi.frag.dealsClosed', data.wonThisQuarter.dealsClosed)}\n </span>\n </div>\n <p className=\"text-xs text-muted-foreground\">\n {t('customers.deals.list.kpi.avgDeal', {\n value: `${formatCompact(data.wonThisQuarter.avgDeal)}${currencySuffix ? ` ${currencySuffix}` : ''}`,\n })}\n </p>\n {currencyHint ? <p className=\"text-xs text-muted-foreground\">{currencyHint}</p> : null}\n </div>\n }\n />\n\n <DealKpiCard\n title={t('customers.deals.list.kpi.winRate')}\n value={data.winRate.value}\n suffix=\"%\"\n headerAction={\n <KpiDeltaBadge\n direction={data.winRate.direction}\n value={Math.abs(data.winRate.deltaPp)}\n unit=\"pp\"\n title={data.winRate.previousValue === 0 ? deltaUnavailableTooltip : deltaTooltip}\n />\n }\n footer={\n <div className=\"space-y-2\">\n <p className=\"text-xs text-muted-foreground\">{scopeLabel}</p>\n <p className=\"text-xs text-muted-foreground\">\n {t('customers.deals.list.kpi.fromLastQuarter', { value: data.winRate.previousValue })}\n </p>\n <div className=\"text-primary\">\n <Sparkline\n values={data.winRate.series.map((point) => point.rate)}\n ariaLabel={t('customers.deals.list.kpi.winRate')}\n />\n </div>\n </div>\n }\n />\n </div>\n </div>\n )\n}\n\nexport default DealsKpiStrip\n"],
5
+ "mappings": ";AAkFS,cAiHD,YAjHC;AAhFT,YAAY,WAAW;AACvB,SAAS,mBAAmB;AAC5B,SAAS,UAAU;AACnB,SAAS,MAAM,iBAAiB;AAChC,SAAS,eAAe;AACxB,SAAS,SAAS,YAAY,iBAAiB;AAC/C,SAAS,QAAQ,mBAAmB;AACpC,SAAS,cAAc;AACvB,SAAS,eAAe;AAExB,SAAS,wBAAwB;AAqDjC,MAAM,yBAAyB,IAAI,KAAK,aAAa,QAAW;AAAA,EAC9D,UAAU;AAAA,EACV,uBAAuB;AACzB,CAAC;AAED,SAAS,cAAc,OAAuB;AAC5C,SAAO,uBAAuB,OAAO,KAAK;AAC5C;AAEA,SAAS,oBAAoB,MAAqB,cAA+B;AAC/E,MAAI,CAAC,KAAM,QAAO,eAAe,KAAK;AACtC,SAAO,eAAe,OAAO,UAAK,IAAI;AACxC;AAEA,MAAM,kBAAkB;AAExB,SAAS,YAAY,OAA6C;AAChE,SAAO,oBAAC,WAAQ,gBAAgB,iBAAkB,GAAG,OAAO;AAC9D;AAEA,SAAS,cAAc;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,MAAI,cAAc,eAAe,UAAU,GAAG;AAC5C,WACE;AAAA,MAAC;AAAA;AAAA,QACC,WAAU;AAAA,QACV;AAAA,QACD;AAAA;AAAA,IAED;AAAA,EAEJ;AACA,SAAO,oBAAC,cAAW,WAAsB,OAAc,MAAY,OAAc;AACnF;AAEA,SAAS,SAAS,OAAkD;AAClE,SAAO,OAAO,UAAU,YAAY,UAAU;AAChD;AAKA,SAAS,uBAAuB,OAA+C;AAC7E,MAAI,CAAC,SAAS,KAAK,EAAG,QAAO;AAC7B,QAAM,EAAE,eAAe,aAAa,gBAAgB,QAAQ,IAAI;AAChE,SACE,SAAS,aAAa,KAAK,MAAM,QAAQ,cAAc,MAAM,KAC7D,SAAS,WAAW,KAAK,MAAM,QAAQ,YAAY,MAAM,KACzD,SAAS,cAAc,KACvB,SAAS,OAAO,KAAK,MAAM,QAAQ,QAAQ,MAAM;AAErD;AAEO,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAuB;AACrB,QAAM,IAAI,KAAK;AACf,QAAM,SAAS,UAAU;AACzB,QAAM,YAAY,MAAM,YAAY,CAAC,UAA0B;AAC7D,QAAI;AACF,aAAO,IAAI,KAAK,YAAY,MAAM,EAAE,OAAO,KAAK;AAAA,IAClD,QAAQ;AACN,aAAO,UAAU,IAAI,QAAQ;AAAA,IAC/B;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AACX,QAAM,KAAK,MAAM,YAAY,CAAC,MAAc,UAA0B;AACpE,UAAM,MAAM,UAAU,KAAK;AAC3B,UAAM,MAAM,GAAG,IAAI,IAAI,GAAG;AAC1B,UAAM,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC;AAC5B,WAAO,QAAQ,MAAM,EAAE,GAAG,IAAI,UAAU,EAAE,MAAM,CAAC,IAAI;AAAA,EACvD,GAAG,CAAC,GAAG,SAAS,CAAC;AACjB,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAsC,IAAI;AACxE,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,IAAI;AACjD,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAwB,IAAI;AAC5D,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAS,CAAC;AACpD,QAAM,0BAA0B,MAAM,OAAO,YAAY;AAEzD,QAAM,QAAQ,MAAM,YAAY,MAAM;AACpC,kBAAc,CAAC,UAAU,QAAQ,CAAC;AAAA,EACpC,GAAG,CAAC,CAAC;AAEL,QAAM,UAAU,MAAM;AACpB,QAAI,YAAY;AAChB,UAAM,eAAe,wBAAwB,YAAY;AACzD,4BAAwB,UAAU;AAClC,QAAI,aAAc,SAAQ,IAAI;AAC9B,eAAW,IAAI;AACf,aAAS,IAAI;AACb,YAA8B,8BAA8B,EACzD,KAAK,CAAC,SAAS;AACd,UAAI,UAAW;AACf,UAAI,CAAC,KAAK,MAAM,CAAC,uBAAuB,KAAK,MAAM,GAAG;AACpD,iBAAS,EAAE,gCAAgC,CAAC;AAC5C;AAAA,MACF;AACA,cAAQ,KAAK,MAAM;AAAA,IACrB,CAAC,EACA,MAAM,MAAM;AACX,UAAI,UAAW;AACf,eAAS,EAAE,gCAAgC,CAAC;AAAA,IAC9C,CAAC,EACA,QAAQ,MAAM;AACb,UAAI,CAAC,UAAW,YAAW,KAAK;AAAA,IAClC,CAAC;AACH,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,GAAG,cAAc,aAAa,UAAU,CAAC;AAE7C,QAAM,mBAAmB,GAAG,aAAa,SAAS;AAClD,QAAM,gBAAgB;AAEtB,MAAI,WAAW,CAAC,MAAM;AACpB,WACE,oBAAC,SAAI,WAAW,kBACd,+BAAC,SAAI,WAAW,eACd;AAAA,0BAAC,eAAY,SAAO,MAAC,OAAO,EAAE,wCAAwC,GAAG,OAAO,MAAM;AAAA,MACtF,oBAAC,eAAY,SAAO,MAAC,OAAO,EAAE,sCAAsC,GAAG,OAAO,MAAM;AAAA,MACpF,oBAAC,eAAY,SAAO,MAAC,OAAO,EAAE,yCAAyC,GAAG,OAAO,MAAM;AAAA,MACvF,oBAAC,eAAY,SAAO,MAAC,OAAO,EAAE,kCAAkC,GAAG,OAAO,MAAM;AAAA,OAClF,GACF;AAAA,EAEJ;AAEA,MAAI,CAAC,MAAM;AACT,UAAM,eAAe,SAAS,EAAE,gCAAgC;AAChE,WACE,oBAAC,SAAI,WAAW,kBACd,+BAAC,SAAI,WAAU,wGACb;AAAA,0BAAC,OAAE,WAAU,4BAA4B,wBAAa;AAAA,MACtD,oBAAC,UAAO,MAAK,UAAS,SAAQ,uBAAsB,MAAK,MAAK,SAAS,OACpE,YAAE,gCAAgC,GACrC;AAAA,OACF,GACF;AAAA,EAEJ;AAEA,QAAM,iBAAiB,oBAAoB,KAAK,kBAAkB,KAAK,YAAY;AACnF,QAAM,kBAAkB,EAAE,0CAA0C;AACpE,QAAM,eAAe,EAAE,uCAAuC;AAC9D,QAAM,0BAA0B,EAAE,2CAA2C;AAC7E,QAAM,aAAa,EAAE,uDAAuD;AAC5E,QAAM,eAAe,EAAE,mCAAmC;AAC1D,QAAM,eAAe,CAAC,KAAK,eACvB,KAAK,mBACH,EAAE,kDAAkD;AAAA,IAClD,YAAY,KAAK,sBAAsB,SAAS,KAAK,sBAAsB,KAAK,IAAI,IAAI;AAAA,EAC1F,CAAC,IACD,EAAE,+CAA+C,IACnD;AACJ,QAAM,iBAAiB,GAAG,+CAA+C,KAAK,YAAY,aAAa;AAEvG,SACE,qBAAC,SAAI,WAAW,kBACb;AAAA,YACC,qBAAC,SAAI,WAAU,8GACb;AAAA,0BAAC,OAAE,WAAU,4BAA4B,iBAAM;AAAA,MAC/C,oBAAC,UAAO,MAAK,UAAS,SAAQ,uBAAsB,MAAK,OAAM,SAAS,OACrE,YAAE,gCAAgC,GACrC;AAAA,OACF,IACE;AAAA,IACH,UACC,qBAAC,SAAI,WAAU,qEACb;AAAA,0BAAC,WAAQ,WAAU,WAAU;AAAA,MAC7B,oBAAC,UAAM,YAAE,mCAAmC,GAAE;AAAA,OAChD,IACE;AAAA,IACJ,qBAAC,SAAI,WAAW,eACd;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,EAAE,wCAAwC;AAAA,UACjD,OAAO,KAAK,cAAc;AAAA,UAC1B,aAAa;AAAA,UACb,QAAQ;AAAA,UACR,cACE;AAAA,YAAC;AAAA;AAAA,cACC,WAAW,KAAK,cAAc,MAAM;AAAA,cACpC,OAAO,KAAK,cAAc,MAAM;AAAA,cAChC,OAAO,KAAK,cAAc,MAAM,cAAc,eAAe,KAAK,cAAc,MAAM,UAAU,IAAI,0BAA0B;AAAA;AAAA,UAChI;AAAA,UAEF,QACE,qBAAC,SAAI,WAAU,aACb;AAAA,gCAAC,OAAE,WAAU,iCAAiC,sBAAW;AAAA,YACzD,oBAAC,OAAE,WAAU,iCACV,YAAE,kDAAkD;AAAA,cACnD,OAAO,GAAG,6CAA6C,KAAK,YAAY,KAAK;AAAA,cAC7E,WAAW,GAAG,2CAA2C,aAAa;AAAA,YACxE,CAAC,GACH;AAAA,YACC,eAAe,oBAAC,OAAE,WAAU,iCAAiC,wBAAa,IAAO;AAAA,YAClF;AAAA,cAAC;AAAA;AAAA,gBACC,QAAQ,KAAK,cAAc;AAAA,gBAC3B;AAAA,gBACA;AAAA;AAAA,YACF;AAAA,aACF;AAAA;AAAA,MAEJ;AAAA,MAEA;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,EAAE,sCAAsC;AAAA,UAC/C,OAAO,KAAK,YAAY;AAAA,UACxB,aAAa;AAAA,UACb,cACE;AAAA,YAAC;AAAA;AAAA,cACC,WAAW,KAAK,YAAY,MAAM;AAAA,cAClC,OAAO,KAAK,YAAY,MAAM;AAAA,cAC9B,OAAO,KAAK,YAAY,MAAM,cAAc,eAAe,KAAK,YAAY,MAAM,UAAU,IAAI,0BAA0B;AAAA;AAAA,UAC5H;AAAA,UAEF,QACE,qBAAC,SAAI,WAAU,aACb;AAAA,gCAAC,OAAE,WAAU,iCAAiC,sBAAW;AAAA,YACzD,qBAAC,SAAI,WAAU,+EACb;AAAA,kCAAC,UAAM,aAAG,wCAAwC,KAAK,YAAY,WAAW,GAAE;AAAA,cAChF,oBAAC,UAAK,eAAY,QAAO,kBAAC;AAAA,cACzB,yBAAyB,KAAK,YAAY,gBAAgB,IACzD;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAK;AAAA,kBACL,SAAQ;AAAA,kBACR,MAAK;AAAA,kBACL,WAAU;AAAA,kBACV,SAAS;AAAA,kBAER;AAAA;AAAA,cACH,IAEA,oBAAC,UAAM,0BAAe;AAAA,eAE1B;AAAA,YACC,KAAK,YAAY,OAAO,SAAS,IAChC,oBAAC,eAAY,KAAK,GAAG,MAAK,MAAK,eAAe,KAAK,YAAY,gBAC5D,eAAK,YAAY,OAAO,IAAI,CAAC,UAAU;AACtC,oBAAM,aAAa,WAAW,MAAM,EAAE,GAAG,KAAK,KAAK;AACnD,qBAAO,oBAAC,UAAsB,OAAO,YAAY,MAAK,QAAlC,MAAM,EAAiC;AAAA,YAC7D,CAAC,GACH,IACE;AAAA,aACN;AAAA;AAAA,MAEJ;AAAA,MAEA;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,EAAE,yCAAyC;AAAA,UAClD,OAAO,KAAK,eAAe;AAAA,UAC3B,aAAa;AAAA,UACb,QAAQ;AAAA,UACR,cACE;AAAA,YAAC;AAAA;AAAA,cACC,WAAW,KAAK,eAAe,MAAM;AAAA,cACrC,OAAO,KAAK,eAAe,MAAM;AAAA,cACjC,OAAO,KAAK,eAAe,MAAM,cAAc,eAAe,KAAK,eAAe,MAAM,UAAU,IAAI,0BAA0B;AAAA;AAAA,UAClI;AAAA,UAEF,QACE,qBAAC,SAAI,WAAU,aACb;AAAA,gCAAC,OAAE,WAAU,iCAAiC,sBAAW;AAAA,YACzD,qBAAC,SAAI,WAAU,2DACb;AAAA,kCAAC,eAAY,WAAU,oCAAmC,eAAW,MAAC;AAAA,cACtE,oBAAC,UACE,aAAG,6CAA6C,KAAK,eAAe,WAAW,GAClF;AAAA,eACF;AAAA,YACA,oBAAC,OAAE,WAAU,iCACV,YAAE,oCAAoC;AAAA,cACrC,OAAO,GAAG,cAAc,KAAK,eAAe,OAAO,CAAC,GAAG,iBAAiB,IAAI,cAAc,KAAK,EAAE;AAAA,YACnG,CAAC,GACH;AAAA,YACC,eAAe,oBAAC,OAAE,WAAU,iCAAiC,wBAAa,IAAO;AAAA,aACpF;AAAA;AAAA,MAEJ;AAAA,MAEA;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,EAAE,kCAAkC;AAAA,UAC3C,OAAO,KAAK,QAAQ;AAAA,UACpB,QAAO;AAAA,UACP,cACE;AAAA,YAAC;AAAA;AAAA,cACC,WAAW,KAAK,QAAQ;AAAA,cACxB,OAAO,KAAK,IAAI,KAAK,QAAQ,OAAO;AAAA,cACpC,MAAK;AAAA,cACL,OAAO,KAAK,QAAQ,kBAAkB,IAAI,0BAA0B;AAAA;AAAA,UACtE;AAAA,UAEF,QACE,qBAAC,SAAI,WAAU,aACb;AAAA,gCAAC,OAAE,WAAU,iCAAiC,sBAAW;AAAA,YACzD,oBAAC,OAAE,WAAU,iCACV,YAAE,4CAA4C,EAAE,OAAO,KAAK,QAAQ,cAAc,CAAC,GACtF;AAAA,YACA,oBAAC,SAAI,WAAU,gBACb;AAAA,cAAC;AAAA;AAAA,gBACC,QAAQ,KAAK,QAAQ,OAAO,IAAI,CAAC,UAAU,MAAM,IAAI;AAAA,gBACrD,WAAW,EAAE,kCAAkC;AAAA;AAAA,YACjD,GACF;AAAA,aACF;AAAA;AAAA,MAEJ;AAAA,OACF;AAAA,KACF;AAEJ;AAEA,IAAO,wBAAQ;",
6
+ "names": []
7
+ }
@@ -83,7 +83,6 @@ function ConfirmDealLostDialog({
83
83
  ] }) }),
84
84
  /* @__PURE__ */ jsxs("div", { className: "space-y-6 px-7 py-6", children: [
85
85
  /* @__PURE__ */ jsxs(Alert, { variant: "warning", className: "rounded-md", children: [
86
- /* @__PURE__ */ jsx(AlertTriangle, { className: "size-4" }),
87
86
  /* @__PURE__ */ jsx(AlertTitle, { children: t("customers.deals.detail.lost.warningTitle", "This action closes the deal") }),
88
87
  /* @__PURE__ */ jsx(AlertDescription, { children: t("customers.deals.detail.lost.warning", "This action sets the stage to 'Lost' and cannot be undone without 'sales.reopen' permission") })
89
88
  ] }),
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/customers/components/detail/ConfirmDealLostDialog.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { AlertTriangle, Check, ChevronDown } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { loadDictionaryEntriesByKey } from '@open-mercato/core/modules/dictionaries/lib/clientEntries'\nimport { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'\nimport { Textarea } from '@open-mercato/ui/primitives/textarea'\nimport { useDialogKeyHandler } from '@open-mercato/ui/hooks/useDialogKeyHandler'\n\ntype LossReasonOption = {\n id: string\n value: string\n label: string\n description?: string | null\n}\n\ntype ConfirmDealLostDialogProps = {\n open: boolean\n dealTitle: string\n dealValue?: string | null\n companyName?: string | null\n onClose: () => void\n onConfirm: (input: { lossReasonId: string; lossNotes?: string }) => void | Promise<void>\n}\n\nexport function ConfirmDealLostDialog({\n open,\n dealTitle,\n dealValue,\n companyName,\n onClose,\n onConfirm,\n}: ConfirmDealLostDialogProps) {\n const t = useT()\n const [lossReasonId, setLossReasonId] = React.useState('')\n const [lossNotes, setLossNotes] = React.useState('')\n const [lossReasons, setLossReasons] = React.useState<LossReasonOption[]>([])\n const [reasonListOpen, setReasonListOpen] = React.useState(false)\n const [error, setError] = React.useState('')\n const [isConfirming, setIsConfirming] = React.useState(false)\n\n React.useEffect(() => {\n if (!open) return\n let cancelled = false\n loadDictionaryEntriesByKey('sales.deal_loss_reason')\n .then((items) => {\n if (!cancelled) setLossReasons(items)\n })\n .catch((loadError) => {\n console.error('customers.deals.detail.lossReasons failed', loadError)\n if (!cancelled) setLossReasons([])\n })\n return () => {\n cancelled = true\n }\n }, [open])\n\n React.useEffect(() => {\n if (!open) return\n setLossReasonId('')\n setLossNotes('')\n setReasonListOpen(false)\n setError('')\n }, [open])\n\n const selectedLossReason = React.useMemo(\n () => lossReasons.find((reason) => reason.id === lossReasonId) ?? null,\n [lossReasonId, lossReasons],\n )\n\n const handleConfirm = React.useCallback(async () => {\n if (!lossReasonId) {\n setError(t('customers.deals.detail.lost.reasonRequired', 'Please select a loss reason'))\n return\n }\n setIsConfirming(true)\n try {\n await onConfirm({\n lossReasonId,\n lossNotes: lossNotes.trim() || undefined,\n })\n } finally {\n setIsConfirming(false)\n }\n }, [lossNotes, lossReasonId, onConfirm, t])\n\n const handleKeyDown = useDialogKeyHandler({\n onConfirm: () => void handleConfirm(),\n disabled: isConfirming,\n })\n\n return (\n <Dialog open={open} onOpenChange={(nextOpen) => { if (!nextOpen) onClose() }}>\n <DialogContent className=\"overflow-hidden p-0 sm:max-w-[560px]\" onKeyDown={handleKeyDown}>\n <div className=\"overflow-hidden rounded-lg bg-card\">\n <DialogHeader className=\"border-b border-border/70 px-7 py-5\">\n <div className=\"flex items-start gap-4\">\n <div className=\"flex size-10 shrink-0 items-center justify-center rounded-md bg-destructive/10 text-destructive\">\n <AlertTriangle className=\"size-5\" />\n </div>\n <div className=\"min-w-0\">\n <DialogTitle className=\"text-lg font-bold leading-none tracking-tight text-foreground\">\n {t('customers.deals.detail.lost.title', 'Mark deal as Lost?')}\n </DialogTitle>\n <p className=\"mt-1 text-xs text-muted-foreground\">\n {dealTitle}\n {dealValue ? ` \u00B7 ${dealValue}` : ''}\n {companyName ? ` \u00B7 ${companyName}` : ''}\n </p>\n </div>\n </div>\n </DialogHeader>\n\n <div className=\"space-y-6 px-7 py-6\">\n <Alert variant=\"warning\" className=\"rounded-md\">\n <AlertTriangle className=\"size-4\" />\n <AlertTitle>\n {t('customers.deals.detail.lost.warningTitle', 'This action closes the deal')}\n </AlertTitle>\n <AlertDescription>\n {t('customers.deals.detail.lost.warning', \"This action sets the stage to 'Lost' and cannot be undone without 'sales.reopen' permission\")}\n </AlertDescription>\n </Alert>\n\n <div className=\"space-y-2\">\n <label className=\"text-sm font-semibold text-foreground\">\n {t('customers.deals.detail.lost.reasonLabel', 'Loss reason')}\n <span className=\"ml-1 text-destructive\">*</span>\n </label>\n <div className=\"space-y-3\">\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => setReasonListOpen((current) => !current)}\n className=\"h-auto flex w-full items-center justify-between rounded-md border-2 border-foreground bg-background px-4 py-3 text-left\"\n >\n <div className=\"min-w-0\">\n <div className=\"truncate text-base font-semibold text-foreground\">\n {selectedLossReason?.label ?? t('customers.deals.detail.lost.reasonPlaceholder', 'Select loss reason')}\n </div>\n <div className=\"truncate text-sm text-muted-foreground\">\n {selectedLossReason?.description ?? t('customers.deals.detail.lost.reasonHelp', 'Choose the closest reason from the dictionary.')}\n </div>\n </div>\n <ChevronDown className=\"ml-3 size-4 shrink-0 text-muted-foreground\" />\n </Button>\n\n {reasonListOpen ? (\n <div className=\"overflow-hidden rounded-md border border-border/80 bg-background\">\n {lossReasons.map((reason, index) => {\n const isSelected = reason.id === lossReasonId\n return (\n <Button\n key={reason.id}\n type=\"button\"\n variant=\"ghost\"\n onClick={() => {\n setLossReasonId(reason.id)\n setReasonListOpen(false)\n setError('')\n }}\n className={`h-auto flex w-full items-center justify-between rounded-none px-4 py-3 text-left ${\n index < lossReasons.length - 1 ? 'border-b border-border/60' : ''\n } ${isSelected ? 'bg-muted/60' : 'hover:bg-accent/50'}`}\n >\n <div className=\"min-w-0\">\n <div className=\"text-base font-semibold text-foreground\">{reason.label}</div>\n <div className=\"text-sm text-muted-foreground\">\n {reason.description ?? t('customers.deals.detail.lost.reasonFallbackDescription', 'No description available.')}\n </div>\n </div>\n {isSelected ? (\n <span className=\"ml-3 flex size-6 shrink-0 items-center justify-center rounded-full bg-foreground text-background\">\n <Check className=\"size-3.5\" />\n </span>\n ) : null}\n </Button>\n )\n })}\n </div>\n ) : null}\n </div>\n {error ? <p className=\"text-xs text-destructive\">{error}</p> : null}\n </div>\n\n <div className=\"space-y-2\">\n <label className=\"text-sm font-semibold text-foreground\">\n {t('customers.deals.detail.lost.notesLabel', 'Loss notes (optional)')}\n </label>\n <Textarea\n value={lossNotes}\n onChange={(event) => setLossNotes(event.target.value)}\n placeholder={t('customers.deals.detail.lost.notesPlaceholder', 'Additional context about the loss...')}\n rows={4}\n className=\"min-h-[88px] rounded-md border-border/80 px-4 py-3 shadow-none\"\n />\n </div>\n </div>\n\n <DialogFooter className=\"border-t border-border/70 px-7 py-4 sm:justify-end\">\n <Button type=\"button\" variant=\"outline\" onClick={onClose}>\n {t('customers.deals.detail.lost.cancel', 'Cancel')}\n </Button>\n <Button type=\"button\" variant=\"destructive\" onClick={() => { void handleConfirm() }}>\n {t('customers.deals.detail.lost.confirm', 'Mark as Lost')}\n </Button>\n </DialogFooter>\n </div>\n </DialogContent>\n </Dialog>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { AlertTriangle, Check, ChevronDown } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { loadDictionaryEntriesByKey } from '@open-mercato/core/modules/dictionaries/lib/clientEntries'\nimport { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'\nimport { Textarea } from '@open-mercato/ui/primitives/textarea'\nimport { useDialogKeyHandler } from '@open-mercato/ui/hooks/useDialogKeyHandler'\n\ntype LossReasonOption = {\n id: string\n value: string\n label: string\n description?: string | null\n}\n\ntype ConfirmDealLostDialogProps = {\n open: boolean\n dealTitle: string\n dealValue?: string | null\n companyName?: string | null\n onClose: () => void\n onConfirm: (input: { lossReasonId: string; lossNotes?: string }) => void | Promise<void>\n}\n\nexport function ConfirmDealLostDialog({\n open,\n dealTitle,\n dealValue,\n companyName,\n onClose,\n onConfirm,\n}: ConfirmDealLostDialogProps) {\n const t = useT()\n const [lossReasonId, setLossReasonId] = React.useState('')\n const [lossNotes, setLossNotes] = React.useState('')\n const [lossReasons, setLossReasons] = React.useState<LossReasonOption[]>([])\n const [reasonListOpen, setReasonListOpen] = React.useState(false)\n const [error, setError] = React.useState('')\n const [isConfirming, setIsConfirming] = React.useState(false)\n\n React.useEffect(() => {\n if (!open) return\n let cancelled = false\n loadDictionaryEntriesByKey('sales.deal_loss_reason')\n .then((items) => {\n if (!cancelled) setLossReasons(items)\n })\n .catch((loadError) => {\n console.error('customers.deals.detail.lossReasons failed', loadError)\n if (!cancelled) setLossReasons([])\n })\n return () => {\n cancelled = true\n }\n }, [open])\n\n React.useEffect(() => {\n if (!open) return\n setLossReasonId('')\n setLossNotes('')\n setReasonListOpen(false)\n setError('')\n }, [open])\n\n const selectedLossReason = React.useMemo(\n () => lossReasons.find((reason) => reason.id === lossReasonId) ?? null,\n [lossReasonId, lossReasons],\n )\n\n const handleConfirm = React.useCallback(async () => {\n if (!lossReasonId) {\n setError(t('customers.deals.detail.lost.reasonRequired', 'Please select a loss reason'))\n return\n }\n setIsConfirming(true)\n try {\n await onConfirm({\n lossReasonId,\n lossNotes: lossNotes.trim() || undefined,\n })\n } finally {\n setIsConfirming(false)\n }\n }, [lossNotes, lossReasonId, onConfirm, t])\n\n const handleKeyDown = useDialogKeyHandler({\n onConfirm: () => void handleConfirm(),\n disabled: isConfirming,\n })\n\n return (\n <Dialog open={open} onOpenChange={(nextOpen) => { if (!nextOpen) onClose() }}>\n <DialogContent className=\"overflow-hidden p-0 sm:max-w-[560px]\" onKeyDown={handleKeyDown}>\n <div className=\"overflow-hidden rounded-lg bg-card\">\n <DialogHeader className=\"border-b border-border/70 px-7 py-5\">\n <div className=\"flex items-start gap-4\">\n <div className=\"flex size-10 shrink-0 items-center justify-center rounded-md bg-destructive/10 text-destructive\">\n <AlertTriangle className=\"size-5\" />\n </div>\n <div className=\"min-w-0\">\n <DialogTitle className=\"text-lg font-bold leading-none tracking-tight text-foreground\">\n {t('customers.deals.detail.lost.title', 'Mark deal as Lost?')}\n </DialogTitle>\n <p className=\"mt-1 text-xs text-muted-foreground\">\n {dealTitle}\n {dealValue ? ` \u00B7 ${dealValue}` : ''}\n {companyName ? ` \u00B7 ${companyName}` : ''}\n </p>\n </div>\n </div>\n </DialogHeader>\n\n <div className=\"space-y-6 px-7 py-6\">\n <Alert variant=\"warning\" className=\"rounded-md\">\n <AlertTitle>\n {t('customers.deals.detail.lost.warningTitle', 'This action closes the deal')}\n </AlertTitle>\n <AlertDescription>\n {t('customers.deals.detail.lost.warning', \"This action sets the stage to 'Lost' and cannot be undone without 'sales.reopen' permission\")}\n </AlertDescription>\n </Alert>\n\n <div className=\"space-y-2\">\n <label className=\"text-sm font-semibold text-foreground\">\n {t('customers.deals.detail.lost.reasonLabel', 'Loss reason')}\n <span className=\"ml-1 text-destructive\">*</span>\n </label>\n <div className=\"space-y-3\">\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => setReasonListOpen((current) => !current)}\n className=\"h-auto flex w-full items-center justify-between rounded-md border-2 border-foreground bg-background px-4 py-3 text-left\"\n >\n <div className=\"min-w-0\">\n <div className=\"truncate text-base font-semibold text-foreground\">\n {selectedLossReason?.label ?? t('customers.deals.detail.lost.reasonPlaceholder', 'Select loss reason')}\n </div>\n <div className=\"truncate text-sm text-muted-foreground\">\n {selectedLossReason?.description ?? t('customers.deals.detail.lost.reasonHelp', 'Choose the closest reason from the dictionary.')}\n </div>\n </div>\n <ChevronDown className=\"ml-3 size-4 shrink-0 text-muted-foreground\" />\n </Button>\n\n {reasonListOpen ? (\n <div className=\"overflow-hidden rounded-md border border-border/80 bg-background\">\n {lossReasons.map((reason, index) => {\n const isSelected = reason.id === lossReasonId\n return (\n <Button\n key={reason.id}\n type=\"button\"\n variant=\"ghost\"\n onClick={() => {\n setLossReasonId(reason.id)\n setReasonListOpen(false)\n setError('')\n }}\n className={`h-auto flex w-full items-center justify-between rounded-none px-4 py-3 text-left ${\n index < lossReasons.length - 1 ? 'border-b border-border/60' : ''\n } ${isSelected ? 'bg-muted/60' : 'hover:bg-accent/50'}`}\n >\n <div className=\"min-w-0\">\n <div className=\"text-base font-semibold text-foreground\">{reason.label}</div>\n <div className=\"text-sm text-muted-foreground\">\n {reason.description ?? t('customers.deals.detail.lost.reasonFallbackDescription', 'No description available.')}\n </div>\n </div>\n {isSelected ? (\n <span className=\"ml-3 flex size-6 shrink-0 items-center justify-center rounded-full bg-foreground text-background\">\n <Check className=\"size-3.5\" />\n </span>\n ) : null}\n </Button>\n )\n })}\n </div>\n ) : null}\n </div>\n {error ? <p className=\"text-xs text-destructive\">{error}</p> : null}\n </div>\n\n <div className=\"space-y-2\">\n <label className=\"text-sm font-semibold text-foreground\">\n {t('customers.deals.detail.lost.notesLabel', 'Loss notes (optional)')}\n </label>\n <Textarea\n value={lossNotes}\n onChange={(event) => setLossNotes(event.target.value)}\n placeholder={t('customers.deals.detail.lost.notesPlaceholder', 'Additional context about the loss...')}\n rows={4}\n className=\"min-h-[88px] rounded-md border-border/80 px-4 py-3 shadow-none\"\n />\n </div>\n </div>\n\n <DialogFooter className=\"border-t border-border/70 px-7 py-4 sm:justify-end\">\n <Button type=\"button\" variant=\"outline\" onClick={onClose}>\n {t('customers.deals.detail.lost.cancel', 'Cancel')}\n </Button>\n <Button type=\"button\" variant=\"destructive\" onClick={() => { void handleConfirm() }}>\n {t('customers.deals.detail.lost.confirm', 'Mark as Lost')}\n </Button>\n </DialogFooter>\n </div>\n </DialogContent>\n </Dialog>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -24,6 +24,67 @@ import { DictionaryEntrySelect } from "@open-mercato/core/modules/dictionaries/c
24
24
  import { normalizeCustomFieldSubmitValue } from "./customFieldUtils.js";
25
25
  const DEAL_ENTITY_IDS = [E.customers.customer_deal];
26
26
  const CURRENCY_PRIORITY = ["EUR", "USD", "GBP", "PLN"];
27
+ const PIPELINE_OPTIONS_TTL_MS = 6e4;
28
+ const PIPELINE_STAGE_OPTIONS_TTL_MS = 3e4;
29
+ let pipelineOptionsCache = null;
30
+ const pipelineStageOptionsCache = /* @__PURE__ */ new Map();
31
+ function isFreshCacheEntry(entry) {
32
+ return Boolean(entry && entry.expiresAt > Date.now());
33
+ }
34
+ function normalizePipelineOptions(options) {
35
+ const byId = /* @__PURE__ */ new Map();
36
+ for (const option of options ?? []) {
37
+ if (!option.id) continue;
38
+ byId.set(option.id, {
39
+ id: option.id,
40
+ name: option.name,
41
+ isDefault: option.isDefault === true
42
+ });
43
+ }
44
+ return Array.from(byId.values());
45
+ }
46
+ function mergePipelineOptions(seed, loaded) {
47
+ const byId = /* @__PURE__ */ new Map();
48
+ for (const option of seed) byId.set(option.id, option);
49
+ for (const option of loaded) byId.set(option.id, option);
50
+ return Array.from(byId.values());
51
+ }
52
+ function normalizePipelineStageOptions(options) {
53
+ return [...options ?? []].filter((option) => option.id).sort((left, right) => left.order - right.order);
54
+ }
55
+ async function fetchPipelineOptions() {
56
+ if (isFreshCacheEntry(pipelineOptionsCache)) return pipelineOptionsCache.promise;
57
+ const entry = {
58
+ expiresAt: Date.now() + PIPELINE_OPTIONS_TTL_MS,
59
+ promise: apiCall("/api/customers/pipelines").then((call) => call.ok && call.result?.items ? normalizePipelineOptions(call.result.items) : [])
60
+ };
61
+ pipelineOptionsCache = entry;
62
+ try {
63
+ return await entry.promise;
64
+ } catch (error) {
65
+ if (pipelineOptionsCache === entry) pipelineOptionsCache = null;
66
+ throw error;
67
+ }
68
+ }
69
+ async function fetchPipelineStageOptions(pipelineId) {
70
+ const cached = pipelineStageOptionsCache.get(pipelineId);
71
+ if (isFreshCacheEntry(cached)) return cached.promise;
72
+ const entry = {
73
+ expiresAt: Date.now() + PIPELINE_STAGE_OPTIONS_TTL_MS,
74
+ promise: apiCall(`/api/customers/pipeline-stages?pipelineId=${encodeURIComponent(pipelineId)}`).then((call) => call.ok && call.result?.items ? normalizePipelineStageOptions(call.result.items) : [])
75
+ };
76
+ pipelineStageOptionsCache.set(pipelineId, entry);
77
+ try {
78
+ return await entry.promise;
79
+ } catch (error) {
80
+ if (pipelineStageOptionsCache.get(pipelineId) === entry) pipelineStageOptionsCache.delete(pipelineId);
81
+ throw error;
82
+ }
83
+ }
84
+ function resetDealPipelineMetadataCacheForTests() {
85
+ pipelineOptionsCache = null;
86
+ pipelineStageOptionsCache.clear();
87
+ }
27
88
  const schema = z.object({
28
89
  title: z.string().trim().min(1, "customers.people.detail.deals.titleRequired").max(200, "customers.people.detail.deals.titleTooLong"),
29
90
  status: z.string().trim().max(50, "customers.people.detail.deals.statusTooLong").optional(),
@@ -476,7 +537,9 @@ function DealForm({
476
537
  singleColumnGroups = false,
477
538
  showAssociationsGroup = true,
478
539
  showVersionHistory = true,
479
- showCancelAction = true
540
+ showCancelAction = true,
541
+ initialPipelineOptions,
542
+ initialPipelineStageOptions
480
543
  }) {
481
544
  const t = useT();
482
545
  const [pending, setPending] = React.useState(false);
@@ -551,47 +614,66 @@ function DealForm({
551
614
  const { searchPeople, fetchPeopleByIds, searchCompanies, fetchCompaniesByIds } = useDealAssociationLookups();
552
615
  const disabled = pending || isSubmitting;
553
616
  const canDelete = mode === "edit" && typeof onDelete === "function";
554
- const [pipelines, setPipelines] = React.useState([]);
555
- const [pipelineStages, setPipelineStages] = React.useState([]);
617
+ const mountedRef = React.useRef(false);
618
+ const seedPipelineOptions = React.useMemo(
619
+ () => normalizePipelineOptions(initialPipelineOptions),
620
+ [initialPipelineOptions]
621
+ );
622
+ const seedPipelineStageOptions = React.useMemo(
623
+ () => Array.isArray(initialPipelineStageOptions) ? normalizePipelineStageOptions(initialPipelineStageOptions) : null,
624
+ [initialPipelineStageOptions]
625
+ );
626
+ const [pipelines, setPipelines] = React.useState(() => seedPipelineOptions);
627
+ const [pipelineStages, setPipelineStages] = React.useState(() => seedPipelineStageOptions ?? []);
628
+ React.useEffect(() => {
629
+ mountedRef.current = true;
630
+ return () => {
631
+ mountedRef.current = false;
632
+ };
633
+ }, []);
556
634
  const loadStagesForPipeline = React.useCallback(async (pipelineId) => {
557
635
  if (!pipelineId) {
558
- setPipelineStages([]);
636
+ if (mountedRef.current) setPipelineStages([]);
559
637
  return;
560
638
  }
561
639
  try {
562
- const call = await apiCall(`/api/customers/pipeline-stages?pipelineId=${encodeURIComponent(pipelineId)}`);
563
- if (call.ok && call.result?.items) {
564
- const sorted = [...call.result.items].sort((a, b) => a.order - b.order);
565
- setPipelineStages(sorted);
566
- }
640
+ const stages = await fetchPipelineStageOptions(pipelineId);
641
+ if (mountedRef.current) setPipelineStages(stages);
567
642
  } catch {
568
- setPipelineStages([]);
643
+ if (mountedRef.current) setPipelineStages([]);
569
644
  }
570
645
  }, []);
571
646
  React.useEffect(() => {
572
647
  let cancelled = false;
573
648
  (async () => {
574
649
  try {
575
- const call = await apiCall("/api/customers/pipelines");
576
- if (cancelled) return;
577
- if (call.ok && call.result?.items) {
578
- setPipelines(call.result.items);
579
- }
650
+ const loaded = await fetchPipelineOptions();
651
+ if (cancelled || !mountedRef.current) return;
652
+ setPipelines(mergePipelineOptions(seedPipelineOptions, loaded));
580
653
  } catch {
654
+ if (!cancelled && mountedRef.current && seedPipelineOptions.length > 0) {
655
+ setPipelines(seedPipelineOptions);
656
+ }
581
657
  }
582
658
  })().catch(() => {
583
659
  });
584
660
  return () => {
585
661
  cancelled = true;
586
662
  };
587
- }, []);
663
+ }, [seedPipelineOptions]);
588
664
  React.useEffect(() => {
589
665
  const pid = initialValues?.pipelineId;
590
666
  if (typeof pid === "string" && pid.length) {
667
+ if (seedPipelineStageOptions) {
668
+ setPipelineStages(seedPipelineStageOptions);
669
+ return;
670
+ }
591
671
  loadStagesForPipeline(pid).catch(() => {
592
672
  });
673
+ } else {
674
+ setPipelineStages([]);
593
675
  }
594
- }, [initialValues?.pipelineId, loadStagesForPipeline]);
676
+ }, [initialValues?.pipelineId, loadStagesForPipeline, seedPipelineStageOptions]);
595
677
  const baseFields = React.useMemo(() => [
596
678
  {
597
679
  id: "title",
@@ -899,6 +981,7 @@ export {
899
981
  buildDealValidationError,
900
982
  dealFormSchema,
901
983
  DealForm_default as default,
984
+ resetDealPipelineMetadataCacheForTests,
902
985
  useDealAssociationLookups
903
986
  };
904
987
  //# sourceMappingURL=DealForm.js.map