@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,402 @@
1
+ import { NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
+ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
+ import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
6
+ import { fetchStuckDealIds } from "../../../lib/stuckDeals.js";
7
+ import {
8
+ computeDelta,
9
+ convertSumsToBase,
10
+ getPreviousQuarterWindow,
11
+ getQuarterWindow,
12
+ getTrailingMonths
13
+ } from "../../../lib/dealsMetrics.js";
14
+ const metadata = {
15
+ GET: { requireAuth: true, requireFeatures: ["customers.deals.view"] }
16
+ };
17
+ const OPEN_STATUSES = ["open", "in_progress"];
18
+ const TRAILING_MONTHS = 6;
19
+ const TOP_OWNERS = 5;
20
+ const deltaSchema = z.object({
21
+ value: z.number(),
22
+ direction: z.enum(["up", "down", "unchanged"])
23
+ });
24
+ const stageBreakdownSchema = z.object({
25
+ stage: z.string().nullable(),
26
+ count: z.number(),
27
+ value: z.number()
28
+ });
29
+ const ownerCountSchema = z.object({
30
+ id: z.string(),
31
+ count: z.number()
32
+ });
33
+ const winRatePointSchema = z.object({
34
+ period: z.string(),
35
+ rate: z.number()
36
+ });
37
+ const summaryResponseSchema = z.object({
38
+ baseCurrencyCode: z.string().nullable(),
39
+ convertedAll: z.boolean(),
40
+ missingRateCurrencies: z.array(z.string()),
41
+ pipelineValue: z.object({
42
+ value: z.number(),
43
+ delta: deltaSchema,
44
+ stages: z.array(stageBreakdownSchema)
45
+ }),
46
+ activeDeals: z.object({
47
+ value: z.number(),
48
+ delta: deltaSchema,
49
+ ownersCount: z.number(),
50
+ needAttention: z.number(),
51
+ owners: z.array(ownerCountSchema),
52
+ ownersOverflow: z.number()
53
+ }),
54
+ wonThisQuarter: z.object({
55
+ value: z.number(),
56
+ delta: deltaSchema,
57
+ dealsClosed: z.number(),
58
+ avgDeal: z.number()
59
+ }),
60
+ winRate: z.object({
61
+ value: z.number(),
62
+ deltaPp: z.number(),
63
+ direction: z.enum(["up", "down", "unchanged"]),
64
+ previousValue: z.number(),
65
+ series: z.array(winRatePointSchema)
66
+ })
67
+ });
68
+ const summaryErrorSchema = z.object({
69
+ error: z.string()
70
+ });
71
+ const openApi = {
72
+ tag: "Customers",
73
+ summary: "Deals KPI summary",
74
+ methods: {
75
+ GET: {
76
+ summary: "Pipeline KPI metrics with period-over-period deltas for the deals list",
77
+ description: "Returns the four list-level KPI cards (pipeline value, active deals, won this quarter, win rate) with quarter-over-quarter deltas, per-stage open-pipeline breakdown, top owners, and a 6-month win-rate series. Values are converted to the tenant base currency where rates are available; partial conversions are disclosed via convertedAll/missingRateCurrencies.",
78
+ responses: [
79
+ { status: 200, description: "Deals KPI summary payload", schema: summaryResponseSchema }
80
+ ],
81
+ errors: [
82
+ { status: 401, description: "Unauthorized", schema: summaryErrorSchema }
83
+ ]
84
+ }
85
+ }
86
+ };
87
+ function toNumber(value) {
88
+ const parsed = Number(value ?? 0);
89
+ return Number.isFinite(parsed) ? parsed : 0;
90
+ }
91
+ function winRate(won, lost) {
92
+ const denom = won + lost;
93
+ if (denom <= 0) return 0;
94
+ return Math.round(100 * won / denom);
95
+ }
96
+ function sumsByCurrency(entries) {
97
+ const byCurrency = /* @__PURE__ */ new Map();
98
+ for (const entry of entries) {
99
+ const currency = (entry.currency ?? "").toString().trim().toUpperCase();
100
+ if (!currency) continue;
101
+ byCurrency.set(currency, (byCurrency.get(currency) ?? 0) + entry.total);
102
+ }
103
+ return Array.from(byCurrency.entries()).map(([currency, total]) => ({ currency, total }));
104
+ }
105
+ async function GET(req) {
106
+ const auth = await getAuthFromRequest(req);
107
+ if (!auth?.tenantId || !auth.orgId) {
108
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
109
+ }
110
+ const container = await createRequestContainer();
111
+ const em = container.resolve("em");
112
+ const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req });
113
+ const effectiveTenantId = scope.tenantId ?? auth.tenantId;
114
+ const orgFilterIds = Array.isArray(scope.filterIds) && scope.filterIds.length > 0 ? scope.filterIds.filter((id) => typeof id === "string" && id.length > 0) : auth.orgId ? [auth.orgId] : [];
115
+ if (!effectiveTenantId || orgFilterIds.length === 0) {
116
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
117
+ }
118
+ const today = /* @__PURE__ */ new Date();
119
+ const currentQuarter = getQuarterWindow(today);
120
+ const previousQuarter = getPreviousQuarterWindow(today);
121
+ const trailingMonths = getTrailingMonths(today, TRAILING_MONTHS);
122
+ const seriesStart = trailingMonths[0]?.start ?? currentQuarter.start;
123
+ const connection = em.getConnection();
124
+ const baseCurrency = await connection.execute(
125
+ `SELECT code FROM currencies WHERE tenant_id = ? AND organization_id = ? AND is_base = true AND deleted_at IS NULL LIMIT 1`,
126
+ [effectiveTenantId, orgFilterIds[0]]
127
+ );
128
+ const baseCurrencyCode = baseCurrency[0]?.code ?? null;
129
+ const orgPlaceholders = orgFilterIds.map(() => "?").join(",");
130
+ const scopeWhere = `tenant_id = ? AND organization_id IN (${orgPlaceholders}) AND deleted_at IS NULL`;
131
+ const scopeValues = [effectiveTenantId, ...orgFilterIds];
132
+ const openPlaceholders = OPEN_STATUSES.map(() => "?").join(",");
133
+ const openRows = await connection.execute(
134
+ `SELECT
135
+ pipeline_stage AS stage,
136
+ UPPER(COALESCE(value_currency, '')) AS currency,
137
+ COALESCE(SUM(value_amount), 0) AS total,
138
+ COUNT(*) AS count,
139
+ owner_user_id
140
+ FROM customer_deals
141
+ WHERE ${scopeWhere} AND status IN (${openPlaceholders})
142
+ GROUP BY pipeline_stage, UPPER(COALESCE(value_currency, '')), owner_user_id`,
143
+ [...scopeValues, ...OPEN_STATUSES]
144
+ );
145
+ const inflowRows = await connection.execute(
146
+ `SELECT
147
+ UPPER(COALESCE(value_currency, '')) AS currency,
148
+ COALESCE(SUM(value_amount) FILTER (WHERE created_at >= ? AND created_at < ?), 0) AS current_total,
149
+ COUNT(*) FILTER (WHERE created_at >= ? AND created_at < ?) AS current_count,
150
+ COALESCE(SUM(value_amount) FILTER (WHERE created_at >= ? AND created_at < ?), 0) AS previous_total,
151
+ COUNT(*) FILTER (WHERE created_at >= ? AND created_at < ?) AS previous_count
152
+ FROM customer_deals
153
+ WHERE ${scopeWhere} AND status IN (${openPlaceholders})
154
+ GROUP BY UPPER(COALESCE(value_currency, ''))`,
155
+ [
156
+ currentQuarter.start.toISOString(),
157
+ currentQuarter.end.toISOString(),
158
+ currentQuarter.start.toISOString(),
159
+ currentQuarter.end.toISOString(),
160
+ previousQuarter.start.toISOString(),
161
+ previousQuarter.end.toISOString(),
162
+ previousQuarter.start.toISOString(),
163
+ previousQuarter.end.toISOString(),
164
+ ...scopeValues,
165
+ ...OPEN_STATUSES
166
+ ]
167
+ );
168
+ const wonRows = await connection.execute(
169
+ `SELECT
170
+ UPPER(COALESCE(value_currency, '')) AS currency,
171
+ COALESCE(SUM(value_amount) FILTER (WHERE updated_at >= ? AND updated_at < ?), 0) AS current_total,
172
+ COUNT(*) FILTER (WHERE updated_at >= ? AND updated_at < ?) AS current_count,
173
+ COALESCE(SUM(value_amount) FILTER (WHERE updated_at >= ? AND updated_at < ?), 0) AS previous_total,
174
+ COUNT(*) FILTER (WHERE updated_at >= ? AND updated_at < ?) AS previous_count
175
+ FROM customer_deals
176
+ WHERE ${scopeWhere} AND (status = 'win' OR closure_outcome = 'won')
177
+ GROUP BY UPPER(COALESCE(value_currency, ''))`,
178
+ [
179
+ currentQuarter.start.toISOString(),
180
+ currentQuarter.end.toISOString(),
181
+ currentQuarter.start.toISOString(),
182
+ currentQuarter.end.toISOString(),
183
+ previousQuarter.start.toISOString(),
184
+ previousQuarter.end.toISOString(),
185
+ previousQuarter.start.toISOString(),
186
+ previousQuarter.end.toISOString(),
187
+ ...scopeValues
188
+ ]
189
+ );
190
+ const winLossRows = await connection.execute(
191
+ `SELECT
192
+ COUNT(*) FILTER (WHERE (status = 'win' OR closure_outcome = 'won') AND updated_at >= ? AND updated_at < ?) AS current_won,
193
+ COUNT(*) FILTER (WHERE (status = 'loose' OR closure_outcome = 'lost') AND updated_at >= ? AND updated_at < ?) AS current_lost,
194
+ COUNT(*) FILTER (WHERE (status = 'win' OR closure_outcome = 'won') AND updated_at >= ? AND updated_at < ?) AS previous_won,
195
+ COUNT(*) FILTER (WHERE (status = 'loose' OR closure_outcome = 'lost') AND updated_at >= ? AND updated_at < ?) AS previous_lost
196
+ FROM customer_deals
197
+ WHERE ${scopeWhere}`,
198
+ [
199
+ currentQuarter.start.toISOString(),
200
+ currentQuarter.end.toISOString(),
201
+ currentQuarter.start.toISOString(),
202
+ currentQuarter.end.toISOString(),
203
+ previousQuarter.start.toISOString(),
204
+ previousQuarter.end.toISOString(),
205
+ previousQuarter.start.toISOString(),
206
+ previousQuarter.end.toISOString(),
207
+ ...scopeValues
208
+ ]
209
+ );
210
+ const seriesRows = await connection.execute(
211
+ `SELECT
212
+ to_char(date_trunc('month', updated_at AT TIME ZONE 'UTC'), 'YYYY-MM') AS period,
213
+ COUNT(*) FILTER (WHERE status = 'win' OR closure_outcome = 'won') AS won,
214
+ COUNT(*) FILTER (WHERE status = 'loose' OR closure_outcome = 'lost') AS lost
215
+ FROM customer_deals
216
+ WHERE ${scopeWhere} AND updated_at >= ?
217
+ GROUP BY 1`,
218
+ [...scopeValues, seriesStart.toISOString()]
219
+ );
220
+ const overdueRows = await connection.execute(
221
+ `SELECT id FROM customer_deals
222
+ WHERE ${scopeWhere} AND status = 'open' AND expected_close_at IS NOT NULL AND expected_close_at < CURRENT_DATE`,
223
+ [...scopeValues]
224
+ );
225
+ const stuckIdLists = await Promise.all(
226
+ orgFilterIds.map((orgId) => fetchStuckDealIds(em, orgId, effectiveTenantId))
227
+ );
228
+ const stuckIdSet = /* @__PURE__ */ new Set();
229
+ for (const list of stuckIdLists) for (const id of list) stuckIdSet.add(id);
230
+ let openStuckIds = [];
231
+ if (stuckIdSet.size > 0) {
232
+ const stuckIdValues = Array.from(stuckIdSet);
233
+ const stuckPlaceholders = stuckIdValues.map(() => "?").join(",");
234
+ const openStuckRows = await connection.execute(
235
+ `SELECT id FROM customer_deals
236
+ WHERE ${scopeWhere} AND status IN (${openPlaceholders}) AND id IN (${stuckPlaceholders})`,
237
+ [...scopeValues, ...OPEN_STATUSES, ...stuckIdValues]
238
+ );
239
+ openStuckIds = openStuckRows.map((row) => row.id);
240
+ }
241
+ const attentionIds = /* @__PURE__ */ new Set();
242
+ for (const row of overdueRows) attentionIds.add(row.id);
243
+ for (const id of openStuckIds) attentionIds.add(id);
244
+ const stageMap = /* @__PURE__ */ new Map();
245
+ const openOwnerCounts = /* @__PURE__ */ new Map();
246
+ const openSums = [];
247
+ for (const row of openRows) {
248
+ const stageKey = row.stage ?? "__null__";
249
+ const total = toNumber(row.total);
250
+ const count = toNumber(row.count);
251
+ const currency = (row.currency ?? "").toString().trim().toUpperCase();
252
+ if (!stageMap.has(stageKey)) {
253
+ stageMap.set(stageKey, { stage: row.stage ?? null, count: 0, byCurrency: [] });
254
+ }
255
+ const stageAgg = stageMap.get(stageKey);
256
+ stageAgg.count += count;
257
+ if (currency) stageAgg.byCurrency.push({ currency, total });
258
+ openSums.push({ currency, total });
259
+ if (row.owner_user_id) {
260
+ openOwnerCounts.set(row.owner_user_id, (openOwnerCounts.get(row.owner_user_id) ?? 0) + count);
261
+ }
262
+ }
263
+ const distinctCurrencies = /* @__PURE__ */ new Set();
264
+ const collect = (entries) => {
265
+ for (const entry of entries) {
266
+ const currency = (entry.currency ?? "").toString().trim().toUpperCase();
267
+ if (currency && currency !== baseCurrencyCode) distinctCurrencies.add(currency);
268
+ }
269
+ };
270
+ collect(openSums);
271
+ collect(inflowRows);
272
+ collect(wonRows);
273
+ let rates = /* @__PURE__ */ new Map();
274
+ if (baseCurrencyCode && distinctCurrencies.size > 0) {
275
+ const exchange = container.resolve("exchangeRateService");
276
+ if (exchange) {
277
+ const pairs = Array.from(distinctCurrencies).map((code) => ({
278
+ fromCurrencyCode: code,
279
+ toCurrencyCode: baseCurrencyCode
280
+ }));
281
+ try {
282
+ rates = await exchange.getRates({
283
+ pairs,
284
+ date: today,
285
+ scope: { tenantId: effectiveTenantId, organizationId: orgFilterIds[0] },
286
+ options: { maxDaysBack: 60, autoFetch: false }
287
+ });
288
+ } catch (err) {
289
+ console.warn("[customers.deals.summary] exchange-rate lookup failed; falling back to per-currency totals", err);
290
+ }
291
+ }
292
+ }
293
+ const missingRateCurrencies = /* @__PURE__ */ new Set();
294
+ const trackMissing = (missing) => {
295
+ for (const code of missing) missingRateCurrencies.add(code);
296
+ };
297
+ let convertedAll = true;
298
+ const dominantCurrencyTotal = (entries) => {
299
+ const byCurrency = /* @__PURE__ */ new Map();
300
+ for (const entry of entries) {
301
+ const currency = (entry.currency ?? "").toString().trim().toUpperCase();
302
+ if (!currency) continue;
303
+ byCurrency.set(currency, (byCurrency.get(currency) ?? 0) + entry.total);
304
+ }
305
+ let best = 0;
306
+ for (const total of byCurrency.values()) {
307
+ if (Math.abs(total) > Math.abs(best)) best = total;
308
+ }
309
+ return Math.round(best);
310
+ };
311
+ const convert = (entries) => {
312
+ if (!baseCurrencyCode) {
313
+ convertedAll = false;
314
+ trackMissing(sumsByCurrency(entries).map((entry) => entry.currency));
315
+ return dominantCurrencyTotal(entries);
316
+ }
317
+ const result = convertSumsToBase(sumsByCurrency(entries), baseCurrencyCode, rates);
318
+ if (!result.convertedAll) convertedAll = false;
319
+ trackMissing(result.missingRateCurrencies);
320
+ return result.total;
321
+ };
322
+ const pipelineValueTotal = convert(openSums);
323
+ const stages = Array.from(stageMap.values()).map((stageAgg) => ({
324
+ stage: stageAgg.stage,
325
+ count: stageAgg.count,
326
+ value: convert(stageAgg.byCurrency)
327
+ }));
328
+ const inflowCurrent = convert(inflowRows.map((row) => ({ currency: row.currency, total: toNumber(row.current_total) })));
329
+ const inflowPrevious = convert(inflowRows.map((row) => ({ currency: row.currency, total: toNumber(row.previous_total) })));
330
+ const pipelineDelta = computeDelta(inflowCurrent, inflowPrevious);
331
+ const activeDealsCount = openRows.reduce((sum, row) => sum + toNumber(row.count), 0);
332
+ const ownersCount = openOwnerCounts.size;
333
+ const inflowCurrentCount = inflowRows.reduce((sum, row) => sum + toNumber(row.current_count), 0);
334
+ const inflowPreviousCount = inflowRows.reduce((sum, row) => sum + toNumber(row.previous_count), 0);
335
+ const activeDelta = computeDelta(inflowCurrentCount, inflowPreviousCount);
336
+ const sortedOwners = Array.from(openOwnerCounts.entries()).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
337
+ const owners = sortedOwners.slice(0, TOP_OWNERS).map(([id, count]) => ({ id, count }));
338
+ const ownersOverflow = Math.max(0, ownersCount - owners.length);
339
+ const wonCurrent = convert(wonRows.map((row) => ({ currency: row.currency, total: toNumber(row.current_total) })));
340
+ const wonPrevious = convert(wonRows.map((row) => ({ currency: row.currency, total: toNumber(row.previous_total) })));
341
+ const dealsClosed = wonRows.reduce((sum, row) => sum + toNumber(row.current_count), 0);
342
+ const wonDelta = computeDelta(wonCurrent, wonPrevious);
343
+ const avgDeal = dealsClosed > 0 ? Math.round(wonCurrent / dealsClosed) : 0;
344
+ const winLoss = winLossRows[0];
345
+ const currentWon = toNumber(winLoss?.current_won);
346
+ const currentLost = toNumber(winLoss?.current_lost);
347
+ const previousWon = toNumber(winLoss?.previous_won);
348
+ const previousLost = toNumber(winLoss?.previous_lost);
349
+ const winRateValue = winRate(currentWon, currentLost);
350
+ const winRatePrevious = winRate(previousWon, previousLost);
351
+ const deltaPp = winRateValue - winRatePrevious;
352
+ const winRateDirection = deltaPp > 0 ? "up" : deltaPp < 0 ? "down" : "unchanged";
353
+ const seriesByPeriod = /* @__PURE__ */ new Map();
354
+ for (const row of seriesRows) {
355
+ seriesByPeriod.set(row.period, { won: toNumber(row.won), lost: toNumber(row.lost) });
356
+ }
357
+ const series = trailingMonths.map((month) => {
358
+ const point = seriesByPeriod.get(month.label);
359
+ const won = point?.won ?? 0;
360
+ const lost = point?.lost ?? 0;
361
+ const denom = won + lost;
362
+ return { period: month.label, rate: denom > 0 ? won / denom : 0 };
363
+ });
364
+ const response = {
365
+ baseCurrencyCode,
366
+ convertedAll,
367
+ missingRateCurrencies: Array.from(missingRateCurrencies),
368
+ pipelineValue: {
369
+ value: pipelineValueTotal,
370
+ delta: pipelineDelta,
371
+ stages
372
+ },
373
+ activeDeals: {
374
+ value: activeDealsCount,
375
+ delta: activeDelta,
376
+ ownersCount,
377
+ needAttention: attentionIds.size,
378
+ owners,
379
+ ownersOverflow
380
+ },
381
+ wonThisQuarter: {
382
+ value: wonCurrent,
383
+ delta: wonDelta,
384
+ dealsClosed,
385
+ avgDeal
386
+ },
387
+ winRate: {
388
+ value: winRateValue,
389
+ deltaPp,
390
+ direction: winRateDirection,
391
+ previousValue: winRatePrevious,
392
+ series
393
+ }
394
+ };
395
+ return NextResponse.json(response);
396
+ }
397
+ export {
398
+ GET,
399
+ metadata,
400
+ openApi
401
+ };
402
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../src/modules/customers/api/deals/summary/route.ts"],
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager as CoreEntityManager } from '@mikro-orm/core'\nimport type { EntityManager as PgEntityManager } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { ExchangeRateService, RateResult } from '@open-mercato/core/modules/currencies/services/exchangeRateService'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { fetchStuckDealIds } from '../../../lib/stuckDeals'\nimport {\n computeDelta,\n convertSumsToBase,\n getPreviousQuarterWindow,\n getQuarterWindow,\n getTrailingMonths,\n type CurrencySum,\n type Delta,\n} from '../../../lib/dealsMetrics'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.deals.view'] },\n}\n\nconst OPEN_STATUSES = ['open', 'in_progress'] as const\nconst TRAILING_MONTHS = 6\nconst TOP_OWNERS = 5\n\nconst deltaSchema = z.object({\n value: z.number(),\n direction: z.enum(['up', 'down', 'unchanged']),\n})\n\nconst stageBreakdownSchema = z.object({\n stage: z.string().nullable(),\n count: z.number(),\n value: z.number(),\n})\n\nconst ownerCountSchema = z.object({\n id: z.string(),\n count: z.number(),\n})\n\nconst winRatePointSchema = z.object({\n period: z.string(),\n rate: z.number(),\n})\n\nconst summaryResponseSchema = z.object({\n baseCurrencyCode: z.string().nullable(),\n convertedAll: z.boolean(),\n missingRateCurrencies: z.array(z.string()),\n pipelineValue: z.object({\n value: z.number(),\n delta: deltaSchema,\n stages: z.array(stageBreakdownSchema),\n }),\n activeDeals: z.object({\n value: z.number(),\n delta: deltaSchema,\n ownersCount: z.number(),\n needAttention: z.number(),\n owners: z.array(ownerCountSchema),\n ownersOverflow: z.number(),\n }),\n wonThisQuarter: z.object({\n value: z.number(),\n delta: deltaSchema,\n dealsClosed: z.number(),\n avgDeal: z.number(),\n }),\n winRate: z.object({\n value: z.number(),\n deltaPp: z.number(),\n direction: z.enum(['up', 'down', 'unchanged']),\n previousValue: z.number(),\n series: z.array(winRatePointSchema),\n }),\n})\n\nexport type DealsSummaryResponse = z.infer<typeof summaryResponseSchema>\n\nconst summaryErrorSchema = z.object({\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n summary: 'Deals KPI summary',\n methods: {\n GET: {\n summary: 'Pipeline KPI metrics with period-over-period deltas for the deals list',\n description:\n 'Returns the four list-level KPI cards (pipeline value, active deals, won this quarter, win rate) with quarter-over-quarter deltas, per-stage open-pipeline breakdown, top owners, and a 6-month win-rate series. Values are converted to the tenant base currency where rates are available; partial conversions are disclosed via convertedAll/missingRateCurrencies.',\n responses: [\n { status: 200, description: 'Deals KPI summary payload', schema: summaryResponseSchema },\n ],\n errors: [\n { status: 401, description: 'Unauthorized', schema: summaryErrorSchema },\n ],\n },\n },\n}\n\ntype OpenPipelineRow = {\n stage: string | null\n currency: string | null\n total: string | number | null\n count: string | number\n owner_user_id: string | null\n}\n\ntype WindowSumRow = {\n currency: string | null\n current_total: string | number | null\n current_count: string | number\n previous_total: string | number | null\n previous_count: string | number\n}\n\ntype WinLossRow = {\n current_won: string | number\n current_lost: string | number\n previous_won: string | number\n previous_lost: string | number\n}\n\ntype WinRateMonthRow = {\n period: string\n won: string | number\n lost: string | number\n}\n\ntype OwnerCountRow = {\n owner_user_id: string | null\n count: string | number\n}\n\nfunction toNumber(value: string | number | null | undefined): number {\n const parsed = Number(value ?? 0)\n return Number.isFinite(parsed) ? parsed : 0\n}\n\nfunction winRate(won: number, lost: number): number {\n const denom = won + lost\n if (denom <= 0) return 0\n return Math.round((100 * won) / denom)\n}\n\nfunction sumsByCurrency(entries: Array<{ currency: string | null; total: number }>): CurrencySum[] {\n const byCurrency = new Map<string, number>()\n for (const entry of entries) {\n const currency = (entry.currency ?? '').toString().trim().toUpperCase()\n if (!currency) continue\n byCurrency.set(currency, (byCurrency.get(currency) ?? 0) + entry.total)\n }\n return Array.from(byCurrency.entries()).map(([currency, total]) => ({ currency, total }))\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const em = container.resolve<CoreEntityManager>('em')\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const effectiveTenantId = scope.tenantId ?? auth.tenantId\n const orgFilterIds = Array.isArray(scope.filterIds) && scope.filterIds.length > 0\n ? scope.filterIds.filter((id) => typeof id === 'string' && id.length > 0)\n : auth.orgId\n ? [auth.orgId]\n : []\n if (!effectiveTenantId || orgFilterIds.length === 0) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const today = new Date()\n const currentQuarter = getQuarterWindow(today)\n const previousQuarter = getPreviousQuarterWindow(today)\n const trailingMonths = getTrailingMonths(today, TRAILING_MONTHS)\n const seriesStart = trailingMonths[0]?.start ?? currentQuarter.start\n\n const connection = em.getConnection()\n\n const baseCurrency: Array<{ code: string }> = await connection.execute<Array<{ code: string }>>(\n `SELECT code FROM currencies WHERE tenant_id = ? AND organization_id = ? AND is_base = true AND deleted_at IS NULL LIMIT 1`,\n [effectiveTenantId, orgFilterIds[0]],\n )\n const baseCurrencyCode = baseCurrency[0]?.code ?? null\n\n const orgPlaceholders = orgFilterIds.map(() => '?').join(',')\n const scopeWhere = `tenant_id = ? AND organization_id IN (${orgPlaceholders}) AND deleted_at IS NULL`\n const scopeValues: Array<string | number | null> = [effectiveTenantId, ...orgFilterIds]\n const openPlaceholders = OPEN_STATUSES.map(() => '?').join(',')\n\n // 1) Open pipeline: per (stage, currency) sums + open-deal owner per row, so we can\n // derive pipeline value (per stage + converted total) and the open owner set in one pass.\n const openRows: OpenPipelineRow[] = await connection.execute<OpenPipelineRow[]>(\n `SELECT\n pipeline_stage AS stage,\n UPPER(COALESCE(value_currency, '')) AS currency,\n COALESCE(SUM(value_amount), 0) AS total,\n COUNT(*) AS count,\n owner_user_id\n FROM customer_deals\n WHERE ${scopeWhere} AND status IN (${openPlaceholders})\n GROUP BY pipeline_stage, UPPER(COALESCE(value_currency, '')), owner_user_id`,\n [...scopeValues, ...OPEN_STATUSES],\n )\n\n // 2) Open-deal value created in the current vs previous quarter (pipeline inflow delta).\n const inflowRows: WindowSumRow[] = await connection.execute<WindowSumRow[]>(\n `SELECT\n UPPER(COALESCE(value_currency, '')) AS currency,\n COALESCE(SUM(value_amount) FILTER (WHERE created_at >= ? AND created_at < ?), 0) AS current_total,\n COUNT(*) FILTER (WHERE created_at >= ? AND created_at < ?) AS current_count,\n COALESCE(SUM(value_amount) FILTER (WHERE created_at >= ? AND created_at < ?), 0) AS previous_total,\n COUNT(*) FILTER (WHERE created_at >= ? AND created_at < ?) AS previous_count\n FROM customer_deals\n WHERE ${scopeWhere} AND status IN (${openPlaceholders})\n GROUP BY UPPER(COALESCE(value_currency, ''))`,\n [\n currentQuarter.start.toISOString(), currentQuarter.end.toISOString(),\n currentQuarter.start.toISOString(), currentQuarter.end.toISOString(),\n previousQuarter.start.toISOString(), previousQuarter.end.toISOString(),\n previousQuarter.start.toISOString(), previousQuarter.end.toISOString(),\n ...scopeValues, ...OPEN_STATUSES,\n ],\n )\n\n // 3) Won value per currency for the current vs previous quarter (updated_at in window).\n const wonRows: WindowSumRow[] = await connection.execute<WindowSumRow[]>(\n `SELECT\n UPPER(COALESCE(value_currency, '')) AS currency,\n COALESCE(SUM(value_amount) FILTER (WHERE updated_at >= ? AND updated_at < ?), 0) AS current_total,\n COUNT(*) FILTER (WHERE updated_at >= ? AND updated_at < ?) AS current_count,\n COALESCE(SUM(value_amount) FILTER (WHERE updated_at >= ? AND updated_at < ?), 0) AS previous_total,\n COUNT(*) FILTER (WHERE updated_at >= ? AND updated_at < ?) AS previous_count\n FROM customer_deals\n WHERE ${scopeWhere} AND (status = 'win' OR closure_outcome = 'won')\n GROUP BY UPPER(COALESCE(value_currency, ''))`,\n [\n currentQuarter.start.toISOString(), currentQuarter.end.toISOString(),\n currentQuarter.start.toISOString(), currentQuarter.end.toISOString(),\n previousQuarter.start.toISOString(), previousQuarter.end.toISOString(),\n previousQuarter.start.toISOString(), previousQuarter.end.toISOString(),\n ...scopeValues,\n ],\n )\n\n // 4) Win/lost counts for the current vs previous quarter (win rate + delta-pp).\n const winLossRows: WinLossRow[] = await connection.execute<WinLossRow[]>(\n `SELECT\n COUNT(*) FILTER (WHERE (status = 'win' OR closure_outcome = 'won') AND updated_at >= ? AND updated_at < ?) AS current_won,\n COUNT(*) FILTER (WHERE (status = 'loose' OR closure_outcome = 'lost') AND updated_at >= ? AND updated_at < ?) AS current_lost,\n COUNT(*) FILTER (WHERE (status = 'win' OR closure_outcome = 'won') AND updated_at >= ? AND updated_at < ?) AS previous_won,\n COUNT(*) FILTER (WHERE (status = 'loose' OR closure_outcome = 'lost') AND updated_at >= ? AND updated_at < ?) AS previous_lost\n FROM customer_deals\n WHERE ${scopeWhere}`,\n [\n currentQuarter.start.toISOString(), currentQuarter.end.toISOString(),\n currentQuarter.start.toISOString(), currentQuarter.end.toISOString(),\n previousQuarter.start.toISOString(), previousQuarter.end.toISOString(),\n previousQuarter.start.toISOString(), previousQuarter.end.toISOString(),\n ...scopeValues,\n ],\n )\n\n // 5) Win-rate series over the trailing months (won/lost grouped by updated_at month).\n const seriesRows: WinRateMonthRow[] = await connection.execute<WinRateMonthRow[]>(\n `SELECT\n to_char(date_trunc('month', updated_at AT TIME ZONE 'UTC'), 'YYYY-MM') AS period,\n COUNT(*) FILTER (WHERE status = 'win' OR closure_outcome = 'won') AS won,\n COUNT(*) FILTER (WHERE status = 'loose' OR closure_outcome = 'lost') AS lost\n FROM customer_deals\n WHERE ${scopeWhere} AND updated_at >= ?\n GROUP BY 1`,\n [...scopeValues, seriesStart.toISOString()],\n )\n\n // Overdue open deals (id set) + stuck deals (id set) \u2192 union count for \"need attention\".\n const overdueRows: Array<{ id: string }> = await connection.execute<Array<{ id: string }>>(\n `SELECT id FROM customer_deals\n WHERE ${scopeWhere} AND status = 'open' AND expected_close_at IS NOT NULL AND expected_close_at < CURRENT_DATE`,\n [...scopeValues],\n )\n // `fetchStuckDealIds` is single-org; run it for every org in scope so multi-org callers don't\n // undercount stuck deals (the aggregates above already span every org in `orgFilterIds`).\n const stuckIdLists = await Promise.all(\n orgFilterIds.map((orgId) =>\n fetchStuckDealIds(em as unknown as PgEntityManager, orgId, effectiveTenantId)),\n )\n const stuckIdSet = new Set<string>()\n for (const list of stuckIdLists) for (const id of list) stuckIdSet.add(id)\n\n // The stuck-deal query does not filter status, so a stuck id can be a won/lost/closed deal.\n // \"Need attention\" is an active-deal metric \u2014 intersect with the open (OPEN_STATUSES) set so\n // terminal deals never inflate the count.\n let openStuckIds: string[] = []\n if (stuckIdSet.size > 0) {\n const stuckIdValues = Array.from(stuckIdSet)\n const stuckPlaceholders = stuckIdValues.map(() => '?').join(',')\n const openStuckRows: Array<{ id: string }> = await connection.execute<Array<{ id: string }>>(\n `SELECT id FROM customer_deals\n WHERE ${scopeWhere} AND status IN (${openPlaceholders}) AND id IN (${stuckPlaceholders})`,\n [...scopeValues, ...OPEN_STATUSES, ...stuckIdValues],\n )\n openStuckIds = openStuckRows.map((row) => row.id)\n }\n\n const attentionIds = new Set<string>()\n for (const row of overdueRows) attentionIds.add(row.id)\n for (const id of openStuckIds) attentionIds.add(id)\n\n // Reduce open rows: per-stage sums, distinct owners, owner counts, and a flat\n // per-currency list for the converted pipeline total.\n const stageMap = new Map<string, { stage: string | null; count: number; byCurrency: CurrencySum[] }>()\n const openOwnerCounts = new Map<string, number>()\n const openSums: Array<{ currency: string | null; total: number }> = []\n for (const row of openRows) {\n const stageKey = row.stage ?? '__null__'\n const total = toNumber(row.total)\n const count = toNumber(row.count)\n const currency = (row.currency ?? '').toString().trim().toUpperCase()\n if (!stageMap.has(stageKey)) {\n stageMap.set(stageKey, { stage: row.stage ?? null, count: 0, byCurrency: [] })\n }\n const stageAgg = stageMap.get(stageKey)!\n stageAgg.count += count\n if (currency) stageAgg.byCurrency.push({ currency, total })\n openSums.push({ currency, total })\n if (row.owner_user_id) {\n openOwnerCounts.set(row.owner_user_id, (openOwnerCounts.get(row.owner_user_id) ?? 0) + count)\n }\n }\n\n // Collect every distinct non-base currency across all metrics and fetch rates ONCE.\n const distinctCurrencies = new Set<string>()\n const collect = (entries: Array<{ currency: string | null }>) => {\n for (const entry of entries) {\n const currency = (entry.currency ?? '').toString().trim().toUpperCase()\n if (currency && currency !== baseCurrencyCode) distinctCurrencies.add(currency)\n }\n }\n collect(openSums)\n collect(inflowRows)\n collect(wonRows)\n\n let rates = new Map<string, RateResult>()\n if (baseCurrencyCode && distinctCurrencies.size > 0) {\n const exchange = container.resolve('exchangeRateService') as ExchangeRateService | undefined\n if (exchange) {\n const pairs = Array.from(distinctCurrencies).map((code) => ({\n fromCurrencyCode: code,\n toCurrencyCode: baseCurrencyCode,\n }))\n try {\n rates = await exchange.getRates({\n pairs,\n date: today,\n scope: { tenantId: effectiveTenantId, organizationId: orgFilterIds[0] },\n options: { maxDaysBack: 60, autoFetch: false },\n })\n } catch (err) {\n console.warn('[customers.deals.summary] exchange-rate lookup failed; falling back to per-currency totals', err)\n }\n }\n }\n\n const missingRateCurrencies = new Set<string>()\n const trackMissing = (missing: string[]) => {\n for (const code of missing) missingRateCurrencies.add(code)\n }\n let convertedAll = true\n\n // Degraded path: when there is no base currency, fall back to the dominant currency's\n // raw sum so the cards still show a number (mirrors the aggregate route's disclosure).\n const dominantCurrencyTotal = (entries: Array<{ currency: string | null; total: number }>): number => {\n const byCurrency = new Map<string, number>()\n for (const entry of entries) {\n const currency = (entry.currency ?? '').toString().trim().toUpperCase()\n if (!currency) continue\n byCurrency.set(currency, (byCurrency.get(currency) ?? 0) + entry.total)\n }\n let best = 0\n for (const total of byCurrency.values()) {\n if (Math.abs(total) > Math.abs(best)) best = total\n }\n return Math.round(best)\n }\n\n const convert = (entries: Array<{ currency: string | null; total: number }>): number => {\n if (!baseCurrencyCode) {\n convertedAll = false\n trackMissing(sumsByCurrency(entries).map((entry) => entry.currency))\n return dominantCurrencyTotal(entries)\n }\n const result = convertSumsToBase(sumsByCurrency(entries), baseCurrencyCode, rates)\n if (!result.convertedAll) convertedAll = false\n trackMissing(result.missingRateCurrencies)\n return result.total\n }\n\n // Pipeline value (open deals, converted) + per-stage converted breakdown.\n const pipelineValueTotal = convert(openSums)\n const stages = Array.from(stageMap.values()).map((stageAgg) => ({\n stage: stageAgg.stage,\n count: stageAgg.count,\n value: convert(stageAgg.byCurrency),\n }))\n\n // Pipeline inflow delta (open value created this vs previous quarter).\n const inflowCurrent = convert(inflowRows.map((row) => ({ currency: row.currency, total: toNumber(row.current_total) })))\n const inflowPrevious = convert(inflowRows.map((row) => ({ currency: row.currency, total: toNumber(row.previous_total) })))\n const pipelineDelta: Delta = computeDelta(inflowCurrent, inflowPrevious)\n\n // Active deals: count of open deals, owners, need-attention, top owners.\n const activeDealsCount = openRows.reduce((sum, row) => sum + toNumber(row.count), 0)\n const ownersCount = openOwnerCounts.size\n const inflowCurrentCount = inflowRows.reduce((sum, row) => sum + toNumber(row.current_count), 0)\n const inflowPreviousCount = inflowRows.reduce((sum, row) => sum + toNumber(row.previous_count), 0)\n const activeDelta: Delta = computeDelta(inflowCurrentCount, inflowPreviousCount)\n const sortedOwners = Array.from(openOwnerCounts.entries())\n .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))\n const owners = sortedOwners.slice(0, TOP_OWNERS).map(([id, count]) => ({ id, count }))\n const ownersOverflow = Math.max(0, ownersCount - owners.length)\n\n // Won this quarter.\n const wonCurrent = convert(wonRows.map((row) => ({ currency: row.currency, total: toNumber(row.current_total) })))\n const wonPrevious = convert(wonRows.map((row) => ({ currency: row.currency, total: toNumber(row.previous_total) })))\n const dealsClosed = wonRows.reduce((sum, row) => sum + toNumber(row.current_count), 0)\n const wonDelta: Delta = computeDelta(wonCurrent, wonPrevious)\n const avgDeal = dealsClosed > 0 ? Math.round(wonCurrent / dealsClosed) : 0\n\n // Win rate (current + previous quarter) and pp delta.\n const winLoss = winLossRows[0]\n const currentWon = toNumber(winLoss?.current_won)\n const currentLost = toNumber(winLoss?.current_lost)\n const previousWon = toNumber(winLoss?.previous_won)\n const previousLost = toNumber(winLoss?.previous_lost)\n const winRateValue = winRate(currentWon, currentLost)\n const winRatePrevious = winRate(previousWon, previousLost)\n const deltaPp = winRateValue - winRatePrevious\n const winRateDirection = deltaPp > 0 ? 'up' : deltaPp < 0 ? 'down' : 'unchanged'\n\n // Win-rate series over trailing months (fill missing months with 0).\n const seriesByPeriod = new Map<string, { won: number; lost: number }>()\n for (const row of seriesRows) {\n seriesByPeriod.set(row.period, { won: toNumber(row.won), lost: toNumber(row.lost) })\n }\n const series = trailingMonths.map((month) => {\n const point = seriesByPeriod.get(month.label)\n const won = point?.won ?? 0\n const lost = point?.lost ?? 0\n const denom = won + lost\n return { period: month.label, rate: denom > 0 ? won / denom : 0 }\n })\n\n const response: DealsSummaryResponse = {\n baseCurrencyCode,\n convertedAll,\n missingRateCurrencies: Array.from(missingRateCurrencies),\n pipelineValue: {\n value: pipelineValueTotal,\n delta: pipelineDelta,\n stages,\n },\n activeDeals: {\n value: activeDealsCount,\n delta: activeDelta,\n ownersCount,\n needAttention: attentionIds.size,\n owners,\n ownersOverflow,\n },\n wonThisQuarter: {\n value: wonCurrent,\n delta: wonDelta,\n dealsClosed,\n avgDeal,\n },\n winRate: {\n value: winRateValue,\n deltaPp,\n direction: winRateDirection,\n previousValue: winRatePrevious,\n series,\n },\n }\n\n return NextResponse.json(response)\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -1,19 +1,30 @@
1
1
  import * as React from "react";
2
2
  import { readApiResultOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
3
3
  import { useInteractionMutations } from "../../../../../components/detail/hooks/useInteractionMutations.js";
4
+ const plannedActivitiesCache = /* @__PURE__ */ new Map();
5
+ function fetchPlannedActivities(dealId, useCache) {
6
+ const url = `/api/customers/interactions?dealId=${encodeURIComponent(dealId)}&status=planned&excludeInteractionType=task&limit=100&sortField=scheduledAt&sortDir=asc`;
7
+ const cached = plannedActivitiesCache.get(url);
8
+ if (useCache && cached) return cached.promise;
9
+ const entry = {
10
+ promise: readApiResultOrThrow(url).then((result) => Array.isArray(result.items) ? result.items : [])
11
+ };
12
+ if (useCache) plannedActivitiesCache.set(url, entry);
13
+ return entry.promise.finally(() => {
14
+ if (plannedActivitiesCache.get(url) === entry) plannedActivitiesCache.delete(url);
15
+ });
16
+ }
4
17
  function useDealActivities({
5
18
  dealId,
6
19
  runMutationWithContext
7
20
  }) {
8
21
  const [plannedActivities, setPlannedActivities] = React.useState([]);
9
22
  const [activityRefreshKey, setActivityRefreshKey] = React.useState(0);
10
- const loadPlannedActivities = React.useCallback(async () => {
23
+ const loadPlannedActivities = React.useCallback(async (options = {}) => {
11
24
  if (!dealId) return;
12
25
  try {
13
- const result = await readApiResultOrThrow(
14
- `/api/customers/interactions?dealId=${encodeURIComponent(dealId)}&status=planned&excludeInteractionType=task&limit=100&sortField=scheduledAt&sortDir=asc`
15
- );
16
- setPlannedActivities(Array.isArray(result.items) ? result.items : []);
26
+ const items = await fetchPlannedActivities(dealId, options.cache === true);
27
+ setPlannedActivities(items);
17
28
  } catch (err) {
18
29
  console.warn("[customers.deals.detail] load planned activities failed", err);
19
30
  setPlannedActivities([]);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../../src/modules/customers/backend/customers/deals/%5Bid%5D/hooks/useDealActivities.ts"],
4
- "sourcesContent": ["import * as React from 'react'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport type { InteractionSummary } from '../../../../../components/detail/types'\nimport { useInteractionMutations } from '../../../../../components/detail/hooks/useInteractionMutations'\nimport type { GuardedMutationRunner } from './types'\n\ntype UseDealActivitiesOptions = {\n dealId: string\n runMutationWithContext: GuardedMutationRunner\n}\n\ntype UseDealActivitiesResult = {\n plannedActivities: InteractionSummary[]\n activityRefreshKey: number\n loadPlannedActivities: () => Promise<void>\n handleActivityCreated: () => Promise<void>\n handleMarkDone: (interactionId: string) => Promise<void>\n handleCancelActivity: (interactionId: string) => Promise<void>\n}\n\nexport function useDealActivities({\n dealId,\n runMutationWithContext,\n}: UseDealActivitiesOptions): UseDealActivitiesResult {\n const [plannedActivities, setPlannedActivities] = React.useState<InteractionSummary[]>([])\n const [activityRefreshKey, setActivityRefreshKey] = React.useState(0)\n\n const loadPlannedActivities = React.useCallback(async () => {\n if (!dealId) return\n try {\n const result = await readApiResultOrThrow<{ items?: InteractionSummary[] }>(\n `/api/customers/interactions?dealId=${encodeURIComponent(dealId)}&status=planned&excludeInteractionType=task&limit=100&sortField=scheduledAt&sortDir=asc`,\n )\n setPlannedActivities(Array.isArray(result.items) ? result.items : [])\n } catch (err) {\n console.warn('[customers.deals.detail] load planned activities failed', err)\n setPlannedActivities([])\n }\n }, [dealId])\n\n const handleActivityCreated = React.useCallback(async () => {\n setActivityRefreshKey((value) => value + 1)\n await loadPlannedActivities()\n }, [loadPlannedActivities])\n\n const { completeInteraction, cancelInteraction } = useInteractionMutations({\n runMutationWithContext,\n onAfterChange: handleActivityCreated,\n logContext: 'customers.deals.detail',\n })\n\n return {\n plannedActivities,\n activityRefreshKey,\n loadPlannedActivities,\n handleActivityCreated,\n handleMarkDone: completeInteraction,\n handleCancelActivity: cancelInteraction,\n }\n}\n"],
5
- "mappings": "AAAA,YAAY,WAAW;AACvB,SAAS,4BAA4B;AAErC,SAAS,+BAA+B;AAiBjC,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AACF,GAAsD;AACpD,QAAM,CAAC,mBAAmB,oBAAoB,IAAI,MAAM,SAA+B,CAAC,CAAC;AACzF,QAAM,CAAC,oBAAoB,qBAAqB,IAAI,MAAM,SAAS,CAAC;AAEpE,QAAM,wBAAwB,MAAM,YAAY,YAAY;AAC1D,QAAI,CAAC,OAAQ;AACb,QAAI;AACF,YAAM,SAAS,MAAM;AAAA,QACnB,sCAAsC,mBAAmB,MAAM,CAAC;AAAA,MAClE;AACA,2BAAqB,MAAM,QAAQ,OAAO,KAAK,IAAI,OAAO,QAAQ,CAAC,CAAC;AAAA,IACtE,SAAS,KAAK;AACZ,cAAQ,KAAK,2DAA2D,GAAG;AAC3E,2BAAqB,CAAC,CAAC;AAAA,IACzB;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,wBAAwB,MAAM,YAAY,YAAY;AAC1D,0BAAsB,CAAC,UAAU,QAAQ,CAAC;AAC1C,UAAM,sBAAsB;AAAA,EAC9B,GAAG,CAAC,qBAAqB,CAAC;AAE1B,QAAM,EAAE,qBAAqB,kBAAkB,IAAI,wBAAwB;AAAA,IACzE;AAAA,IACA,eAAe;AAAA,IACf,YAAY;AAAA,EACd,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,gBAAgB;AAAA,IAChB,sBAAsB;AAAA,EACxB;AACF;",
4
+ "sourcesContent": ["import * as React from 'react'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport type { InteractionSummary } from '../../../../../components/detail/types'\nimport { useInteractionMutations } from '../../../../../components/detail/hooks/useInteractionMutations'\nimport type { GuardedMutationRunner } from './types'\n\ntype LoadPlannedActivitiesOptions = {\n cache?: boolean\n}\n\ntype UseDealActivitiesOptions = {\n dealId: string\n runMutationWithContext: GuardedMutationRunner\n}\n\ntype UseDealActivitiesResult = {\n plannedActivities: InteractionSummary[]\n activityRefreshKey: number\n loadPlannedActivities: (options?: LoadPlannedActivitiesOptions) => Promise<void>\n handleActivityCreated: () => Promise<void>\n handleMarkDone: (interactionId: string) => Promise<void>\n handleCancelActivity: (interactionId: string) => Promise<void>\n}\n\ntype PlannedActivitiesCacheEntry = {\n promise: Promise<InteractionSummary[]>\n}\n\nconst plannedActivitiesCache = new Map<string, PlannedActivitiesCacheEntry>()\n\nfunction fetchPlannedActivities(dealId: string, useCache: boolean): Promise<InteractionSummary[]> {\n const url = `/api/customers/interactions?dealId=${encodeURIComponent(dealId)}&status=planned&excludeInteractionType=task&limit=100&sortField=scheduledAt&sortDir=asc`\n const cached = plannedActivitiesCache.get(url)\n if (useCache && cached) return cached.promise\n const entry: PlannedActivitiesCacheEntry = {\n promise: readApiResultOrThrow<{ items?: InteractionSummary[] }>(url)\n .then((result) => (Array.isArray(result.items) ? result.items : [])),\n }\n if (useCache) plannedActivitiesCache.set(url, entry)\n return entry.promise.finally(() => {\n if (plannedActivitiesCache.get(url) === entry) plannedActivitiesCache.delete(url)\n })\n}\n\nexport function useDealActivities({\n dealId,\n runMutationWithContext,\n}: UseDealActivitiesOptions): UseDealActivitiesResult {\n const [plannedActivities, setPlannedActivities] = React.useState<InteractionSummary[]>([])\n const [activityRefreshKey, setActivityRefreshKey] = React.useState(0)\n\n const loadPlannedActivities = React.useCallback(async (options: LoadPlannedActivitiesOptions = {}) => {\n if (!dealId) return\n try {\n const items = await fetchPlannedActivities(dealId, options.cache === true)\n setPlannedActivities(items)\n } catch (err) {\n console.warn('[customers.deals.detail] load planned activities failed', err)\n setPlannedActivities([])\n }\n }, [dealId])\n\n const handleActivityCreated = React.useCallback(async () => {\n setActivityRefreshKey((value) => value + 1)\n await loadPlannedActivities()\n }, [loadPlannedActivities])\n\n const { completeInteraction, cancelInteraction } = useInteractionMutations({\n runMutationWithContext,\n onAfterChange: handleActivityCreated,\n logContext: 'customers.deals.detail',\n })\n\n return {\n plannedActivities,\n activityRefreshKey,\n loadPlannedActivities,\n handleActivityCreated,\n handleMarkDone: completeInteraction,\n handleCancelActivity: cancelInteraction,\n }\n}\n"],
5
+ "mappings": "AAAA,YAAY,WAAW;AACvB,SAAS,4BAA4B;AAErC,SAAS,+BAA+B;AAyBxC,MAAM,yBAAyB,oBAAI,IAAyC;AAE5E,SAAS,uBAAuB,QAAgB,UAAkD;AAChG,QAAM,MAAM,sCAAsC,mBAAmB,MAAM,CAAC;AAC5E,QAAM,SAAS,uBAAuB,IAAI,GAAG;AAC7C,MAAI,YAAY,OAAQ,QAAO,OAAO;AACtC,QAAM,QAAqC;AAAA,IACzC,SAAS,qBAAuD,GAAG,EAChE,KAAK,CAAC,WAAY,MAAM,QAAQ,OAAO,KAAK,IAAI,OAAO,QAAQ,CAAC,CAAE;AAAA,EACvE;AACA,MAAI,SAAU,wBAAuB,IAAI,KAAK,KAAK;AACnD,SAAO,MAAM,QAAQ,QAAQ,MAAM;AACjC,QAAI,uBAAuB,IAAI,GAAG,MAAM,MAAO,wBAAuB,OAAO,GAAG;AAAA,EAClF,CAAC;AACH;AAEO,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AACF,GAAsD;AACpD,QAAM,CAAC,mBAAmB,oBAAoB,IAAI,MAAM,SAA+B,CAAC,CAAC;AACzF,QAAM,CAAC,oBAAoB,qBAAqB,IAAI,MAAM,SAAS,CAAC;AAEpE,QAAM,wBAAwB,MAAM,YAAY,OAAO,UAAwC,CAAC,MAAM;AACpG,QAAI,CAAC,OAAQ;AACb,QAAI;AACF,YAAM,QAAQ,MAAM,uBAAuB,QAAQ,QAAQ,UAAU,IAAI;AACzE,2BAAqB,KAAK;AAAA,IAC5B,SAAS,KAAK;AACZ,cAAQ,KAAK,2DAA2D,GAAG;AAC3E,2BAAqB,CAAC,CAAC;AAAA,IACzB;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,wBAAwB,MAAM,YAAY,YAAY;AAC1D,0BAAsB,CAAC,UAAU,QAAQ,CAAC;AAC1C,UAAM,sBAAsB;AAAA,EAC9B,GAAG,CAAC,qBAAqB,CAAC;AAE1B,QAAM,EAAE,qBAAqB,kBAAkB,IAAI,wBAAwB;AAAA,IACzE;AAAA,IACA,eAAe;AAAA,IACf,YAAY;AAAA,EACd,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,gBAAgB;AAAA,IAChB,sBAAsB;AAAA,EACxB;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,23 @@
1
1
  import * as React from "react";
2
2
  import { readApiResultOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
3
3
  import { useT } from "@open-mercato/shared/lib/i18n/context";
4
+ const dealDataCache = /* @__PURE__ */ new Map();
5
+ function fetchDealData(id, errorMessage, useCache) {
6
+ const url = `/api/customers/deals/${encodeURIComponent(id)}?include=stages&view=lite`;
7
+ const cached = dealDataCache.get(url);
8
+ if (useCache && cached) return cached.promise;
9
+ const entry = {
10
+ promise: readApiResultOrThrow(
11
+ url,
12
+ void 0,
13
+ { errorMessage }
14
+ )
15
+ };
16
+ if (useCache) dealDataCache.set(url, entry);
17
+ return entry.promise.finally(() => {
18
+ if (dealDataCache.get(url) === entry) dealDataCache.delete(url);
19
+ });
20
+ }
4
21
  function useDealData(id) {
5
22
  const t = useT();
6
23
  const [data, setData] = React.useState(null);
@@ -8,7 +25,7 @@ function useDealData(id) {
8
25
  const [error, setError] = React.useState(null);
9
26
  const [isNotFound, setIsNotFound] = React.useState(false);
10
27
  const initialLoadDoneRef = React.useRef(false);
11
- const loadData = React.useCallback(async () => {
28
+ const loadData = React.useCallback(async (options = {}) => {
12
29
  if (!id) {
13
30
  setIsNotFound(true);
14
31
  setIsLoading(false);
@@ -19,10 +36,10 @@ function useDealData(id) {
19
36
  }
20
37
  setError(null);
21
38
  try {
22
- const payload = await readApiResultOrThrow(
23
- `/api/customers/deals/${encodeURIComponent(id)}?include=stages&view=lite`,
24
- void 0,
25
- { errorMessage: t("customers.deals.detail.error.load", "Failed to load deal.") }
39
+ const payload = await fetchDealData(
40
+ id,
41
+ t("customers.deals.detail.error.load", "Failed to load deal."),
42
+ options.cache === true
26
43
  );
27
44
  setData(payload);
28
45
  } catch (loadError) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../../src/modules/customers/backend/customers/deals/%5Bid%5D/hooks/useDealData.ts"],
4
- "sourcesContent": ["import * as React from 'react'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { DealDetailPayload } from './types'\n\ntype UseDealDataResult = {\n data: DealDetailPayload | null\n setData: React.Dispatch<React.SetStateAction<DealDetailPayload | null>>\n isLoading: boolean\n error: string | null\n isNotFound: boolean\n loadData: () => Promise<void>\n}\n\nexport function useDealData(id: string): UseDealDataResult {\n const t = useT()\n const [data, setData] = React.useState<DealDetailPayload | null>(null)\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [isNotFound, setIsNotFound] = React.useState(false)\n const initialLoadDoneRef = React.useRef(false)\n\n const loadData = React.useCallback(async () => {\n if (!id) {\n setIsNotFound(true)\n setIsLoading(false)\n return\n }\n if (!initialLoadDoneRef.current) {\n setIsLoading(true)\n }\n setError(null)\n try {\n const payload = await readApiResultOrThrow<DealDetailPayload>(\n `/api/customers/deals/${encodeURIComponent(id)}?include=stages&view=lite`,\n undefined,\n { errorMessage: t('customers.deals.detail.error.load', 'Failed to load deal.') },\n )\n setData(payload)\n } catch (loadError) {\n if ((loadError as { status?: number }).status === 404) {\n setIsNotFound(true)\n } else {\n const message =\n loadError instanceof Error\n ? loadError.message\n : t('customers.deals.detail.error.load', 'Failed to load deal.')\n setError(message)\n }\n if (!initialLoadDoneRef.current) setData(null)\n } finally {\n setIsLoading(false)\n initialLoadDoneRef.current = true\n }\n }, [id, t])\n\n return { data, setData, isLoading, error, isNotFound, loadData }\n}\n"],
5
- "mappings": "AAAA,YAAY,WAAW;AACvB,SAAS,4BAA4B;AACrC,SAAS,YAAY;AAYd,SAAS,YAAY,IAA+B;AACzD,QAAM,IAAI,KAAK;AACf,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAmC,IAAI;AACrE,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAS,IAAI;AACrD,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAwB,IAAI;AAC5D,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAS,KAAK;AACxD,QAAM,qBAAqB,MAAM,OAAO,KAAK;AAE7C,QAAM,WAAW,MAAM,YAAY,YAAY;AAC7C,QAAI,CAAC,IAAI;AACP,oBAAc,IAAI;AAClB,mBAAa,KAAK;AAClB;AAAA,IACF;AACA,QAAI,CAAC,mBAAmB,SAAS;AAC/B,mBAAa,IAAI;AAAA,IACnB;AACA,aAAS,IAAI;AACb,QAAI;AACF,YAAM,UAAU,MAAM;AAAA,QACpB,wBAAwB,mBAAmB,EAAE,CAAC;AAAA,QAC9C;AAAA,QACA,EAAE,cAAc,EAAE,qCAAqC,sBAAsB,EAAE;AAAA,MACjF;AACA,cAAQ,OAAO;AAAA,IACjB,SAAS,WAAW;AAClB,UAAK,UAAkC,WAAW,KAAK;AACrD,sBAAc,IAAI;AAAA,MACpB,OAAO;AACL,cAAM,UACJ,qBAAqB,QACjB,UAAU,UACV,EAAE,qCAAqC,sBAAsB;AACnE,iBAAS,OAAO;AAAA,MAClB;AACA,UAAI,CAAC,mBAAmB,QAAS,SAAQ,IAAI;AAAA,IAC/C,UAAE;AACA,mBAAa,KAAK;AAClB,yBAAmB,UAAU;AAAA,IAC/B;AAAA,EACF,GAAG,CAAC,IAAI,CAAC,CAAC;AAEV,SAAO,EAAE,MAAM,SAAS,WAAW,OAAO,YAAY,SAAS;AACjE;",
4
+ "sourcesContent": ["import * as React from 'react'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { DealDetailPayload } from './types'\n\ntype LoadDataOptions = {\n cache?: boolean\n}\n\ntype UseDealDataResult = {\n data: DealDetailPayload | null\n setData: React.Dispatch<React.SetStateAction<DealDetailPayload | null>>\n isLoading: boolean\n error: string | null\n isNotFound: boolean\n loadData: (options?: LoadDataOptions) => Promise<void>\n}\n\ntype DealDataCacheEntry = {\n promise: Promise<DealDetailPayload>\n}\n\nconst dealDataCache = new Map<string, DealDataCacheEntry>()\n\nfunction fetchDealData(id: string, errorMessage: string, useCache: boolean): Promise<DealDetailPayload> {\n const url = `/api/customers/deals/${encodeURIComponent(id)}?include=stages&view=lite`\n const cached = dealDataCache.get(url)\n if (useCache && cached) return cached.promise\n const entry: DealDataCacheEntry = {\n promise: readApiResultOrThrow<DealDetailPayload>(\n url,\n undefined,\n { errorMessage },\n ),\n }\n if (useCache) dealDataCache.set(url, entry)\n return entry.promise.finally(() => {\n if (dealDataCache.get(url) === entry) dealDataCache.delete(url)\n })\n}\n\nexport function useDealData(id: string): UseDealDataResult {\n const t = useT()\n const [data, setData] = React.useState<DealDetailPayload | null>(null)\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [isNotFound, setIsNotFound] = React.useState(false)\n const initialLoadDoneRef = React.useRef(false)\n\n const loadData = React.useCallback(async (options: LoadDataOptions = {}) => {\n if (!id) {\n setIsNotFound(true)\n setIsLoading(false)\n return\n }\n if (!initialLoadDoneRef.current) {\n setIsLoading(true)\n }\n setError(null)\n try {\n const payload = await fetchDealData(\n id,\n t('customers.deals.detail.error.load', 'Failed to load deal.'),\n options.cache === true,\n )\n setData(payload)\n } catch (loadError) {\n if ((loadError as { status?: number }).status === 404) {\n setIsNotFound(true)\n } else {\n const message =\n loadError instanceof Error\n ? loadError.message\n : t('customers.deals.detail.error.load', 'Failed to load deal.')\n setError(message)\n }\n if (!initialLoadDoneRef.current) setData(null)\n } finally {\n setIsLoading(false)\n initialLoadDoneRef.current = true\n }\n }, [id, t])\n\n return { data, setData, isLoading, error, isNotFound, loadData }\n}\n"],
5
+ "mappings": "AAAA,YAAY,WAAW;AACvB,SAAS,4BAA4B;AACrC,SAAS,YAAY;AAoBrB,MAAM,gBAAgB,oBAAI,IAAgC;AAE1D,SAAS,cAAc,IAAY,cAAsB,UAA+C;AACtG,QAAM,MAAM,wBAAwB,mBAAmB,EAAE,CAAC;AAC1D,QAAM,SAAS,cAAc,IAAI,GAAG;AACpC,MAAI,YAAY,OAAQ,QAAO,OAAO;AACtC,QAAM,QAA4B;AAAA,IAChC,SAAS;AAAA,MACP;AAAA,MACA;AAAA,MACA,EAAE,aAAa;AAAA,IACjB;AAAA,EACF;AACA,MAAI,SAAU,eAAc,IAAI,KAAK,KAAK;AAC1C,SAAO,MAAM,QAAQ,QAAQ,MAAM;AACjC,QAAI,cAAc,IAAI,GAAG,MAAM,MAAO,eAAc,OAAO,GAAG;AAAA,EAChE,CAAC;AACH;AAEO,SAAS,YAAY,IAA+B;AACzD,QAAM,IAAI,KAAK;AACf,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAmC,IAAI;AACrE,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAS,IAAI;AACrD,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAwB,IAAI;AAC5D,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAS,KAAK;AACxD,QAAM,qBAAqB,MAAM,OAAO,KAAK;AAE7C,QAAM,WAAW,MAAM,YAAY,OAAO,UAA2B,CAAC,MAAM;AAC1E,QAAI,CAAC,IAAI;AACP,oBAAc,IAAI;AAClB,mBAAa,KAAK;AAClB;AAAA,IACF;AACA,QAAI,CAAC,mBAAmB,SAAS;AAC/B,mBAAa,IAAI;AAAA,IACnB;AACA,aAAS,IAAI;AACb,QAAI;AACF,YAAM,UAAU,MAAM;AAAA,QACpB;AAAA,QACA,EAAE,qCAAqC,sBAAsB;AAAA,QAC7D,QAAQ,UAAU;AAAA,MACpB;AACA,cAAQ,OAAO;AAAA,IACjB,SAAS,WAAW;AAClB,UAAK,UAAkC,WAAW,KAAK;AACrD,sBAAc,IAAI;AAAA,MACpB,OAAO;AACL,cAAM,UACJ,qBAAqB,QACjB,UAAU,UACV,EAAE,qCAAqC,sBAAsB;AACnE,iBAAS,OAAO;AAAA,MAClB;AACA,UAAI,CAAC,mBAAmB,QAAS,SAAQ,IAAI;AAAA,IAC/C,UAAE;AACA,mBAAa,KAAK;AAClB,yBAAmB,UAAU;AAAA,IAC/B;AAAA,EACF,GAAG,CAAC,IAAI,CAAC,CAAC;AAEV,SAAO,EAAE,MAAM,SAAS,WAAW,OAAO,YAAY,SAAS;AACjE;",
6
6
  "names": []
7
7
  }