@open-mercato/core 0.6.5-develop.5382.1.f542de69af → 0.6.5
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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/bootstrap.js +46 -6
- package/dist/bootstrap.js.map +2 -2
- package/dist/generated/entities/organization/index.js +2 -0
- package/dist/generated/entities/organization/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +1 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/crmFixtures.js +4 -0
- package/dist/helpers/integration/crmFixtures.js.map +2 -2
- package/dist/modules/attachments/api/route.js +2 -0
- package/dist/modules/attachments/api/route.js.map +2 -2
- package/dist/modules/attachments/lib/access.js +18 -0
- package/dist/modules/attachments/lib/access.js.map +2 -2
- package/dist/modules/audit_logs/data/entities.js +2 -1
- package/dist/modules/audit_logs/data/entities.js.map +2 -2
- package/dist/modules/audit_logs/migrations/Migration20260611104500.js +13 -0
- package/dist/modules/audit_logs/migrations/Migration20260611104500.js.map +7 -0
- package/dist/modules/audit_logs/services/accessLogService.js +10 -0
- package/dist/modules/audit_logs/services/accessLogService.js.map +2 -2
- package/dist/modules/auth/api/admin/nav.js +9 -0
- package/dist/modules/auth/api/admin/nav.js.map +2 -2
- package/dist/modules/auth/api/login.js +4 -13
- package/dist/modules/auth/api/login.js.map +2 -2
- package/dist/modules/auth/data/entities.js +3 -1
- package/dist/modules/auth/data/entities.js.map +2 -2
- package/dist/modules/auth/lib/backendChrome.js +35 -2
- package/dist/modules/auth/lib/backendChrome.js.map +2 -2
- package/dist/modules/auth/lib/consentIntegrity.js +3 -3
- package/dist/modules/auth/lib/consentIntegrity.js.map +2 -2
- package/dist/modules/auth/migrations/Migration20260611103000.js +15 -0
- package/dist/modules/auth/migrations/Migration20260611103000.js.map +7 -0
- package/dist/modules/auth/services/authService.js +5 -3
- package/dist/modules/auth/services/authService.js.map +2 -2
- package/dist/modules/auth/services/rbacService.js +3 -2
- package/dist/modules/auth/services/rbacService.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js +0 -3
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js.map +2 -2
- package/dist/modules/customers/api/deals/route.js +43 -2
- package/dist/modules/customers/api/deals/route.js.map +2 -2
- package/dist/modules/customers/api/deals/summary/route.js +402 -0
- package/dist/modules/customers/api/deals/summary/route.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.js +16 -5
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealData.js +22 -5
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealData.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +12 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +221 -56
- package/dist/modules/customers/backend/customers/deals/page.js.map +3 -3
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +1 -1
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +18 -0
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/cli.js +15 -9
- package/dist/modules/customers/cli.js.map +2 -2
- package/dist/modules/customers/components/DealsKpiStrip.js +282 -0
- package/dist/modules/customers/components/DealsKpiStrip.js.map +7 -0
- package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js +0 -1
- package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/DealForm.js +100 -17
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/PersonDetailTabs.js +11 -3
- package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +1 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
- package/dist/modules/customers/components/kpi/PipelineStageBar.js +63 -0
- package/dist/modules/customers/components/kpi/PipelineStageBar.js.map +7 -0
- package/dist/modules/customers/lib/dealsMetrics.js +82 -0
- package/dist/modules/customers/lib/dealsMetrics.js.map +7 -0
- package/dist/modules/directory/api/organization-branding/route.js +214 -0
- package/dist/modules/directory/api/organization-branding/route.js.map +7 -0
- package/dist/modules/directory/api/organizations/route.js +7 -0
- package/dist/modules/directory/api/organizations/route.js.map +3 -3
- package/dist/modules/directory/backend/directory/branding/page.js +214 -0
- package/dist/modules/directory/backend/directory/branding/page.js.map +7 -0
- package/dist/modules/directory/backend/directory/branding/page.meta.js +26 -0
- package/dist/modules/directory/backend/directory/branding/page.meta.js.map +7 -0
- package/dist/modules/directory/commands/organizations.js +8 -1
- package/dist/modules/directory/commands/organizations.js.map +2 -2
- package/dist/modules/directory/data/entities.js +3 -0
- package/dist/modules/directory/data/entities.js.map +2 -2
- package/dist/modules/directory/data/validators.js +9 -0
- package/dist/modules/directory/data/validators.js.map +2 -2
- package/dist/modules/directory/migrations/Migration20260607222259_directory.js +13 -0
- package/dist/modules/directory/migrations/Migration20260607222259_directory.js.map +7 -0
- package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js +2 -1
- package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js.map +2 -2
- package/dist/modules/directory/utils/organizationScope.js +59 -27
- package/dist/modules/directory/utils/organizationScope.js.map +2 -2
- package/dist/modules/entities/api/definitions.batch.js +2 -1
- package/dist/modules/entities/api/definitions.batch.js.map +2 -2
- package/dist/modules/entities/api/entities.js +7 -0
- package/dist/modules/entities/api/entities.js.map +2 -2
- package/dist/modules/entities/api/records.js +26 -15
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.js +14 -0
- package/dist/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/records/create/page.js +14 -0
- package/dist/modules/entities/backend/entities/user/[entityId]/records/create/page.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +12 -0
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
- package/dist/modules/entities/components/useRecordsEntityGuard.js +30 -0
- package/dist/modules/entities/components/useRecordsEntityGuard.js.map +7 -0
- package/dist/modules/query_index/data/entities.js +2 -1
- package/dist/modules/query_index/data/entities.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +4 -2
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/query_index/migrations/Migration20260611103000_query_index.js +16 -0
- package/dist/modules/query_index/migrations/Migration20260611103000_query_index.js.map +7 -0
- package/dist/modules/sales/commands/documents.js +7 -5
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesDocumentsTable.js +2 -1
- package/dist/modules/sales/components/documents/SalesDocumentsTable.js.map +2 -2
- package/dist/modules/sales/components/documents/salesDocumentsColumns.js +10 -0
- package/dist/modules/sales/components/documents/salesDocumentsColumns.js.map +7 -0
- package/dist/modules/staff/api/team-members.js +9 -2
- package/dist/modules/staff/api/team-members.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js +24 -1
- package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js.map +2 -2
- package/dist/modules/staff/backend/staff/team-members/[id]/page.js +11 -6
- package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
- package/dist/modules/staff/commands/team-members.js +1 -1
- package/dist/modules/staff/commands/team-members.js.map +2 -2
- package/dist/modules/staff/components/TeamMemberForm.js +1 -1
- package/dist/modules/staff/components/TeamMemberForm.js.map +2 -2
- package/dist/modules/staff/lib/scheduleSwitch.js +23 -0
- package/dist/modules/staff/lib/scheduleSwitch.js.map +7 -0
- package/dist/modules/workflows/backend/definitions/create/page.js +1 -2
- package/dist/modules/workflows/backend/definitions/create/page.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js +1 -2
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
- package/dist/modules/workflows/components/DefinitionTriggersEditor.js +1 -2
- package/dist/modules/workflows/components/DefinitionTriggersEditor.js.map +2 -2
- package/dist/modules/workflows/components/NodeEditDialog.js +4 -13
- package/dist/modules/workflows/components/NodeEditDialog.js.map +2 -2
- package/dist/modules/workflows/components/NodeEditDialogCrudForm.js +4 -13
- package/dist/modules/workflows/components/NodeEditDialogCrudForm.js.map +2 -2
- package/dist/modules/workflows/components/WorkflowGraphImpl.js +1 -4
- package/dist/modules/workflows/components/WorkflowGraphImpl.js.map +2 -2
- package/dist/modules/workflows/components/fields/FormFieldArrayEditor.js +2 -5
- package/dist/modules/workflows/components/fields/FormFieldArrayEditor.js.map +2 -2
- package/generated/entities/organization/index.ts +1 -0
- package/generated/entity-fields-registry.ts +1 -0
- package/package.json +11 -12
- package/src/bootstrap.ts +65 -7
- package/src/helpers/integration/crmFixtures.ts +21 -1
- package/src/modules/attachments/AGENTS.md +79 -0
- package/src/modules/attachments/api/route.ts +2 -0
- package/src/modules/attachments/lib/access.ts +36 -0
- package/src/modules/audit_logs/data/entities.ts +1 -0
- package/src/modules/audit_logs/migrations/.snapshot-open-mercato.json +10 -0
- package/src/modules/audit_logs/migrations/Migration20260611104500.ts +13 -0
- package/src/modules/audit_logs/services/accessLogService.ts +15 -0
- package/src/modules/auth/api/admin/nav.ts +9 -0
- package/src/modules/auth/api/login.ts +13 -13
- package/src/modules/auth/data/entities.ts +2 -0
- package/src/modules/auth/i18n/de.json +0 -1
- package/src/modules/auth/i18n/en.json +0 -1
- package/src/modules/auth/i18n/es.json +0 -1
- package/src/modules/auth/i18n/pl.json +0 -1
- package/src/modules/auth/lib/backendChrome.tsx +37 -1
- package/src/modules/auth/lib/consentIntegrity.ts +6 -3
- package/src/modules/auth/migrations/.snapshot-open-mercato.json +20 -0
- package/src/modules/auth/migrations/Migration20260611103000.ts +21 -0
- package/src/modules/auth/services/authService.ts +24 -4
- package/src/modules/auth/services/rbacService.ts +11 -2
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.tsx +0 -3
- package/src/modules/customers/api/deals/route.ts +51 -2
- package/src/modules/customers/api/deals/summary/route.ts +496 -0
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.ts +28 -6
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealData.ts +33 -6
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +17 -2
- package/src/modules/customers/backend/customers/deals/page.tsx +254 -66
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +1 -2
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +18 -0
- package/src/modules/customers/cli.ts +15 -15
- package/src/modules/customers/components/DealsKpiStrip.tsx +389 -0
- package/src/modules/customers/components/detail/ConfirmDealLostDialog.tsx +0 -1
- package/src/modules/customers/components/detail/DealForm.tsx +121 -19
- package/src/modules/customers/components/detail/PersonDetailTabs.tsx +12 -2
- package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +1 -2
- package/src/modules/customers/components/kpi/PipelineStageBar.tsx +77 -0
- package/src/modules/customers/i18n/de.json +43 -0
- package/src/modules/customers/i18n/en.json +43 -0
- package/src/modules/customers/i18n/es.json +43 -0
- package/src/modules/customers/i18n/pl.json +43 -0
- package/src/modules/customers/lib/dealsMetrics.ts +159 -0
- package/src/modules/directory/api/organization-branding/route.ts +238 -0
- package/src/modules/directory/api/organizations/route.ts +7 -0
- package/src/modules/directory/backend/directory/branding/page.meta.ts +24 -0
- package/src/modules/directory/backend/directory/branding/page.tsx +248 -0
- package/src/modules/directory/commands/organizations.ts +9 -1
- package/src/modules/directory/data/entities.ts +3 -0
- package/src/modules/directory/data/validators.ts +12 -0
- package/src/modules/directory/i18n/de.json +21 -0
- package/src/modules/directory/i18n/en.json +21 -0
- package/src/modules/directory/i18n/es.json +21 -0
- package/src/modules/directory/i18n/pl.json +21 -0
- package/src/modules/directory/migrations/.snapshot-open-mercato.json +40 -0
- package/src/modules/directory/migrations/Migration20260607222259_directory.ts +13 -0
- package/src/modules/directory/subscribers/invalidateOrgScopeCache.ts +3 -1
- package/src/modules/directory/utils/organizationScope.ts +85 -30
- package/src/modules/entities/api/definitions.batch.ts +11 -7
- package/src/modules/entities/api/entities.ts +11 -0
- package/src/modules/entities/api/records.ts +46 -25
- package/src/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.tsx +15 -0
- package/src/modules/entities/backend/entities/user/[entityId]/records/create/page.tsx +15 -0
- package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +23 -0
- package/src/modules/entities/components/useRecordsEntityGuard.ts +41 -0
- package/src/modules/entities/i18n/de.json +1 -0
- package/src/modules/entities/i18n/en.json +1 -0
- package/src/modules/entities/i18n/es.json +1 -0
- package/src/modules/entities/i18n/pl.json +1 -0
- package/src/modules/query_index/data/entities.ts +1 -0
- package/src/modules/query_index/lib/engine.ts +11 -5
- package/src/modules/query_index/migrations/.snapshot-open-mercato.json +11 -0
- package/src/modules/query_index/migrations/Migration20260611103000_query_index.ts +29 -0
- package/src/modules/sales/commands/documents.ts +7 -5
- package/src/modules/sales/components/documents/SalesDocumentsTable.tsx +2 -1
- package/src/modules/sales/components/documents/salesDocumentsColumns.ts +6 -0
- package/src/modules/staff/api/team-members.ts +9 -2
- package/src/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.ts +31 -1
- package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +18 -8
- package/src/modules/staff/commands/team-members.ts +5 -2
- package/src/modules/staff/components/TeamMemberForm.tsx +4 -1
- package/src/modules/staff/i18n/de.json +1 -0
- package/src/modules/staff/i18n/en.json +1 -0
- package/src/modules/staff/i18n/es.json +1 -0
- package/src/modules/staff/i18n/pl.json +1 -0
- package/src/modules/staff/lib/scheduleSwitch.ts +46 -0
- package/src/modules/workflows/backend/definitions/create/page.tsx +1 -2
- package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +1 -2
- package/src/modules/workflows/components/DefinitionTriggersEditor.tsx +1 -2
- package/src/modules/workflows/components/NodeEditDialog.tsx +1 -4
- package/src/modules/workflows/components/NodeEditDialogCrudForm.tsx +4 -7
- package/src/modules/workflows/components/WorkflowGraphImpl.tsx +1 -2
- 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": "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",
|
|
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 <
|
|
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": ";AAqGgB,cAMA,YANA;AAnGhB,YAAY,WAAW;AACvB,SAAS,eAAe,OAAO,mBAAmB;AAClD,SAAS,YAAY;AACrB,SAAS,kCAAkC;AAC3C,SAAS,OAAO,kBAAkB,kBAAkB;AACpD,SAAS,cAAc;AACvB,SAAS,QAAQ,eAAe,cAAc,cAAc,mBAAmB;AAC/E,SAAS,gBAAgB;AACzB,SAAS,2BAA2B;AAkB7B,SAAS,sBAAsB;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA+B;AAC7B,QAAM,IAAI,KAAK;AACf,QAAM,CAAC,cAAc,eAAe,IAAI,MAAM,SAAS,EAAE;AACzD,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAS,EAAE;AACnD,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAA6B,CAAC,CAAC;AAC3E,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,MAAM,SAAS,KAAK;AAChE,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAS,EAAE;AAC3C,QAAM,CAAC,cAAc,eAAe,IAAI,MAAM,SAAS,KAAK;AAE5D,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,KAAM;AACX,QAAI,YAAY;AAChB,+BAA2B,wBAAwB,EAChD,KAAK,CAAC,UAAU;AACf,UAAI,CAAC,UAAW,gBAAe,KAAK;AAAA,IACtC,CAAC,EACA,MAAM,CAAC,cAAc;AACpB,cAAQ,MAAM,6CAA6C,SAAS;AACpE,UAAI,CAAC,UAAW,gBAAe,CAAC,CAAC;AAAA,IACnC,CAAC;AACH,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,IAAI,CAAC;AAET,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,KAAM;AACX,oBAAgB,EAAE;AAClB,iBAAa,EAAE;AACf,sBAAkB,KAAK;AACvB,aAAS,EAAE;AAAA,EACb,GAAG,CAAC,IAAI,CAAC;AAET,QAAM,qBAAqB,MAAM;AAAA,IAC/B,MAAM,YAAY,KAAK,CAAC,WAAW,OAAO,OAAO,YAAY,KAAK;AAAA,IAClE,CAAC,cAAc,WAAW;AAAA,EAC5B;AAEA,QAAM,gBAAgB,MAAM,YAAY,YAAY;AAClD,QAAI,CAAC,cAAc;AACjB,eAAS,EAAE,8CAA8C,6BAA6B,CAAC;AACvF;AAAA,IACF;AACA,oBAAgB,IAAI;AACpB,QAAI;AACF,YAAM,UAAU;AAAA,QACd;AAAA,QACA,WAAW,UAAU,KAAK,KAAK;AAAA,MACjC,CAAC;AAAA,IACH,UAAE;AACA,sBAAgB,KAAK;AAAA,IACvB;AAAA,EACF,GAAG,CAAC,WAAW,cAAc,WAAW,CAAC,CAAC;AAE1C,QAAM,gBAAgB,oBAAoB;AAAA,IACxC,WAAW,MAAM,KAAK,cAAc;AAAA,IACpC,UAAU;AAAA,EACZ,CAAC;AAED,SACE,oBAAC,UAAO,MAAY,cAAc,CAAC,aAAa;AAAE,QAAI,CAAC,SAAU,SAAQ;AAAA,EAAE,GACzE,8BAAC,iBAAc,WAAU,wCAAuC,WAAW,eACzE,+BAAC,SAAI,WAAU,sCACb;AAAA,wBAAC,gBAAa,WAAU,uCACtB,+BAAC,SAAI,WAAU,0BACb;AAAA,0BAAC,SAAI,WAAU,mGACb,8BAAC,iBAAc,WAAU,UAAS,GACpC;AAAA,MACA,qBAAC,SAAI,WAAU,WACb;AAAA,4BAAC,eAAY,WAAU,iEACpB,YAAE,qCAAqC,oBAAoB,GAC9D;AAAA,QACA,qBAAC,OAAE,WAAU,sCACV;AAAA;AAAA,UACA,YAAY,SAAM,SAAS,KAAK;AAAA,UAChC,cAAc,SAAM,WAAW,KAAK;AAAA,WACvC;AAAA,SACF;AAAA,OACF,GACF;AAAA,IAEA,qBAAC,SAAI,WAAU,uBACb;AAAA,2BAAC,SAAM,SAAQ,WAAU,WAAU,cACjC;AAAA,4BAAC,cACE,YAAE,4CAA4C,6BAA6B,GAC9E;AAAA,QACA,oBAAC,oBACE,YAAE,uCAAuC,6FAA6F,GACzI;AAAA,SACF;AAAA,MAEA,qBAAC,SAAI,WAAU,aACb;AAAA,6BAAC,WAAM,WAAU,yCACd;AAAA,YAAE,2CAA2C,aAAa;AAAA,UAC3D,oBAAC,UAAK,WAAU,yBAAwB,eAAC;AAAA,WAC3C;AAAA,QACA,qBAAC,SAAI,WAAU,aACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,SAAS,MAAM,kBAAkB,CAAC,YAAY,CAAC,OAAO;AAAA,cACtD,WAAU;AAAA,cAEV;AAAA,qCAAC,SAAI,WAAU,WACb;AAAA,sCAAC,SAAI,WAAU,oDACZ,8BAAoB,SAAS,EAAE,iDAAiD,oBAAoB,GACvG;AAAA,kBACA,oBAAC,SAAI,WAAU,0CACZ,8BAAoB,eAAe,EAAE,0CAA0C,gDAAgD,GAClI;AAAA,mBACF;AAAA,gBACA,oBAAC,eAAY,WAAU,8CAA6C;AAAA;AAAA;AAAA,UACtE;AAAA,UAEC,iBACC,oBAAC,SAAI,WAAU,oEACZ,sBAAY,IAAI,CAAC,QAAQ,UAAU;AAClC,kBAAM,aAAa,OAAO,OAAO;AACjC,mBACE;AAAA,cAAC;AAAA;AAAA,gBAEC,MAAK;AAAA,gBACL,SAAQ;AAAA,gBACR,SAAS,MAAM;AACb,kCAAgB,OAAO,EAAE;AACzB,oCAAkB,KAAK;AACvB,2BAAS,EAAE;AAAA,gBACb;AAAA,gBACA,WAAW,oFACT,QAAQ,YAAY,SAAS,IAAI,8BAA8B,EACjE,IAAI,aAAa,gBAAgB,oBAAoB;AAAA,gBAErD;AAAA,uCAAC,SAAI,WAAU,WACb;AAAA,wCAAC,SAAI,WAAU,2CAA2C,iBAAO,OAAM;AAAA,oBACvE,oBAAC,SAAI,WAAU,iCACZ,iBAAO,eAAe,EAAE,yDAAyD,2BAA2B,GAC/G;AAAA,qBACF;AAAA,kBACC,aACC,oBAAC,UAAK,WAAU,oGACd,8BAAC,SAAM,WAAU,YAAW,GAC9B,IACE;AAAA;AAAA;AAAA,cAtBC,OAAO;AAAA,YAuBd;AAAA,UAEJ,CAAC,GACH,IACE;AAAA,WACN;AAAA,QACC,QAAQ,oBAAC,OAAE,WAAU,4BAA4B,iBAAM,IAAO;AAAA,SACjE;AAAA,MAEA,qBAAC,SAAI,WAAU,aACb;AAAA,4BAAC,WAAM,WAAU,yCACd,YAAE,0CAA0C,uBAAuB,GACtE;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO;AAAA,YACP,UAAU,CAAC,UAAU,aAAa,MAAM,OAAO,KAAK;AAAA,YACpD,aAAa,EAAE,gDAAgD,sCAAsC;AAAA,YACrG,MAAM;AAAA,YACN,WAAU;AAAA;AAAA,QACZ;AAAA,SACF;AAAA,OACF;AAAA,IAEA,qBAAC,gBAAa,WAAU,sDACtB;AAAA,0BAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,SAAS,SAC9C,YAAE,sCAAsC,QAAQ,GACnD;AAAA,MACA,oBAAC,UAAO,MAAK,UAAS,SAAQ,eAAc,SAAS,MAAM;AAAE,aAAK,cAAc;AAAA,MAAE,GAC/E,YAAE,uCAAuC,cAAc,GAC1D;AAAA,OACF;AAAA,KACF,GACF,GACF;AAEJ;",
|
|
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
|
|
555
|
-
const
|
|
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
|
|
563
|
-
if (
|
|
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
|
|
576
|
-
if (cancelled) return;
|
|
577
|
-
|
|
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
|