Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"hotcrm": patch
---

**Add to Campaign is no longer a button in a lead list's header.** On **My Leads**, **🔥 Hot Leads**, **High Priority**, **Suspected Duplicates**, **Lead Pipeline**, **Lead Calendar** and **Lead Cards**, the header carried an **Add to Campaign** button (加入营销活动 in Simplified Chinese) for marketing users and administrators. The button works on one lead at a time but was drawn with nothing selected: it opened the whole Campaign dialog, let you pick or even create a campaign, and only then refused in English ("This action runs on a single record — select a row first."), adding nobody. A campaign created from that dialog stayed behind.

Add to Campaign is now offered where a lead is already chosen: in every lead row's ⋮ menu, for that one lead, and on **All Leads**, where you tick the rows and run it from the selection toolbar for all of them at once. Who is offered it is unchanged: marketing users and administrators, not sales reps or sales managers. Contacts were not affected: their header never showed the button.

**The Opportunity Funnel by Owner → Stage report now charts amount by stage as a horizontal bar chart instead of a funnel.** The amounts do not shrink from one stage to the next (on the demo data, Negotiation and Closed Won hold the most money), so the funnel pinched and then flared, and the Negotiation label was hidden. Now each stage is one bar, top to bottom in sales order from Prospecting to Closed Won, and every stage name is shown in full. The report's name, its table and its numbers have not changed.
2 changes: 1 addition & 1 deletion content/docs/analytics/reports.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ HotCRM ships **ten** reports out of the box — the two report barrels, `src/sal
| **Opportunities by Stage** | Pipeline amount and average probability by stage, over deals closing this year and excluding closed-lost — with a bar chart of amount by stage |
| **Won Opportunities by Owner** | Closed-won amount per rep, charted as **Revenue by Sales Rep** |
| **Pipeline Coverage by Forecast × Quarter** | A matrix of open pipeline: forecast category down the rows, close quarter across the columns, with amount and deal count in each cell |
| **Opportunity Funnel by Owner → Stage** | A two-level funnel — each rep's book of business broken down stage by stage, with amount, deal count and average probability |
| **Opportunity Funnel by Owner → Stage** | Each rep's book of business broken down stage by stage, with amount, deal count and average probability, excluding closed-lost — with a horizontal bar chart of amount by stage across all reps |

All four bind `opportunity_metrics` (`src/sales/datasets/opportunity.dataset.ts`), which is what makes *pipeline* mean the same number here as on the dashboards.

Expand Down
2 changes: 1 addition & 1 deletion content/docs/analytics/reports.zh-Hans.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ HotCRM 开箱即用地发布 **十份** 报表 —— `src/sales/reports/index.t
| **Opportunities by Stage** | 按阶段的管道金额与平均赢单概率,只算今年内成交的、并排除已丢单的交易 —— 附一张按阶段的金额柱状图 |
| **Won Opportunities by Owner** | 每位销售代表的赢单金额,图表标题是 **Revenue by Sales Rep** |
| **Pipeline Coverage by Forecast × Quarter** | 未结管道的矩阵:行是预测类别,列是预计成交季度,每格给出金额与商机数 |
| **Opportunity Funnel by Owner → Stage** | 两级漏斗 —— 每位代表的商机盘子再按阶段拆开,带金额、商机数与平均概率 |
| **Opportunity Funnel by Owner → Stage** | 每位代表的商机盘子再按阶段拆开,带金额、商机数与平均概率,排除已丢单的交易 —— 附一张按阶段的金额条形图,汇总所有代表 |

四份都绑定 `opportunity_metrics`(`src/sales/datasets/opportunity.dataset.ts`),这也正是「管道」在这里和在仪表盘上是同一个数字的原因。

Expand Down
2 changes: 1 addition & 1 deletion content/docs/analytics/reports.zh-Hant.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ HotCRM 開箱即用地發布 **十份** 報表 —— `src/sales/reports/index.t
| **Opportunities by Stage** | 按階段的管道金額與平均贏單機率,只算今年內成交的、並排除已丟單的交易 —— 附一張按階段的金額長條圖 |
| **Won Opportunities by Owner** | 每位銷售代表的贏單金額,圖表標題是 **Revenue by Sales Rep** |
| **Pipeline Coverage by Forecast × Quarter** | 未結管道的矩陣:列是預測類別,欄是預計成交季度,每格給出金額與商機數 |
| **Opportunity Funnel by Owner → Stage** | 兩級漏斗 —— 每位代表的商機盤子再按階段拆開,帶金額、商機數與平均機率 |
| **Opportunity Funnel by Owner → Stage** | 每位代表的商機盤子再按階段拆開,帶金額、商機數與平均機率,排除已丟單的交易 —— 附一張按階段的金額橫條圖,彙總所有代表 |

四份都繫結 `opportunity_metrics`(`src/sales/datasets/opportunity.dataset.ts`),這也正是「管道」在這裡和在儀表板上是同一個數字的原因。

Expand Down
2 changes: 1 addition & 1 deletion content/docs/marketing/campaign-members.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ A **campaign member** is the link between a [campaign](/docs/marketing/campaigns
Three ways:

1. **Bulk enrollment, run from the campaign** — open a campaign and click **Enroll Members** in the record header; the action is offered while the campaign is *Planning* or *In Progress*. It opens the *Enrollment Criteria* screen of the [Campaign Enrollment flow](/docs/marketing/campaigns#campaign-enrollment-flow), where you pick the audience yourself — leads by status, or contacts by department — and it then creates a member per matching person with status *Sent*, skipping the already-enrolled and the opted-out. Nothing runs it for you: the criteria are answered on that screen, and a scheduled run would have nobody to answer them. A run that added everyone it could ends with *Eligible members enrolled in campaign.* A run in which anyone was refused ends with a refusal instead, giving both counts: how many people were enrolled and how many were refused. A refused person is not on the campaign.
2. **From a lead list or a contact list** — **Add to Campaign** ships on both. Tick the rows you want and run it from the selection toolbar, or run it on a single row from that row's ⋮ menu; either way you choose the campaign from a record picker. From the toolbar, a confirmation step lists the people you ticked, each one is enrolled on its own, and the result reads *Succeeded* with a count; from a row's menu, a confirmation toast follows. It is a list action on both objects, so opening a lead or a contact does not offer it. People already on that campaign are skipped, and the run still reports success for them, so re-running it never double-counts a touch — the skip-check is per relationship, so a person enrolled once as a lead and once as a contact is deliberately two memberships rather than a duplicate. There is still no *Add Members* picker on the campaign that reaches lead and contact list views.
2. **From a lead list or a contact list** — **Add to Campaign** ships on both. On **All Leads** or **All Contacts**, tick the rows you want and run it from the selection toolbar; on any list laid out in rows, you can also run it on a single row from that row's ⋮ menu. Either way you choose the campaign from a record picker. It is not a button in the list header, where nothing is selected yet. From the toolbar, a confirmation step lists the people you ticked, each one is enrolled on its own, and the result reads *Succeeded* with a count; from a row's menu, a confirmation toast follows. It is a list action on both objects, so opening a lead or a contact does not offer it. People already on that campaign are skipped, and the run still reports success for them, so re-running it never double-counts a touch — the skip-check is per relationship, so a person enrolled once as a lead and once as a contact is deliberately two memberships rather than a duplicate. There is still no *Add Members* picker on the campaign that reaches lead and contact list views.
3. **One at a time on the campaign** — open a campaign, find the **Campaign Members** panel on the *Related* tab, and use its **New** button to fill in a single membership by hand.

**Who can enroll.** Enrolling someone creates a campaign member, and all three ways check that one right, as the person doing it: their profile's permission to create campaign members. Marketing User and the administrators hold it. Sales Rep and Sales Manager do not: their profiles read campaign members and leave enrollment to marketing, so they are not offered **Enroll Members** or **Add to Campaign**, and a member they try to create anyway — through either of those, the panel's **New** or the API — is refused.
Expand Down
2 changes: 1 addition & 1 deletion content/docs/marketing/campaign-members.zh-Hans.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ description: 加入每个营销活动的线索和联系人,以及如何跟踪
三种方式:

1. **在营销活动上批量加入** — 打开一个营销活动,在记录页头部点击 **Enroll Members**(批量加入成员);该操作在营销活动处于 *Planning* 或 *In Progress* 时提供。它会打开 [Campaign Enrollment flow](/zh-Hans/docs/marketing/campaigns#campaign-enrollment-flow) 的 *Enrollment Criteria* 屏幕,由你自己在那里挑选受众——线索按状态,联系人按部门——然后为每个匹配的人创建一条状态为 *Sent* 的成员记录,并跳过已加入的人和已退订的人。没有任何东西会替你运行它:条件是在那个屏幕上回答的,而定时运行没有人来回答它们。能加的人都加上了,运行结束时显示 *Eligible members enrolled in campaign.*;只要有人被拒绝,运行就以拒绝结束,并同时给出两个数字:加入了多少人、被拒绝了多少人。被拒绝的人不在该活动中。
2. **从线索列表或联系人列表加入** — **Add to Campaign**(加入营销活动)两边都有。在列表里勾选需要的记录后批量运行,或在列表视图行的 ⋮ 菜单里对单条运行;两种方式都由记录选择器来选定营销活动。从工具栏运行时,确认步骤会列出你勾选的人,每个人单独加入,结果显示 *Succeeded* 和人数;从行菜单运行时,随后弹出一条确认提示。它在两个对象上都是列表操作,所以打开一条线索或一个联系人并不会提供它。已经在该活动里的人会被跳过,运行对他们同样报告成功,所以重复运行不会把一次触达算成两次——跳过检查按关系各查各的,因此同一个人以线索身份加入一次、又以联系人身份加入一次,是刻意保留的两条成员记录,而不是一次重复。营销活动上仍然没有一个 *Add Members* 选人器供你从线索和联系人列表视图中挑人。
2. **从线索列表或联系人列表加入** — **Add to Campaign**(加入营销活动)两边都有。在**全部线索**或**全部联系人**里勾选需要的记录后,从选择工具栏批量运行;在任何按行排列的列表里,也可以从某一行的 ⋮ 菜单对这一条运行。两种方式都由记录选择器来选定营销活动。列表标题栏上没有这个按钮,那里还什么都没有选中。从工具栏运行时,确认步骤会列出你勾选的人,每个人单独加入,结果显示 *Succeeded* 和人数;从行菜单运行时,随后弹出一条确认提示。它在两个对象上都是列表操作,所以打开一条线索或一个联系人并不会提供它。已经在该活动里的人会被跳过,运行对他们同样报告成功,所以重复运行不会把一次触达算成两次——跳过检查按关系各查各的,因此同一个人以线索身份加入一次、又以联系人身份加入一次,是刻意保留的两条成员记录,而不是一次重复。营销活动上仍然没有一个 *Add Members* 选人器供你从线索和联系人列表视图中挑人。
3. **在营销活动上逐条新建** — 打开营销活动,在 *Related*(相关)选项卡里找到 **Campaign Members** 面板,用面板上的 **New**(新建)按钮手工填写一条成员记录。

**谁能加入成员。** 把一个人加入活动就是创建一条营销活动成员记录,三种方式都以操作者本人的身份检查同一项权限:其权限档案是否允许创建营销活动成员。Marketing User 和管理员拥有这项权限。Sales Rep 和 Sales Manager 没有:他们的权限档案只能查看营销活动成员,加入成员交给市场部,所以他们看不到 **Enroll Members** 和 **Add to Campaign**;无论经由这两个操作、面板上的 **New** 还是 API,他们想创建的成员都会被拒绝。
Expand Down
2 changes: 1 addition & 1 deletion content/docs/marketing/campaign-members.zh-Hant.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ description: 加入每個行銷活動的潛在客戶和聯絡人,以及如何
三種方式:

1. **在行銷活動上批次加入** — 開啟一個行銷活動,在記錄頁標頭點擊 **Enroll Members**(批次加入成員);該操作在行銷活動處於 *Planning* 或 *In Progress* 時提供。它會開啟 [Campaign Enrollment flow](/zh-Hant/docs/marketing/campaigns#campaign-enrollment-flow) 的 *Enrollment Criteria* 畫面,由你自己在那裡挑選受眾——潛在客戶按狀態,聯絡人按部門——然後為每個符合的人建立一筆狀態為 *Sent* 的成員記錄,並略過已加入的人和已退訂的人。沒有任何東西會替你執行它:條件是在那個畫面上回答的,而定時執行沒有人來回答它們。能加的人都加上了,執行結束時顯示 *Eligible members enrolled in campaign.*;只要有人被拒絕,執行就以拒絕結束,並同時給出兩個數字:加入了多少人、被拒絕了多少人。被拒絕的人不在該活動中。
2. **從潛在客戶清單或聯絡人清單加入** — **Add to Campaign**(加入行銷活動)兩邊都有。在清單裡勾選需要的記錄後批次執行,或在清單檢視列的 ⋮ 選單裡對單筆執行;兩種方式都由記錄選擇器來選定行銷活動。從工具列執行時,確認步驟會列出你勾選的人,每個人單獨加入,結果顯示 *Succeeded* 和人數;從列選單執行時,隨後彈出一則確認提示。它在兩個物件上都是清單操作,所以開啟一筆潛在客戶或一個聯絡人並不會提供它。已經在該活動裡的人會被略過,執行對他們同樣回報成功,所以重複執行不會把一次觸達算成兩次——略過檢查按關係各查各的,因此同一個人以潛在客戶身分加入一次、又以聯絡人身分加入一次,是刻意保留的兩筆成員記錄,而不是一次重複。行銷活動上仍然沒有一個 *Add Members* 選人器讓你從潛在客戶和聯絡人清單檢視中挑人。
2. **從潛在客戶清單或聯絡人清單加入** — **Add to Campaign**(加入行銷活動)兩邊都有。在**全部線索**或**全部聯絡人**裡勾選需要的記錄後,從選擇工具列批次執行;在任何按列排列的清單裡,也可以從某一列的 ⋮ 選單對這一筆執行。兩種方式都由記錄選擇器來選定行銷活動。清單標題列上沒有這個按鈕,那裡還什麼都沒有選取。從工具列執行時,確認步驟會列出你勾選的人,每個人單獨加入,結果顯示 *Succeeded* 和人數;從列選單執行時,隨後彈出一則確認提示。它在兩個物件上都是清單操作,所以開啟一筆潛在客戶或一個聯絡人並不會提供它。已經在該活動裡的人會被略過,執行對他們同樣回報成功,所以重複執行不會把一次觸達算成兩次——略過檢查按關係各查各的,因此同一個人以潛在客戶身分加入一次、又以聯絡人身分加入一次,是刻意保留的兩筆成員記錄,而不是一次重複。行銷活動上仍然沒有一個 *Add Members* 選人器讓你從潛在客戶和聯絡人清單檢視中挑人。
3. **在行銷活動上逐筆新增** — 開啟行銷活動,在 *Related*(相關)索引標籤裡找到 **Campaign Members** 面板,用面板上的 **New**(新增)按鈕手動填寫一筆成員記錄。

**誰能加入成員。** 把一個人加入活動就是建立一筆行銷活動成員記錄,三種方式都以操作者本人的身分檢查同一項權限:其權限設定檔是否允許建立行銷活動成員。Marketing User 和管理員擁有這項權限。Sales Rep 和 Sales Manager 沒有:他們的權限設定檔只能檢視行銷活動成員,加入成員交給行銷部,所以他們看不到 **Enroll Members** 和 **Add to Campaign**;無論經由這兩個操作、面板上的 **New** 還是 API,他們想建立的成員都會被拒絕。
Expand Down
29 changes: 16 additions & 13 deletions src/sales/actions/lead.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,19 +184,22 @@ export const CreateCampaignAction: Action = {
target: 'campaign_lead_member_enroll',
// The flow's own input name for the lead (the door seeds `recordId` too).
recordIdParam: 'leadId',
// list_item gives every row a working record-scoped entry point (the row's
// menu). Multi-lead enrollment is the SELECTION-BAR button, which All Leads'
// `bulkActions: ['create_campaign']` produces and which fans out per row.
// Since 18.0.0-next.2 the console's list header leaves out a record-scoped
// list_toolbar action the active view already wires that way
// (objectui#12462), so on All Leads the action is drawn in the selection bar
// and on the row only (measured). A lead view that wires no `bulkActions`
// (My Leads, for one) still draws the list_toolbar button in its header, and
// that button is single-record by construction: with no `_selectedIds` in
// the bag the dispatcher resolves a recordId from the grid selection, and a
// multi-row selection there is ambiguous, so it is refused client-side with
// "This action runs on a single record — select exactly one row."
locations: ['list_toolbar', 'list_item'],
// Two doors, and no header button (#2073 item 5):
// - `list_item`: every row's menu, on every lead view — the row's lead.
// - the SELECTION BAR, which a view opens by naming the action in its
// `bulkActions` (All Leads does) and which fans out once per ticked row.
// That placement is the view's, so it takes no location here.
// ⛔ Do not add `list_toolbar` back. A header button is drawn with nothing
// selected, and this action runs on exactly one record: launched from the
// header it opened the whole Campaign dialog, then refused in English
// ("This action runs on a single record — select a row first.") and sent
// nothing. The console leaves it out of the header only on a view whose
// `bulkActions` names it (objectui#12462). Measured on 18.0.0-next.5, the
// seven lead views that name it nowhere (My Leads, Hot Leads, High Priority,
// Suspected Duplicates, and the kanban, calendar and gallery views) all drew
// it, and a view a user creates names nothing either. On those views a
// marketing user gets no row checkboxes at all, so it could never succeed.
locations: ['list_item'],
// The server refusal is the gate; this only stops offering a button that
// cannot succeed (see the header).
visible: P`current_user.can("crm_campaign_member", "create")`,
Expand Down
Loading
Loading