@open-mercato/core 0.6.8-develop.7072.1.19c2a8bbe0 → 0.6.8-develop.7077.1.94f1126c13
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/modules/auth/api/login.js +4 -1
- package/dist/modules/auth/api/login.js.map +2 -2
- package/dist/modules/auth/api/session/refresh.js +25 -1
- package/dist/modules/auth/api/session/refresh.js.map +2 -2
- package/dist/modules/customers/ai-tools/deals-pack.js +48 -5
- package/dist/modules/customers/ai-tools/deals-pack.js.map +2 -2
- package/dist/modules/customers/api/deals/aggregate/route.js +11 -4
- package/dist/modules/customers/api/deals/aggregate/route.js.map +2 -2
- package/dist/modules/customers/api/deals/route.js +38 -1
- package/dist/modules/customers/api/deals/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/Lane.js +4 -13
- package/dist/modules/customers/backend/customers/deals/pipeline/components/Lane.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.js +85 -14
- package/dist/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/toneClasses.js +18 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/toneClasses.js.map +7 -0
- package/dist/modules/customers/commands/deals.js +21 -34
- package/dist/modules/customers/commands/deals.js.map +2 -2
- package/dist/modules/customers/lib/closureStage.js +45 -0
- package/dist/modules/customers/lib/closureStage.js.map +7 -0
- package/dist/modules/customers/lib/dealStatus.js +30 -0
- package/dist/modules/customers/lib/dealStatus.js.map +2 -2
- package/dist/modules/query_index/lib/search-entity-policy.js +14 -0
- package/dist/modules/query_index/lib/search-entity-policy.js.map +7 -0
- package/package.json +7 -7
- package/src/modules/auth/api/login.ts +9 -1
- package/src/modules/auth/api/session/refresh.ts +32 -1
- package/src/modules/customers/ai-tools/deals-pack.ts +62 -0
- package/src/modules/customers/api/deals/aggregate/route.ts +14 -4
- package/src/modules/customers/api/deals/route.ts +50 -1
- package/src/modules/customers/backend/customers/deals/pipeline/components/Lane.tsx +5 -14
- package/src/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.tsx +109 -27
- package/src/modules/customers/backend/customers/deals/pipeline/components/toneClasses.ts +21 -0
- package/src/modules/customers/commands/deals.ts +34 -59
- package/src/modules/customers/i18n/de.json +3 -0
- package/src/modules/customers/i18n/en.json +3 -0
- package/src/modules/customers/i18n/es.json +3 -0
- package/src/modules/customers/i18n/ko.json +3 -0
- package/src/modules/customers/i18n/pl.json +3 -0
- package/src/modules/customers/lib/closureStage.ts +76 -0
- package/src/modules/customers/lib/dealStatus.ts +42 -0
- package/src/modules/query_index/lib/search-entity-policy.ts +46 -0
package/dist/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.js
CHANGED
|
@@ -10,43 +10,107 @@ import {
|
|
|
10
10
|
} from "@open-mercato/ui/primitives/popover";
|
|
11
11
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
12
12
|
import { translateWithFallback } from "@open-mercato/shared/lib/i18n/translate";
|
|
13
|
+
import { useOrganizationScopeVersion } from "@open-mercato/shared/lib/frontend/useOrganizationScope";
|
|
14
|
+
import { mapDictionaryColorToTone } from "@open-mercato/shared/lib/query/advanced-filter";
|
|
15
|
+
import { Spinner } from "@open-mercato/ui/primitives/spinner";
|
|
16
|
+
import { useCustomerDictionary } from "../../../../../components/detail/hooks/useCustomerDictionary.js";
|
|
17
|
+
import { canonicalDealStatus } from "../../../../../lib/dealStatus.js";
|
|
18
|
+
import { toneToDotClass } from "./toneClasses.js";
|
|
13
19
|
import { ChipButton } from "./ChipButton.js";
|
|
14
20
|
import { FilterPopoverShell } from "./FilterPopoverShell.js";
|
|
15
|
-
const
|
|
21
|
+
const FALLBACK_STATUS_OPTIONS = [
|
|
16
22
|
{
|
|
17
23
|
value: "open",
|
|
18
24
|
labelKey: "customers.deals.kanban.filter.status.open",
|
|
19
25
|
labelFallback: "Open",
|
|
20
|
-
|
|
26
|
+
// Tones mirror mapDictionaryColorToTone over the seeded dictionary colors
|
|
27
|
+
// (#2563eb → info, #22c55e → success, #ef4444 → error) so the fallback pills
|
|
28
|
+
// render identically to the dictionary-driven ones.
|
|
29
|
+
dotClass: "bg-status-info-icon"
|
|
21
30
|
},
|
|
22
31
|
{
|
|
23
32
|
value: "win",
|
|
24
33
|
labelKey: "customers.deals.kanban.filter.status.won",
|
|
25
34
|
labelFallback: "Won",
|
|
26
|
-
dotClass: "bg-status-
|
|
35
|
+
dotClass: "bg-status-success-icon"
|
|
27
36
|
},
|
|
28
37
|
{
|
|
29
38
|
value: "loose",
|
|
30
39
|
labelKey: "customers.deals.kanban.filter.status.lost",
|
|
31
40
|
labelFallback: "Lost",
|
|
41
|
+
dotClass: "bg-status-error-icon"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
value: "closed",
|
|
45
|
+
labelKey: "customers.deals.kanban.filter.status.closed",
|
|
46
|
+
labelFallback: "Closed",
|
|
32
47
|
dotClass: "bg-status-neutral-icon"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
value: "in_progress",
|
|
51
|
+
labelKey: "customers.deals.kanban.filter.status.inProgress",
|
|
52
|
+
labelFallback: "In progress",
|
|
53
|
+
dotClass: "bg-status-warning-icon"
|
|
33
54
|
}
|
|
34
55
|
];
|
|
35
56
|
function StatusFilterPopover({ values, onApply }) {
|
|
36
57
|
const t = useT();
|
|
58
|
+
const scopeVersion = useOrganizationScopeVersion();
|
|
59
|
+
const { data: dictionaryData, isLoading: dictionaryLoading } = useCustomerDictionary(
|
|
60
|
+
"deal-statuses",
|
|
61
|
+
scopeVersion
|
|
62
|
+
);
|
|
37
63
|
const [open, setOpen] = React.useState(false);
|
|
38
|
-
const
|
|
64
|
+
const normalizedValues = React.useMemo(
|
|
65
|
+
() => Array.from(new Set(values.map(canonicalDealStatus))),
|
|
66
|
+
[values]
|
|
67
|
+
);
|
|
68
|
+
const [draft, setDraft] = React.useState(normalizedValues);
|
|
39
69
|
React.useEffect(() => {
|
|
40
|
-
if (open) setDraft(
|
|
41
|
-
}, [open,
|
|
70
|
+
if (open) setDraft(normalizedValues);
|
|
71
|
+
}, [open, normalizedValues]);
|
|
72
|
+
const statusOptions = React.useMemo(() => {
|
|
73
|
+
const entries = dictionaryData?.entries;
|
|
74
|
+
if (entries && entries.length > 0) {
|
|
75
|
+
const byCanonical = /* @__PURE__ */ new Map();
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
const canonical = canonicalDealStatus(entry.value);
|
|
78
|
+
if (byCanonical.has(canonical)) continue;
|
|
79
|
+
const tone = mapDictionaryColorToTone(entry.color ?? null);
|
|
80
|
+
byCanonical.set(canonical, {
|
|
81
|
+
value: canonical,
|
|
82
|
+
label: entry.label,
|
|
83
|
+
dotClass: toneToDotClass(tone)
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return Array.from(byCanonical.values()).sort(
|
|
87
|
+
(a, b) => a.label.localeCompare(b.label, void 0, { sensitivity: "base" })
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return FALLBACK_STATUS_OPTIONS.map((entry) => ({
|
|
91
|
+
value: entry.value,
|
|
92
|
+
label: translateWithFallback(t, entry.labelKey, entry.labelFallback),
|
|
93
|
+
dotClass: entry.dotClass
|
|
94
|
+
}));
|
|
95
|
+
}, [dictionaryData, t]);
|
|
96
|
+
const labelByValue = React.useMemo(() => {
|
|
97
|
+
const map = /* @__PURE__ */ new Map();
|
|
98
|
+
for (const option of statusOptions) {
|
|
99
|
+
map.set(option.value, option.label);
|
|
100
|
+
}
|
|
101
|
+
for (const fallback of FALLBACK_STATUS_OPTIONS) {
|
|
102
|
+
if (!map.has(fallback.value)) {
|
|
103
|
+
map.set(fallback.value, translateWithFallback(t, fallback.labelKey, fallback.labelFallback));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return map;
|
|
107
|
+
}, [statusOptions, t]);
|
|
42
108
|
const chipLabel = translateWithFallback(t, "customers.deals.kanban.filter.status", "Status");
|
|
43
|
-
const chipValue =
|
|
44
|
-
const option = STATUS_OPTIONS.find((entry) => entry.value === value);
|
|
45
|
-
return option ? translateWithFallback(t, option.labelKey, option.labelFallback) : value;
|
|
46
|
-
}).join(", ");
|
|
109
|
+
const chipValue = normalizedValues.length === 0 ? translateWithFallback(t, "customers.deals.kanban.filter.all", "All") : normalizedValues.map((value) => labelByValue.get(value) ?? value).join(", ");
|
|
47
110
|
const toggleDraft = (value) => {
|
|
111
|
+
const normalized = canonicalDealStatus(value);
|
|
48
112
|
setDraft(
|
|
49
|
-
(prev) => prev.includes(
|
|
113
|
+
(prev) => prev.includes(normalized) ? prev.filter((entry) => entry !== normalized) : [...prev, normalized]
|
|
50
114
|
);
|
|
51
115
|
};
|
|
52
116
|
const handleApply = () => {
|
|
@@ -60,7 +124,7 @@ function StatusFilterPopover({ values, onApply }) {
|
|
|
60
124
|
}
|
|
61
125
|
};
|
|
62
126
|
return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
|
|
63
|
-
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(ChipButton, { label: chipLabel, value: chipValue, active:
|
|
127
|
+
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(ChipButton, { label: chipLabel, value: chipValue, active: normalizedValues.length > 0 }) }),
|
|
64
128
|
/* @__PURE__ */ jsx(
|
|
65
129
|
PopoverContent,
|
|
66
130
|
{
|
|
@@ -84,9 +148,16 @@ function StatusFilterPopover({ values, onApply }) {
|
|
|
84
148
|
] }),
|
|
85
149
|
children: [
|
|
86
150
|
/* @__PURE__ */ jsx("span", { className: "text-xs font-semibold uppercase leading-normal tracking-wide text-muted-foreground", children: translateWithFallback(t, "customers.deals.kanban.filter.status", "Status") }),
|
|
87
|
-
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-x-1.5 gap-y-2", children:
|
|
151
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-x-1.5 gap-y-2", children: dictionaryLoading ? /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2 text-xs text-muted-foreground", role: "status", "aria-live": "polite", children: [
|
|
152
|
+
/* @__PURE__ */ jsx(Spinner, { className: "size-3" }),
|
|
153
|
+
translateWithFallback(
|
|
154
|
+
t,
|
|
155
|
+
"customers.deals.kanban.filter.status.loading",
|
|
156
|
+
"Loading statuses\u2026"
|
|
157
|
+
)
|
|
158
|
+
] }) : statusOptions.map((option) => {
|
|
88
159
|
const isSelected = draft.includes(option.value);
|
|
89
|
-
const label =
|
|
160
|
+
const label = option.label;
|
|
90
161
|
return /* @__PURE__ */ jsxs(
|
|
91
162
|
Button,
|
|
92
163
|
{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../../src/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { X } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from '@open-mercato/ui/primitives/popover'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'\nimport { ChipButton } from './ChipButton'\nimport { FilterPopoverShell } from './FilterPopoverShell'\n\n/**\n * Filter options exposed to the operator.\n *\n * The deal `status` column
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { X } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from '@open-mercato/ui/primitives/popover'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { mapDictionaryColorToTone } from '@open-mercato/shared/lib/query/advanced-filter'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { useCustomerDictionary } from '../../../../../components/detail/hooks/useCustomerDictionary'\nimport { canonicalDealStatus } from '../../../../../lib/dealStatus'\nimport { toneToDotClass } from './toneClasses'\nimport { ChipButton } from './ChipButton'\nimport { FilterPopoverShell } from './FilterPopoverShell'\n\n/**\n * Filter options exposed to the operator.\n *\n * The deal `status` column is dictionary-driven (`deal-statuses`); the seeded dictionary\n * carries five values (`open`, `closed`, `win`, `loose`, `in_progress`) plus any\n * tenant-custom entries. We render every dictionary entry de-duplicated by canonical\n * spelling so the kanban Status pill stays aligned with the list page's advanced filter\n * (which also uses the dictionary). A hard-coded fallback keeps the popover usable while\n * the dictionary is loading or when a tenant has no entries.\n *\n * `won` / `lost` are accepted as aliases for `win` / `loose` at the API layer (see\n * `lib/dealStatus.ts:expandDealStatusAliases`, shared by the deals list route and the\n * kanban aggregate route). The UI only exposes the canonical values to avoid duplicate\n * pills, but `canonicalDealStatus` normalizes any alias passed in through `values` so\n * the chip and draft selection render the correct label.\n */\nconst FALLBACK_STATUS_OPTIONS: Array<{\n value: string\n labelKey: string\n labelFallback: string\n dotClass: string\n}> = [\n {\n value: 'open',\n labelKey: 'customers.deals.kanban.filter.status.open',\n labelFallback: 'Open',\n // Tones mirror mapDictionaryColorToTone over the seeded dictionary colors\n // (#2563eb \u2192 info, #22c55e \u2192 success, #ef4444 \u2192 error) so the fallback pills\n // render identically to the dictionary-driven ones.\n dotClass: 'bg-status-info-icon',\n },\n {\n value: 'win',\n labelKey: 'customers.deals.kanban.filter.status.won',\n labelFallback: 'Won',\n dotClass: 'bg-status-success-icon',\n },\n {\n value: 'loose',\n labelKey: 'customers.deals.kanban.filter.status.lost',\n labelFallback: 'Lost',\n dotClass: 'bg-status-error-icon',\n },\n {\n value: 'closed',\n labelKey: 'customers.deals.kanban.filter.status.closed',\n labelFallback: 'Closed',\n dotClass: 'bg-status-neutral-icon',\n },\n {\n value: 'in_progress',\n labelKey: 'customers.deals.kanban.filter.status.inProgress',\n labelFallback: 'In progress',\n dotClass: 'bg-status-warning-icon',\n },\n]\n\ntype StatusFilterPopoverProps = {\n values: string[]\n onApply: (next: string[]) => void\n}\n\nexport function StatusFilterPopover({ values, onApply }: StatusFilterPopoverProps): React.ReactElement {\n const t = useT()\n const scopeVersion = useOrganizationScopeVersion()\n const { data: dictionaryData, isLoading: dictionaryLoading } = useCustomerDictionary(\n 'deal-statuses',\n scopeVersion,\n )\n const [open, setOpen] = React.useState(false)\n const normalizedValues = React.useMemo(\n () => Array.from(new Set(values.map(canonicalDealStatus))),\n [values],\n )\n const [draft, setDraft] = React.useState<string[]>(normalizedValues)\n\n React.useEffect(() => {\n if (open) setDraft(normalizedValues)\n }, [open, normalizedValues])\n\n const statusOptions = React.useMemo(() => {\n const entries = dictionaryData?.entries\n if (entries && entries.length > 0) {\n const byCanonical = new Map<string, { value: string; label: string; dotClass: string }>()\n for (const entry of entries) {\n const canonical = canonicalDealStatus(entry.value)\n if (byCanonical.has(canonical)) continue\n const tone = mapDictionaryColorToTone(entry.color ?? null)\n byCanonical.set(canonical, {\n value: canonical,\n label: entry.label,\n dotClass: toneToDotClass(tone),\n })\n }\n // Sort by label exactly like the List page's advanced filter\n // (backend/customers/deals/page.tsx dictionaryOptions) so both surfaces render\n // the same pills in the same order.\n return Array.from(byCanonical.values()).sort((a, b) =>\n a.label.localeCompare(b.label, undefined, { sensitivity: 'base' }),\n )\n }\n return FALLBACK_STATUS_OPTIONS.map((entry) => ({\n value: entry.value,\n label: translateWithFallback(t, entry.labelKey, entry.labelFallback),\n dotClass: entry.dotClass,\n }))\n }, [dictionaryData, t])\n\n const labelByValue = React.useMemo(() => {\n const map = new Map<string, string>()\n for (const option of statusOptions) {\n map.set(option.value, option.label)\n }\n for (const fallback of FALLBACK_STATUS_OPTIONS) {\n if (!map.has(fallback.value)) {\n map.set(fallback.value, translateWithFallback(t, fallback.labelKey, fallback.labelFallback))\n }\n }\n return map\n }, [statusOptions, t])\n\n const chipLabel = translateWithFallback(t, 'customers.deals.kanban.filter.status', 'Status')\n const chipValue =\n normalizedValues.length === 0\n ? translateWithFallback(t, 'customers.deals.kanban.filter.all', 'All')\n : normalizedValues\n .map((value) => labelByValue.get(value) ?? value)\n .join(', ')\n\n const toggleDraft = (value: string) => {\n const normalized = canonicalDealStatus(value)\n setDraft((prev) =>\n prev.includes(normalized) ? prev.filter((entry) => entry !== normalized) : [...prev, normalized],\n )\n }\n\n const handleApply = () => {\n onApply(draft)\n setOpen(false)\n }\n\n // Cmd/Ctrl+Enter from anywhere inside the popover confirms \u2014 parity with the dialog\n // primary-action shortcut (`AGENTS.md` UI Interaction rules).\n const handleKeyDown = (event: React.KeyboardEvent) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n handleApply()\n }\n }\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <ChipButton label={chipLabel} value={chipValue} active={normalizedValues.length > 0} />\n </PopoverTrigger>\n <PopoverContent\n className=\"w-96 rounded-2xl border-border bg-transparent p-0 shadow-xl\"\n align=\"start\"\n onKeyDown={handleKeyDown}\n >\n <FilterPopoverShell\n title={\n <>\n <span className=\"font-bold\">\n {translateWithFallback(t, 'customers.deals.kanban.filter.status.title.label', 'Filter : ')}\n </span>\n <span className=\"font-normal\">\n {translateWithFallback(t, 'customers.deals.kanban.filter.status', 'Status')}\n </span>\n </>\n }\n onClose={() => setOpen(false)}\n onCancel={() => setOpen(false)}\n onApply={handleApply}\n footerLeft={\n <span>\n {draft.length}{' '}\n {translateWithFallback(t, 'customers.deals.kanban.filter.selected', 'selected')}\n </span>\n }\n >\n <span className=\"text-xs font-semibold uppercase leading-normal tracking-wide text-muted-foreground\">\n {translateWithFallback(t, 'customers.deals.kanban.filter.status', 'Status')}\n </span>\n <div className=\"flex flex-wrap items-center gap-x-1.5 gap-y-2\">\n {dictionaryLoading ? (\n <span className=\"flex items-center gap-2 text-xs text-muted-foreground\" role=\"status\" aria-live=\"polite\">\n <Spinner className=\"size-3\" />\n {translateWithFallback(\n t,\n 'customers.deals.kanban.filter.status.loading',\n 'Loading statuses\u2026',\n )}\n </span>\n ) : (\n statusOptions.map((option) => {\n const isSelected = draft.includes(option.value)\n const label = option.label\n return (\n <Button\n key={option.value}\n type=\"button\"\n variant=\"ghost\"\n size=\"2xs\"\n onClick={() => toggleDraft(option.value)}\n aria-pressed={isSelected}\n className={`gap-1.5 rounded-full px-2.5 py-1.5 text-xs leading-normal ${\n isSelected\n ? 'bg-muted font-semibold text-foreground'\n : 'border border-border bg-card font-normal text-muted-foreground hover:bg-muted'\n }`}\n >\n <span\n className={`inline-block size-2 shrink-0 rounded-full ${option.dotClass}`}\n aria-hidden=\"true\"\n />\n <span>{label}</span>\n {isSelected ? (\n <X className=\"size-2.5 shrink-0 text-muted-foreground\" aria-hidden=\"true\" />\n ) : null}\n </Button>\n )\n })\n )}\n </div>\n </FilterPopoverShell>\n </PopoverContent>\n </Popover>\n )\n}\n\nexport default StatusFilterPopover\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const TONE_DOT_CLASS = {
|
|
2
|
+
success: "bg-status-success-icon",
|
|
3
|
+
error: "bg-status-error-icon",
|
|
4
|
+
warning: "bg-status-warning-icon",
|
|
5
|
+
info: "bg-status-info-icon",
|
|
6
|
+
neutral: "bg-status-neutral-icon",
|
|
7
|
+
brand: "bg-brand-violet",
|
|
8
|
+
pink: "bg-status-pink-icon"
|
|
9
|
+
};
|
|
10
|
+
function toneToDotClass(tone, fallback = "bg-status-neutral-icon") {
|
|
11
|
+
if (tone && tone in TONE_DOT_CLASS) return TONE_DOT_CLASS[tone];
|
|
12
|
+
return fallback;
|
|
13
|
+
}
|
|
14
|
+
export {
|
|
15
|
+
TONE_DOT_CLASS,
|
|
16
|
+
toneToDotClass
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=toneClasses.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../../../src/modules/customers/backend/customers/deals/pipeline/components/toneClasses.ts"],
|
|
4
|
+
"sourcesContent": ["import type { FilterOptionTone } from '@open-mercato/shared/lib/query/advanced-filter'\n\n// Saturated icon tokens shared by the kanban lane accent bar and the status filter\n// pills, so both surfaces render the same tone identically (#5107 review).\nexport const TONE_DOT_CLASS: Record<FilterOptionTone, string> = {\n success: 'bg-status-success-icon',\n error: 'bg-status-error-icon',\n warning: 'bg-status-warning-icon',\n info: 'bg-status-info-icon',\n neutral: 'bg-status-neutral-icon',\n brand: 'bg-brand-violet',\n pink: 'bg-status-pink-icon',\n}\n\nexport function toneToDotClass(\n tone: FilterOptionTone | null | undefined,\n fallback = 'bg-status-neutral-icon',\n): string {\n if (tone && tone in TONE_DOT_CLASS) return TONE_DOT_CLASS[tone]\n return fallback\n}\n"],
|
|
5
|
+
"mappings": "AAIO,MAAM,iBAAmD;AAAA,EAC9D,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS;AAAA,EACT,MAAM;AAAA,EACN,SAAS;AAAA,EACT,OAAO;AAAA,EACP,MAAM;AACR;AAEO,SAAS,eACd,MACA,WAAW,0BACH;AACR,MAAI,QAAQ,QAAQ,eAAgB,QAAO,eAAe,IAAI;AAC9D,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -38,6 +38,11 @@ import { CrudHttpError, notFound } from "@open-mercato/shared/lib/crud/errors";
|
|
|
38
38
|
import { E } from "../../../generated/entities.ids.generated.js";
|
|
39
39
|
import { findWithDecryption, findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
40
40
|
import { isMissingDealStageTransitionTable, warnMissingDealStageTransitionTable } from "../lib/dealStageTransitionTable.js";
|
|
41
|
+
import {
|
|
42
|
+
dealClosureOutcomeFromStatus,
|
|
43
|
+
loadClosurePipelineStageSnapshot
|
|
44
|
+
} from "../lib/closureStage.js";
|
|
45
|
+
import { canonicalDealStatus, isClosedDealStatus } from "../lib/dealStatus.js";
|
|
41
46
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
42
47
|
const logger = createLogger("customers");
|
|
43
48
|
const DEAL_ENTITY_ID = "customers:customer_deal";
|
|
@@ -54,10 +59,6 @@ const dealCrudEvents = {
|
|
|
54
59
|
tenantId: ctx.identifiers.tenantId
|
|
55
60
|
})
|
|
56
61
|
};
|
|
57
|
-
const TERMINAL_PIPELINE_STAGE_LABELS = {
|
|
58
|
-
won: /* @__PURE__ */ new Set(["won", "win", "closed won", "closed win"]),
|
|
59
|
-
lost: /* @__PURE__ */ new Set(["lost", "loose", "closed lost", "closed loose"])
|
|
60
|
-
};
|
|
61
62
|
function coerceSnapshotDate(value, fieldName) {
|
|
62
63
|
if (value === void 0 || value === null) return null;
|
|
63
64
|
if (value instanceof Date) return value;
|
|
@@ -84,39 +85,11 @@ async function loadPipelineStageSnapshot(em, pipelineStageId, tenantId, organiza
|
|
|
84
85
|
order: stage.order
|
|
85
86
|
};
|
|
86
87
|
}
|
|
87
|
-
function normalizePipelineStageLabel(value) {
|
|
88
|
-
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
89
|
-
}
|
|
90
88
|
function resolveRequestedClosureOutcome(input) {
|
|
91
89
|
if (input.closureOutcome === "won" || input.closureOutcome === "lost") {
|
|
92
90
|
return input.closureOutcome;
|
|
93
91
|
}
|
|
94
|
-
|
|
95
|
-
if (input.status === "loose") return "lost";
|
|
96
|
-
return null;
|
|
97
|
-
}
|
|
98
|
-
async function loadClosurePipelineStageSnapshot(em, input) {
|
|
99
|
-
if (!input.pipelineId) return null;
|
|
100
|
-
const stages = await findWithDecryption(
|
|
101
|
-
em,
|
|
102
|
-
CustomerPipelineStage,
|
|
103
|
-
{
|
|
104
|
-
pipelineId: input.pipelineId,
|
|
105
|
-
tenantId: input.tenantId,
|
|
106
|
-
organizationId: input.organizationId
|
|
107
|
-
},
|
|
108
|
-
{ orderBy: { order: "ASC" } },
|
|
109
|
-
{ tenantId: input.tenantId, organizationId: input.organizationId }
|
|
110
|
-
);
|
|
111
|
-
const aliases = TERMINAL_PIPELINE_STAGE_LABELS[input.closureOutcome];
|
|
112
|
-
const stage = stages.find((candidate) => aliases.has(normalizePipelineStageLabel(candidate.label)));
|
|
113
|
-
if (!stage) return null;
|
|
114
|
-
return {
|
|
115
|
-
id: stage.id,
|
|
116
|
-
pipelineId: stage.pipelineId,
|
|
117
|
-
label: stage.label,
|
|
118
|
-
order: stage.order
|
|
119
|
-
};
|
|
92
|
+
return dealClosureOutcomeFromStatus(input.status);
|
|
120
93
|
}
|
|
121
94
|
async function resolvePipelineStageValue(em, pipelineStageId, tenantId, organizationId) {
|
|
122
95
|
const stage = await loadPipelineStageSnapshot(em, pipelineStageId, tenantId, organizationId);
|
|
@@ -582,6 +555,7 @@ const updateDealCommand = {
|
|
|
582
555
|
let nextPipelineStageLabel = null;
|
|
583
556
|
let resolvedCurrentPipelineStageLabel = null;
|
|
584
557
|
let pipelineStageAssignmentChanged = false;
|
|
558
|
+
let requestedClosureOutcome = null;
|
|
585
559
|
await runCrudCommandWrite({
|
|
586
560
|
ctx,
|
|
587
561
|
em,
|
|
@@ -601,7 +575,7 @@ const updateDealCommand = {
|
|
|
601
575
|
}),
|
|
602
576
|
phases: [
|
|
603
577
|
async () => {
|
|
604
|
-
|
|
578
|
+
requestedClosureOutcome = resolveRequestedClosureOutcome(parsed);
|
|
605
579
|
const requestedPipelineId = parsed.pipelineId !== void 0 ? parsed.pipelineId ?? null : record.pipelineId ?? null;
|
|
606
580
|
const closureStageSnapshot = parsed.pipelineStageId === void 0 && requestedClosureOutcome ? await loadClosurePipelineStageSnapshot(em, {
|
|
607
581
|
pipelineId: requestedPipelineId,
|
|
@@ -649,6 +623,19 @@ const updateDealCommand = {
|
|
|
649
623
|
if (parsed.ownerUserId !== void 0) record.ownerUserId = parsed.ownerUserId ?? null;
|
|
650
624
|
if (parsed.source !== void 0) record.source = parsed.source ?? null;
|
|
651
625
|
if (parsed.closureOutcome !== void 0) record.closureOutcome = parsed.closureOutcome ?? null;
|
|
626
|
+
else if (requestedClosureOutcome && parsed.pipelineStageId === void 0) {
|
|
627
|
+
record.closureOutcome = requestedClosureOutcome;
|
|
628
|
+
} else if (parsed.status !== void 0 && !requestedClosureOutcome && // `closed` is a seeded dictionary status: saving it is not a reopen, so only
|
|
629
|
+
// a genuinely non-closed status clears stored closure state — and only when
|
|
630
|
+
// the deal actually carries any (a no-op status echo must not wipe loss data).
|
|
631
|
+
// Canonicalize first: `dealClosureOutcomeFromStatus` above already matches
|
|
632
|
+
// case-insensitively, so matching `closed` exactly would let `Closed` through
|
|
633
|
+
// this guard and null the operator's loss notes.
|
|
634
|
+
!isClosedDealStatus(canonicalDealStatus(parsed.status)) && record.closureOutcome !== null) {
|
|
635
|
+
record.closureOutcome = null;
|
|
636
|
+
if (parsed.lossReasonId === void 0) record.lossReasonId = null;
|
|
637
|
+
if (parsed.lossNotes === void 0) record.lossNotes = null;
|
|
638
|
+
}
|
|
652
639
|
if (parsed.lossReasonId !== void 0) record.lossReasonId = parsed.lossReasonId ?? null;
|
|
653
640
|
if (parsed.lossNotes !== void 0) record.lossNotes = parsed.lossNotes ?? null;
|
|
654
641
|
},
|