From 436bdeaac102b6f15260a88703ba5a173dac8512 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 06:04:55 +0000 Subject: [PATCH 1/3] wip(docs): getting started, activities and metrics pages, with the metrics definition guard (#2329) The Tasks section moves from Opportunities into the new Activities page. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01J8VVAkXhMGs3b6W5rW9ioC --- src/docs/crm_analytics_metrics.md | 126 ++++++++++++++ src/docs/crm_analytics_metrics.zh.md | 98 +++++++++++ src/docs/crm_getting_started.md | 86 ++++++++++ src/docs/crm_sales_activities.md | 86 ++++++++++ src/docs/crm_sales_opportunities.md | 13 +- src/docs/crm_sales_opportunities.zh.md | 8 +- test/docs-analytics-metrics.test.ts | 217 +++++++++++++++++++++++++ 7 files changed, 618 insertions(+), 16 deletions(-) create mode 100644 src/docs/crm_analytics_metrics.md create mode 100644 src/docs/crm_analytics_metrics.zh.md create mode 100644 src/docs/crm_getting_started.md create mode 100644 src/docs/crm_sales_activities.md create mode 100644 test/docs-analytics-metrics.test.ts diff --git a/src/docs/crm_analytics_metrics.md b/src/docs/crm_analytics_metrics.md new file mode 100644 index 00000000..a8e8d148 --- /dev/null +++ b/src/docs/crm_analytics_metrics.md @@ -0,0 +1,126 @@ +--- +title: Metrics — What the Dashboards Count +description: What win rate, pipeline, quota attainment, coverage and the churn signals count, so a number on a dashboard can be checked rather than guessed. +order: 1 +# sources: the analytics metadata this doc describes. Every definition below +# is pinned to it by test/docs-analytics-metrics.test.ts; build ignores +# unknown frontmatter keys. +sources: + - dataset:opportunity_metrics + - dataset:forecast_metrics + - dataset:account_metrics + - dataset:lead_metrics + - dataset:event_metrics + - dataset:task_metrics + - report:customer_churn_signals + - report:pipeline_coverage_by_quarter +--- + +# Metrics — What the Dashboards Count + +For anyone who reads a dashboard and wants to know what a number means before +acting on it. The tiles show a name and a figure; this page says what each +figure counts, so two people looking at the same tile read it the same way. + +Every figure reads only the records **you** are allowed to see. A manager who +reads every deal sees company totals; a rep sees their own. + +## Deals and revenue + +These figures count opportunities. "Open" always means a deal that is not yet +**Closed Won** or **Closed Lost**. + +| Figure | What it counts | +|---|---| +| **Total Amount** | The sum of **Amount** over the deals in view. Pipeline tiles and won-revenue tiles are both this figure; what differs is which deals they keep. | +| **Avg Deal Size** | The average **Amount** over the deals in view. | +| **Avg Probability** | The average **Probability (%)**. Probability follows the stage, so this reads as how far along the deals are. | +| **Won Deals** | Deals at **Closed Won**. | +| **Lost Deals** | Deals at **Closed Lost**. | +| **Settled Deals** | Deals at **Closed Won** or **Closed Lost**: every deal that has an outcome. | +| **Won Revenue** | The **Amount** of deals at **Closed Won**. | +| **Lost Revenue** | The **Amount** of deals at **Closed Lost**. | +| **Win Rate** | **Won Deals** ÷ **Settled Deals**. Open deals are left out, so a busy pipeline does not drag the rate down. | + +A table that shows **Win Rate** always shows **Won Deals** and **Lost Deals** +beside it, so you can check the arithmetic. A blank rate next to some lost +deals means no wins yet, which is 0%, not missing data. + +The tiles apply these figures to a set of deals and a time window: + +| Tile | What it counts | +|---|---| +| **Open Pipeline (Selected Period)** | **Total Amount** of the deals not at **Closed Won** or **Closed Lost**, for the period the date picker selects. | +| **Closed Won (QTD)** | **Total Amount** of the deals at **Closed Won** whose **Close Date** falls in the current quarter so far. | +| **Total Revenue (YTD)** | **Total Amount** of the deals at **Closed Won** whose **Close Date** falls in the current year so far. | +| **Win Rate (12M)** | **Win Rate** over the deals whose **Close Date** falls in the last 12 months. | +| **Deals Won (12M)** | **Won Deals** whose **Close Date** falls in the last 12 months. | +| **Deals Lost (12M)** | **Lost Deals** whose **Close Date** falls in the last 12 months. | + +## Quota and attainment + +Quota figures come from the **Forecasts** records: one per owner per period, +holding that owner's quota and what they have closed, committed and still have +in the pipeline. + +| Figure | What it counts | +|---|---| +| **Quota** | The sum of **Quota** over the forecasts in view. | +| **Closed** | The sum of **Closed Won**: what the owners have already won. | +| **Pipeline** | The sum of **Pipeline**: the owners' open deals. | +| **Commit** | The sum of **Commit**: what the owners have committed to land. | +| **Attainment** | **Closed** ÷ **Quota**. The amounts are added up before dividing, so a large quota weighs more than a small one. | +| **Attainment %** | On one forecast record: **Closed Won** ÷ **Quota**. It reads 0% until a quota is set. | +| **Coverage Ratio** | On one forecast record: **Pipeline** ÷ (**Quota** − **Closed Won**), whether enough pipeline remains to cover what is still missing. It reads 0 once the quota is met. | +| **Quota Attainment by Rep** | **Quota**, **Closed** and **Attainment** per owner, from the **Quarter** forecasts of the current quarter only. | + +A period is always pinned. Adding a quarter's quota to a month's quota gives a +number that means nothing, so every quota figure either shows one period or +lists the periods side by side. + +Under **Reports**, **Pipeline Coverage** asks the same question for the whole +team: + +| Report | What it counts | +|---|---| +| **Pipeline Coverage by Forecast × Quarter** | **Total Amount** and **Opportunities** of the deals not at **Closed Won** or **Closed Lost**, by **Forecast Category** and **Close Quarter**. | + +## Activity + +| Figure | What it counts | +|---|---| +| **Activities** | Calls and meetings, one per event. | +| **Minutes** | The sum of **Duration (minutes)**. | +| **Interactions Logged** | **Activities** whose status is **Held**: contact that actually happened. | +| **Meetings Booked** | **Activities** of type **Meeting** whose status is **Planned**: meetings still ahead. | +| **Customer Minutes** | **Minutes** of the events whose status is **Held**. | +| **Tasks Completed** | **Tasks** whose **Is Completed** box is ticked. | + +Why only held events count as contact is in [Activities](./crm_sales_activities.md). + +## Accounts, leads and churn signals + +| Figure | What it counts | +|---|---| +| **Active Accounts** | **Accounts** marked **Active**. | +| **Open Leads** | **Leads** that are not **Converted**. | +| **Quiet 30+ Days** | **Accounts** marked **Active** whose **Last Activity Date** is more than 30 days ago. | +| **Quiet 60+ Days** | **Accounts** marked **Active** whose **Last Activity Date** is more than 60 days ago. | +| **Quiet 90+ Days** | **Accounts** marked **Active** whose **Last Activity Date** is more than 90 days ago. | + +**Customer Churn Signals**, under **Reports**, stacks four early warnings: + +| Block | What it counts | +|---|---| +| **CSM-Flagged Accounts** | **Accounts** marked **Active** whose **Health Score** a person has set to **At Risk** or **Churning**. It has no time window: it is the one signal a person asserts rather than a date. | +| **At-Risk Accounts** | **Accounts** marked **Active** whose **Last Activity Date** is more than 60 days ago. | +| **Silent High-Value Accounts** | **Accounts** marked **Active**, with **Customer Tier** **Strategic** or **Enterprise**, whose **Last Activity Date** is more than 90 days ago. | +| **Recently Lost Opportunities** | **Total Amount** and **Opportunities** of the deals at **Closed Lost** whose **Close Date** falls in the last 30 days. | + +The quiet and at-risk figures read **Last Activity Date**, which only a held +call or meeting moves. An account that is talked to every day is never quiet, +and one with only meetings booked for next quarter still is. + +Case figures (SLA compliance, resolution time, knowledge deflection) are +explained with the cases themselves, in [Cases & SLAs](./crm_service_cases.md) +and [Knowledge](./crm_service_knowledge.md). diff --git a/src/docs/crm_analytics_metrics.zh.md b/src/docs/crm_analytics_metrics.zh.md new file mode 100644 index 00000000..41a99dd0 --- /dev/null +++ b/src/docs/crm_analytics_metrics.zh.md @@ -0,0 +1,98 @@ +--- +title: 指标——仪表盘上的数字怎么算 +description: 赢率、管道、配额达成、覆盖倍数和客户流失信号各自统计什么,让仪表盘上的数字可以核对,而不是靠猜。 +--- + +# 指标——仪表盘上的数字怎么算 + +写给看仪表盘、想在行动之前弄清一个数字含义的每个人。磁贴上只有名称和数字;这一页说明每个数字统计的是什么,让两个人看同一块磁贴时读出同样的意思。 + +每个数字只统计**你**有权查看的记录。能看到全部商机的经理看到的是全公司的合计;销售代表看到的是自己的。 + +## 商机与收入 + +这些数字统计的是商机。"未结"始终指还没有到**成交**或**失败**的商机。 + +| 数字 | 统计内容 | +|---|---| +| **Total Amount** | 当前范围内商机的**金额**之和。管道磁贴和成交收入磁贴用的都是这个数字,区别只在保留哪些商机。 | +| **Avg Deal Size** | 当前范围内商机**金额**的平均值。 | +| **Avg Probability** | **成交概率 (%)** 的平均值。成交概率随阶段变化,所以它反映的是这些商机推进到了哪一步。 | +| **Won Deals** | 处于**成交**的商机。 | +| **Lost Deals** | 处于**失败**的商机。 | +| **Settled Deals** | 处于**成交**或**失败**的商机:所有已有结果的商机。 | +| **Won Revenue** | 处于**成交**的商机的**金额**。 | +| **Lost Revenue** | 处于**失败**的商机的**金额**。 | +| **Win Rate** | **Won Deals** ÷ **Settled Deals**。未结商机不计入,所以管道再满也不会拉低赢率。 | + +凡是显示 **Win Rate** 的表格,旁边都同时显示 **Won Deals** 和 **Lost Deals**,方便你核对算式。如果赢率是空的、旁边却有失败的商机,意思是还没有赢单,即 0%,而不是缺数据。 + +磁贴把这些数字套在一组商机和一个时间窗口上: + +| 磁贴 | 统计内容 | +|---|---| +| **Open Pipeline (Selected Period)** | 不处于**成交**或**失败**的商机的 **Total Amount**,时间段由日期选择器决定。 | +| **Closed Won (QTD)** | **预计成交日期**落在本季度至今、处于**成交**的商机的 **Total Amount**。 | +| **Total Revenue (YTD)** | **预计成交日期**落在本年至今、处于**成交**的商机的 **Total Amount**。 | +| **Win Rate (12M)** | **预计成交日期**落在最近 12 个月内的商机的 **Win Rate**。 | +| **Deals Won (12M)** | **预计成交日期**落在最近 12 个月内的 **Won Deals**。 | +| **Deals Lost (12M)** | **预计成交日期**落在最近 12 个月内的 **Lost Deals**。 | + +## 配额与达成 + +配额类数字来自**销售预测**记录:每位负责人每个周期一条,记着这位负责人的配额,以及已成交、已承诺和仍在管道中的金额。 + +| 数字 | 统计内容 | +|---|---| +| **Quota** | 当前范围内销售预测的**配额**之和。 | +| **Closed** | **已成交金额**之和:负责人们已经赢下的金额。 | +| **Pipeline** | **管道金额**之和:负责人们的未结商机。 | +| **Commit** | **承诺金额**之和:负责人们承诺能落地的金额。 | +| **Attainment** | **Closed** ÷ **Quota**。先把金额加总再相除,所以配额大的人权重更大。 | +| **达成率(%)** | 单条销售预测上的 **已成交金额** ÷ **配额**。设置配额之前显示 0%。 | +| **覆盖倍数** | 单条销售预测上的 **管道金额** ÷(**配额** − **已成交金额**),即剩下的管道够不够填补还差的部分。配额已完成后显示 0。 | +| **Quota Attainment by Rep** | 每位负责人的 **Quota**、**Closed** 和 **Attainment**,只取本季度的**季度**预测。 | + +周期始终是固定的。把一个季度的配额和一个月的配额加在一起,得到的数字没有意义,所以每个配额数字要么只显示一个周期,要么把各个周期并排列出。 + +在**报表**下,**管道覆盖率**用整个团队回答同一个问题: + +| 报表 | 统计内容 | +|---|---| +| **Pipeline Coverage by Forecast × Quarter** | 不处于**成交**或**失败**的商机的 **Total Amount** 和 **Opportunities**,按 **Forecast Category** 和 **Close Quarter** 分列。 | + +## 互动 + +| 数字 | 统计内容 | +|---|---| +| **Activities** | 电话和会议,每个活动算一次。 | +| **Minutes** | **时长(分钟)**之和。 | +| **Interactions Logged** | 状态为**已举行**的 **Activities**:真正发生过的接触。 | +| **Meetings Booked** | 类型为**会议**、状态为**已计划**的 **Activities**:还没到来的会议。 | +| **Customer Minutes** | 状态为**已举行**的活动的 **Minutes**。 | +| **Tasks Completed** | 勾选了**是否完成**的 **Tasks**。 | + +为什么只有已举行的活动才算接触,见[活动](./crm_sales_activities.md)。 + +## 客户、线索与流失信号 + +| 数字 | 统计内容 | +|---|---| +| **Active Accounts** | 标记为**是否活跃**的 **Accounts**。 | +| **Open Leads** | 未勾选**已转化**的 **Leads**。 | +| **Quiet 30+ Days** | 标记为**是否活跃**、**最近活动日期**在 30 天以前的 **Accounts**。 | +| **Quiet 60+ Days** | 标记为**是否活跃**、**最近活动日期**在 60 天以前的 **Accounts**。 | +| **Quiet 90+ Days** | 标记为**是否活跃**、**最近活动日期**在 90 天以前的 **Accounts**。 | + +**报表**下的**客户流失信号**把四个早期预警叠在一起: + +| 区块 | 统计内容 | +|---|---| +| **CSM-Flagged Accounts** | 标记为**是否活跃**、且**健康度**被人设为**风险**或**流失中**的 **Accounts**。它没有时间窗口:这是唯一一个由人判断、而不是由日期推出的信号。 | +| **At-Risk Accounts** | 标记为**是否活跃**、**最近活动日期**在 60 天以前的 **Accounts**。 | +| **Silent High-Value Accounts** | 标记为**是否活跃**、**客户分层**为**战略客户**或**企业客户**、**最近活动日期**在 90 天以前的 **Accounts**。 | +| **Recently Lost Opportunities** | **预计成交日期**落在最近 30 天内、处于**失败**的商机的 **Total Amount** 和 **Opportunities**。 | + +沉默客户和风险客户的数字读的是**最近活动日期**,而只有已举行的电话或会议才会推动它。每天都在沟通的客户永远不会显示为沉默;只约了下个季度会议的客户,仍然算沉默。 + +工单类数字(SLA 达成率、解决时长、知识库自助解决率)和工单本身放在一起讲,见[工单与 SLA](./crm_service_cases.md) 和[知识库](./crm_service_knowledge.md)。 diff --git a/src/docs/crm_getting_started.md b/src/docs/crm_getting_started.md new file mode 100644 index 00000000..807ee4b9 --- /dev/null +++ b/src/docs/crm_getting_started.md @@ -0,0 +1,86 @@ +--- +title: Getting Started — Your First Day +description: Where HotCRM opens for you, the My Work queue, the approvals Inbox, and where help and the assistant live. +order: 2 +# sources: the navigation this doc describes. Build ignores unknown +# frontmatter keys. +sources: + - app:crm_enterprise +--- + +# Getting Started — Your First Day + +For everyone opening HotCRM for the first time. The [Overview](./crm_overview.md) +explains what the four areas are for; this page is about the screens: where the +app opens, where your own work waits for you, and where to look when you are +stuck. + +## Where HotCRM opens + +The app opens on the first entry your sidebar shows, and that entry depends on +the job you hold: + +| You are | HotCRM opens on | +|---|---| +| A sales rep | **Sales Home** — your own figures, your leads, open deals and open tasks, and your upcoming events | +| A service agent or service manager | **Service Overview** — the case dashboard, at the top of **Service** | +| A sales manager, a marketer or an administrator | **Home** — the **Executive Overview** dashboard | + +The sidebar only shows what you are allowed to read: a sales rep sees no cases, +and a service agent sees no pipeline dashboards. If you expected an entry and it +is not there, your access is the reason, not a fault — ask your administrator. + +## Your own work: My Work + +**My Work** collects the records that are yours, so you rarely need to search +for them: + +- **My Tasks** — your open tasks, the next thing to do first. Tasks the system + creates for you (a follow-up on a stalled deal, an escalation) land here too; + see [Activities](./crm_sales_activities.md). +- **My Deals** and **My Leads** — the opportunities and leads you own. +- **My Calendar** — your meetings and calls. +- **Inbox** — approval requests waiting on you (below). +- **My Cases** — the cases you own, for service staff. + +Each of these opens a list page whose tabs across the top offer other views of +the same records, such as all tasks or the pipeline board. A different view is +a tab there, not another sidebar entry. + +## Approvals: the Inbox + +Some records stop and wait for a person: a large deal, a new account, a lead +conversion that needs sign-off. When one waits on **you**, it appears in +**My Work → Inbox**; open it there to approve or reject. The record's owner +is notified of the outcome. While a request is pending, the record it is about +is locked against edits. What triggers each approval is on the page +for that record, for example [Opportunities](./crm_sales_opportunities.md). + +## Notifications + +The bell at the top right holds the alerts HotCRM sends you: a new lead +assigned to you, an urgent task, a won deal, a reminder that is due. Many of +them also arrive by email. Each one links to the record it is about. + +## Finding a record + +The search box at the top (**Ctrl+K**, or **⌘K** on a Mac) finds records, +lists, dashboards and reports by name, and lists the records you opened +recently. + +## Asking the assistant + +Where your deployment runs the platform's assistant (ObjectOS Cloud and +Enterprise), you can ask it about your CRM in plain language: "which of my +deals are at risk?", "give me the full picture of Acme", "qualify this lead". +It reads only what you can read and changes nothing without asking you first. +What it can do here is described in [Using the assistant](./crm_ai_skills.md). +A self-hosted open-source install ships without the assistant, so the panel is +not there. + +## Help + +**Help**, the last group in the sidebar, holds the manuals. The **User Manual** +is this one. Administrators also see the **Admin Manual**, and installs with +the revenue module see a **Revenue Manual** for quotes and pricing. The group +starts collapsed; click **Help** to open it. diff --git a/src/docs/crm_sales_activities.md b/src/docs/crm_sales_activities.md new file mode 100644 index 00000000..b10bff1d --- /dev/null +++ b/src/docs/crm_sales_activities.md @@ -0,0 +1,86 @@ +--- +title: Activities — Tasks, Calls & Meetings +description: Tasks, logged calls and meetings, scheduled follow-ups, and the automation behind them — reminders, urgent alerts and the activity clock. +order: 4 +# sources: flows/objects this doc documents. Build ignores unknown +# frontmatter keys. +sources: + - flow:task_urgent_alert + - flow:task_due_reminder + - flow:schedule_followup + - object:crm_task + - object:crm_event +--- + +# Activities — Tasks, Calls & Meetings + +For sales reps, service staff and their managers. HotCRM keeps two kinds of +activity: **tasks** are things still to do, and **events** are calls and +meetings, planned or held. This page covers how each is created and the rules +that act on them. + +## Logging a call or a meeting + +Leads, contacts, accounts, opportunities and cases each carry three buttons: + +- **Log a Call** and **Log a Meeting** record something that already happened. + The event is saved as **held**, and the people you name are added as its + attendees. +- **Schedule a Meeting** books one in the future. It is saved as **planned** + and appears in **My Calendar**. + +The event is linked to the record you started from, so it shows in that +record's related lists and on the calendar views under **Events**. + +## Held events keep the activity clock honest + +Only a **held** event counts as contact with the customer. When one is saved, +HotCRM stamps the date on the records it touches: + +- the account's **Last Activity Date**, even when you logged the call on one of + its contacts, opportunities or cases, since the date walks up to the account; +- the lead's or contact's **Last Contacted** date; +- a case's **First Response Date**, the first time anything is held against it. + +A meeting that is only **planned** changes none of these, because it has not +happened yet. That is deliberate: the "quiet account" figures and the churn +report read these dates, and a meeting booked for next quarter must not make a +silent customer look busy. See [Metrics](./crm_analytics_metrics.md). + +## Scheduling a follow-up from a lead + +**Schedule Follow-up** on a lead asks for the next step, a due date, and +optionally the activity type, priority and notes. It creates the task, owned by +you and linked to the lead, and sets the lead's **Next Follow-up Date** to the +same day, so the **🔥 Hot Leads** tab on **Leads**, which sorts by that date, +reflects the commitment you actually made. + +## Tasks + +Creating a task with **Urgent** priority immediately notifies its owner. Routine +follow-up tasks (e.g. from the stalled-deal nudge) are created as **High** +priority. + +**Complete** on a task marks it done and stamps the completion date. Your open +tasks are in **My Work → My Tasks**. + +## Reminders (automatic) + +Give a task a **Reminder Date/Time** and, where the deployment has switched +scheduled work on (an administrator setting), an **hourly** sweep notifies the +owner once that time has passed, in the app and by email. Each task is reminded +once. A reminder later than the task's due date is refused: a reminder has to +fire before the deadline. + +## Do Not Call + +A lead or contact flagged **Do Not Call** cannot have a **Call** task scheduled +against it: the save is refused and says why. Recording a call that already +happened is still allowed. Clear the flag on the record first if the person has +asked to be called again. + +--- + +**Related:** the deals these activities move forward — see +[Opportunities](./crm_sales_opportunities.md); where you find your open work — +see [Getting Started](./crm_getting_started.md). diff --git a/src/docs/crm_sales_opportunities.md b/src/docs/crm_sales_opportunities.md index 581071f7..733172bf 100644 --- a/src/docs/crm_sales_opportunities.md +++ b/src/docs/crm_sales_opportunities.md @@ -1,6 +1,6 @@ --- title: Opportunities — Stages, Alerts & Approvals -description: Opportunity stages, the stalled-deal nudge, the won-deal alert, large-deal approvals and urgent tasks — every automated rule and threshold. +description: Opportunity stages, the stalled-deal nudge, the won-deal alert and large-deal approvals — every automated rule and threshold. order: 2 # sources: flows/objects this doc documents. Thresholds here are guarded by # test/docs-drift.test.ts; build ignores unknown frontmatter keys. @@ -8,9 +8,7 @@ sources: - flow:opportunity_approval - flow:opportunity_stagnation - flow:opportunity_won_alert - - flow:task_urgent_alert - object:crm_opportunity - - object:crm_task --- # Opportunities — Stages, Alerts & Approvals @@ -86,14 +84,9 @@ deal of $100K or more cannot be closed won until a new request is approved. The submitter can also **Recall** a pending request to unlock the deal; the next save resubmits it. Approvers act from their **Approvals → Inbox**. -## Tasks - -Creating a task with **Urgent** priority immediately notifies its owner. Routine -follow-up tasks (e.g. from the stalled-deal nudge) are created as **High** -priority. - --- **Related:** a deal starts life as a lead — see [Leads](./crm_sales_leads.md); once it is won, the relationship continues as a contract — see -[Contracts](./crm_sales_contracts.md). +[Contracts](./crm_sales_contracts.md); the tasks, calls and meetings that move +it along — see [Activities](./crm_sales_activities.md). diff --git a/src/docs/crm_sales_opportunities.zh.md b/src/docs/crm_sales_opportunities.zh.md index 4577beac..00736b1f 100644 --- a/src/docs/crm_sales_opportunities.zh.md +++ b/src/docs/crm_sales_opportunities.zh.md @@ -1,6 +1,6 @@ --- title: 商机——阶段、提醒与审批 -description: 商机阶段、停滞商机提醒、大单成交提醒、大额商机审批和紧急任务——每一条自动规则和门槛。 +description: 商机阶段、停滞商机提醒、大单成交提醒和大额商机审批——每一条自动规则和门槛。 --- # 商机——阶段、提醒与审批 @@ -56,10 +56,6 @@ stateDiagram-v2 审批进行期间,商机被**锁定**,其**审批状态**显示实时进展。全部通过后,它会被标记为**已批准**(并记录日期),你会收到通知。被驳回时,它会被标记为**已驳回**——商机解锁;修改后保存即可重新送审,因为一笔被驳回的、金额 $100,000 或以上的商机,在新的审批请求通过之前不能标记为成交。提交人也可以**撤回**一个待审批的请求来解锁商机;下一次保存会重新送审。审批人在**我的工作 › 待我审批**中处理。 -## 任务 - -创建优先级为**紧急**的任务会立即通知其负责人。例行的跟进任务(例如停滞商机提醒创建的任务)以**高**优先级创建。 - --- -**相关:** 商机的前身是线索——见[线索](./crm_sales_leads.md);赢单之后,客户关系以合同延续——见[合同](./crm_sales_contracts.md)。 +**相关:** 商机的前身是线索——见[线索](./crm_sales_leads.md);赢单之后,客户关系以合同延续——见[合同](./crm_sales_contracts.md);推动它前进的任务、电话和会议——见[活动](./crm_sales_activities.md)。 diff --git a/test/docs-analytics-metrics.test.ts b/test/docs-analytics-metrics.test.ts new file mode 100644 index 00000000..45e969f1 --- /dev/null +++ b/test/docs-analytics-metrics.test.ts @@ -0,0 +1,217 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { REPO_ROOT } from './helpers/repo-root'; +import stack, { packFor } from './helpers/composed-stack'; +import { packageStacks } from '../objectstack.composition'; + +/** + * The Metrics page says what each dashboard figure counts (#2329). + * + * `src/docs/crm_analytics_metrics.md` exists so a reader can check a number + * instead of guessing at it, which makes a wrong definition worse than none. + * A definition is prose about a machine fact — a measure's filter, a ratio's + * two halves, a tile's window — so this file holds each one to the metadata it + * describes, read from the compiled stack, the way + * `test/docs-lifecycle-diagrams.test.ts` holds the state diagrams: + * + * - every table row the page opens with a bold name is a definition, and the + * name must be a real figure: a dataset measure, a dashboard tile, a report + * or one of its blocks, or a formula field — in that order; + * - the row must name what the figure is built from: a ratio's two measures as + * `**A** ÷ **B**`, the field a sum or average reads, each filter value by + * its option label, each boolean filter by its field label, each date + * window by its field label and the window's phrase, and a tile's or + * report's measures by their labels. A changed filter, ratio or window + * turns the page red here; + * - every app-package measure that carries a filter or is derived — the ones + * a wrong denominator makes plausible and false — has a definition. + * + * Labels are read per locale: field and option labels from the object (English) + * or the zh-CN pack (`.zh.md`); measure, tile and report names in English in + * both, by AGENTS.md *Documentation discipline* rule 6. Polarity ("not at + * Closed Won") is prose and not checked. + */ + +type AnyRec = Record; +const S = stack as AnyRec; + +const PAGE = 'src/docs/crm_analytics_metrics'; + +const OBJECTS = new Map((S.objects as AnyRec[]).map((o) => [o.name, o])); +const DATASETS = (S.datasets ?? []) as AnyRec[]; +const WIDGETS = ((S.dashboards ?? []) as AnyRec[]).flatMap((d) => (d.widgets ?? []) as AnyRec[]); +const REPORTS = (S.reports ?? []) as AnyRec[]; +const APP_DATASETS = ((packageStacks as AnyRec[]).find((s) => s.manifest.type === 'app')!.datasets ?? []) as AnyRec[]; + +interface Locale { + suffix: string; + field: (object: string, field: string) => string; + option: (object: string, field: string, value: string) => string; + /** The phrase a date-macro window is written as. Unknown macro ⇒ deliberate failure. */ + window: Record; +} + +const objectField = (object: string, field: string): AnyRec => { + const f = OBJECTS.get(object)?.fields?.[field]; + if (!f) throw new Error(`${object}.${field} is not a field`); + return f; +}; + +const zh = packFor('zh-CN'); +const LOCALES: Locale[] = [ + { + suffix: '.md', + field: (o, f) => String(objectField(o, f).label), + option: (o, f, v) => String((objectField(o, f).options as AnyRec[]).find((x) => x.value === v)?.label), + window: { + '{12_months_ago}': 'last 12 months', '{30_days_ago}': '30 days', '{60_days_ago}': '60 days', + '{90_days_ago}': '90 days', '{current_quarter_start}': 'current quarter', '{current_year_start}': 'current year', + }, + }, + { + suffix: '.zh.md', + field: (o, f) => String(zh?.objects?.[o]?.fields?.[f]?.label), + option: (o, f, v) => String(zh?.objects?.[o]?.fields?.[f]?.options?.[v]), + window: { + '{12_months_ago}': '最近 12 个月', '{30_days_ago}': '30 天', '{60_days_ago}': '60 天', + '{90_days_ago}': '90 天', '{current_quarter_start}': '本季度', '{current_year_start}': '本年', + }, + }, +]; + +const bold = (s: string): string => `**${s}**`; +const measureOf = (dataset: AnyRec, name: string): AnyRec => { + const m = (dataset.measures as AnyRec[]).find((x) => x.name === name); + if (!m) throw new Error(`${dataset.name} has no measure ${name}`); + return m; +}; +const datasetNamed = (name: string): AnyRec => { + const d = DATASETS.find((x) => x.name === name); + if (!d) throw new Error(`no dataset ${name}`); + return d; +}; + +/** What a filter on `object` must be described by. `{today}` closes a window and is not written. */ +const filterTokens = (object: string, filter: AnyRec | undefined, L: Locale): string[] => + Object.entries(filter ?? {}).flatMap(([key, value]): string[] => { + const field = objectField(object, key); + const options = (values: unknown[]) => values.map((v) => bold(L.option(object, key, String(v)))); + if (typeof value === 'boolean') return [bold(L.field(object, key))]; + if (typeof value === 'string') { + if (value.startsWith('{')) { + if (!(value in L.window)) throw new Error(`no ${L.suffix} phrase for the window ${value}`); + return [L.window[value]]; + } + if (!field.options) throw new Error(`${object}.${key} = "${value}" is not an option filter`); + return options([value]); + } + if (value && typeof value === 'object') { + return Object.entries(value as AnyRec).flatMap(([op, operand]): string[] => { + if (op === '$in' || op === '$nin') return options(operand as unknown[]); + if (['$gte', '$gt', '$lte', '$lt'].includes(op) && typeof operand === 'string' && operand.startsWith('{')) { + if (operand === '{today}') return []; + if (!(operand in L.window)) throw new Error(`no ${L.suffix} phrase for the window ${operand}`); + return [bold(L.field(object, key)), L.window[operand]]; + } + if (op === '$ne' && operand === null) return [bold(L.field(object, key))]; + throw new Error(`unsupported filter ${object}.${key} ${op}`); + }); + } + throw new Error(`unsupported filter ${object}.${key}`); + }); + +const measureTokens = (dataset: AnyRec, m: AnyRec, L: Locale): string[] => { + if (m.derived) { + if (m.derived.op !== 'ratio') throw new Error(`${dataset.name}.${m.name}: unsupported derived op ${m.derived.op}`); + const [a, b] = (m.derived.of as string[]).map((n) => String(measureOf(dataset, n).label)); + return [`${bold(a)} ÷ ${bold(b)}`]; + } + return [ + ...(m.field ? [bold(L.field(dataset.object, m.field))] : []), + ...filterTokens(dataset.object, m.filter, L), + ]; +}; + +const valuesOf = (w: AnyRec): string[] => (w.values ?? w.measures ?? []) as string[]; + +/** Every reading of a name: the tokens a row naming it must carry. Empty ⇒ not a figure. */ +const readingsOf = (name: string, L: Locale): { what: string; tokens: string[] }[] => { + const measures = DATASETS.flatMap((d) => + (d.measures as AnyRec[]).filter((m) => m.label === name).map((m) => ({ d, m }))); + if (measures.length > 1) throw new Error(`"${name}" is the label of ${measures.length} measures — ambiguous`); + if (measures.length === 1) { + const { d, m } = measures[0]; + return [{ what: `measure ${d.name}.${m.name}`, tokens: measureTokens(d, m, L) }]; + } + const widgets = WIDGETS.filter((w) => w.title === name); + if (widgets.length > 0) { + return widgets.map((w) => { + const d = datasetNamed(w.dataset); + return { + what: `tile ${w.id ?? w.title}`, + tokens: [...valuesOf(w).map((v) => bold(String(measureOf(d, v).label))), ...filterTokens(d.object, w.filter, L)], + }; + }); + } + const reportish = REPORTS.flatMap((r) => [r, ...((r.blocks ?? []) as AnyRec[])]).filter((r) => r.label === name); + if (reportish.length > 0) { + return reportish.map((r) => { + const d = datasetNamed(r.dataset); + return { + what: `report ${r.name}`, + tokens: [...valuesOf(r).map((v) => bold(String(measureOf(d, v).label))), ...filterTokens(d.object, r.runtimeFilter, L)], + }; + }); + } + const formulas = [...OBJECTS.values()].flatMap((o) => + Object.entries((o.fields ?? {}) as AnyRec) + .filter(([f, def]) => def.type === 'formula' && L.field(o.name, f) === name) + .map(([f, def]) => ({ o, f, def }))); + return formulas.map(({ o, f, def }) => ({ + what: `formula ${o.name}.${f}`, + tokens: [...new Set([...String(def.expression).matchAll(/record\.([a-z_]+)/g)].map(([, ref]) => ref))] + .map((ref) => bold(L.field(o.name, ref))), + })); +}; + +/** `| **Name** | definition |` rows. */ +const definitionsIn = (markdown: string): { name: string; text: string }[] => + [...markdown.matchAll(/^\| \*\*([^*]+)\*\* \| (.+) \|$/gm)].map(([, name, text]) => ({ name, text })); + +describe('the Metrics page states what each figure really counts (#2329)', () => { + for (const L of LOCALES) { + const file = `${PAGE}${L.suffix}`; + const rows = definitionsIn(readFileSync(join(REPO_ROOT, file), 'utf8')); + + it(`${file} defines figures`, () => { + expect(rows.length, 'no `| **Name** | … |` rows found — the guard has gone vacuous').toBeGreaterThanOrEqual(20); + }); + + it(`${file} names only real figures, each by what it is built from`, () => { + const problems: string[] = []; + for (const { name, text } of rows) { + const readings = readingsOf(name, L); + if (readings.length === 0) { + problems.push(`**${name}** is not a measure, tile, report or formula field this app ships`); + continue; + } + for (const { what, tokens } of readings) { + const missing = tokens.filter((t) => !text.includes(t)); + if (missing.length) problems.push(`**${name}** (${what}) does not mention ${missing.join(', ')}`); + } + } + expect(problems).toEqual([]); + }); + + it(`${file} defines every filtered or derived measure of the app package`, () => { + const named = new Set(rows.map((r) => r.name)); + const undefinedMeasures = APP_DATASETS.flatMap((d) => + (d.measures as AnyRec[]).filter((m) => m.filter || m.derived).filter((m) => !named.has(String(m.label))) + .map((m) => `${d.name}.${m.name} (**${m.label}**)`)); + expect(undefinedMeasures).toEqual([]); + }); + } +}); From 25ed26345985e84842c913ebda25d4c2d4b2e684 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 06:14:28 +0000 Subject: [PATCH 2/3] wip(docs): AI skills, go-live checklist and data import pages; books, link-check rows and guards (#2329) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01J8VVAkXhMGs3b6W5rW9ioC --- .github/markdown-link-check-config.json | 12 +++ src/docs/crm_admin_automation.md | 8 +- src/docs/crm_admin_automation.zh.md | 2 +- src/docs/crm_admin_data_import.md | 95 +++++++++++++++++++++ src/docs/crm_admin_data_import.zh.md | 53 ++++++++++++ src/docs/crm_admin_setup.md | 99 ++++++++++++++++++++++ src/docs/crm_admin_setup.zh.md | 52 ++++++++++++ src/docs/crm_ai_skills.md | 100 +++++++++++++++++++++++ src/docs/crm_ai_skills.zh.md | 49 +++++++++++ src/docs/crm_getting_started.md | 7 +- src/docs/crm_getting_started.zh.md | 52 ++++++++++++ src/docs/crm_sales_activities.zh.md | 49 +++++++++++ src/sales/books/crm-admin-manual.book.ts | 8 +- src/sales/books/crm-manual.book.ts | 7 +- test/docs-ai-skills.test.ts | 78 ++++++++++++++++++ test/docs-import-matching.test.ts | 48 +++++++++++ test/docs-lifecycle-diagrams.test.ts | 2 + 17 files changed, 707 insertions(+), 14 deletions(-) create mode 100644 src/docs/crm_admin_data_import.md create mode 100644 src/docs/crm_admin_data_import.zh.md create mode 100644 src/docs/crm_admin_setup.md create mode 100644 src/docs/crm_admin_setup.zh.md create mode 100644 src/docs/crm_ai_skills.md create mode 100644 src/docs/crm_ai_skills.zh.md create mode 100644 src/docs/crm_getting_started.zh.md create mode 100644 src/docs/crm_sales_activities.zh.md create mode 100644 test/docs-ai-skills.test.ts create mode 100644 test/docs-import-matching.test.ts diff --git a/.github/markdown-link-check-config.json b/.github/markdown-link-check-config.json index b686b7be..c489d350 100644 --- a/.github/markdown-link-check-config.json +++ b/.github/markdown-link-check-config.json @@ -34,6 +34,18 @@ { "pattern": "^\\./(crm_admin_[a-z_]+\\.md)$", "replacement": "{{BASEURL}}/src/docs/$1" + }, + { + "pattern": "^\\./(crm_getting_[a-z_]+\\.md)$", + "replacement": "{{BASEURL}}/src/docs/$1" + }, + { + "pattern": "^\\./(crm_analytics_[a-z_]+\\.md)$", + "replacement": "{{BASEURL}}/src/docs/$1" + }, + { + "pattern": "^\\./(crm_ai_[a-z_]+\\.md)$", + "replacement": "{{BASEURL}}/src/docs/$1" } ] } diff --git a/src/docs/crm_admin_automation.md b/src/docs/crm_admin_automation.md index 92258d0a..ef29e8d6 100644 --- a/src/docs/crm_admin_automation.md +++ b/src/docs/crm_admin_automation.md @@ -63,11 +63,9 @@ renewal reminder goes out. The times above are what runs once the switch is on. HotCRM ships **skills**, not assistants of its own. The platform provides the assistant (ObjectStack ADR-0063: the runtime owns exactly two agents and the surface you are in binds one); an app extends it by authoring skills that -attach by surface affinity. HotCRM's six — live data, lead qualification, case -triage, email drafting, revenue forecasting, customer 360 — operate over live -CRM data and reach anything that *changes* state through the same Actions the -UI buttons use, so permissions and audit are identical. The end user simply -asks — no agent selection required. +attach by surface affinity. There is nothing to switch on per skill: which +skills exist is decided by the app. What each one does, and the buttons it may +press, is in [Using the Assistant](./crm_ai_skills.md). --- diff --git a/src/docs/crm_admin_automation.zh.md b/src/docs/crm_admin_automation.zh.md index 72d39172..4248c4d9 100644 --- a/src/docs/crm_admin_automation.zh.md +++ b/src/docs/crm_admin_automation.zh.md @@ -34,7 +34,7 @@ description: 每个自动化门槛和计划时间、定义它的流程、计划 ## AI 技能 -HotCRM 提供的是**技能**,而不是自己的助手。助手由平台提供(ObjectStack ADR-0063:运行时只拥有两个智能体,你所在的界面绑定其中一个);应用通过编写按界面亲和性挂载的技能来扩展它。HotCRM 的六个技能——实时数据、线索资格评估、工单分诊、邮件起草、收入预测、客户 360——在实时 CRM 数据上运行,任何*改变*状态的操作都通过与界面按钮相同的操作完成,所以权限和审计完全一致。最终用户只需直接提问——无需选择智能体。 +HotCRM 提供的是**技能**,而不是自己的助手。助手由平台提供(ObjectStack ADR-0063:运行时只拥有两个智能体,你所在的界面绑定其中一个);应用通过编写按界面亲和性挂载的技能来扩展它。技能不需要逐个开启:有哪些技能由应用决定。每项技能做什么、可以按哪些按钮,见[使用助手](./crm_ai_skills.md)。 --- diff --git a/src/docs/crm_admin_data_import.md b/src/docs/crm_admin_data_import.md new file mode 100644 index 00000000..0fec9191 --- /dev/null +++ b/src/docs/crm_admin_data_import.md @@ -0,0 +1,95 @@ +--- +title: Importing Data — Order, Matching & Refusals +description: The order to import accounts, contacts and leads in, how a row is matched to a record that already exists, and the refusals an import can meet. +order: 3 +# sources: the import mappings and integrity rules this doc describes. Build +# ignores unknown frontmatter keys. +sources: + - mapping:crm_account_import + - mapping:crm_contact_import + - mapping:crm_lead_import + - object:crm_account + - object:crm_contact + - object:crm_lead +--- + +# Importing Data — Order, Matching & Refusals + +For the administrator loading an existing customer base into HotCRM. This page +covers what HotCRM does with your rows; the file templates and the step-by-step +import screens are in the product documentation's import guide. + +## Import in this order + +1. **Accounts** first. +2. **Contacts** next: every contact names its account, and the account must + already exist. +3. **Leads** last. + +Open **Import** on the **Accounts**, **Contacts** or **Leads** list and upload +your file. When its columns are the ones HotCRM's template uses, the wizard +picks the saved mapping HotCRM ships for that object, such as +**Account Import (CSV)**: it knows the column headings and translates the +values other systems use, so there is nothing to map by hand. On the last step, +**Validate data** checks every row and writes nothing; import only once it is +clean. + +An import can be undone from **History** in the same dialog. Undo in the +reverse order (leads, then contacts, then accounts), so no contact is left +pointing at an account that is gone. + +## A row updates the record it matches + +Each saved mapping matches a row to an existing record by a business key: + +| Object | Matched on | What that means | +|---|---|---| +| **Account** | **Account Name** | A row naming an account that already exists updates it. Account names are unique within your organization. | +| **Contact** | **Email** | A row whose email already belongs to a contact updates that contact, including the account it sits under. One email address belongs to one contact in your organization. | +| **Lead** | **Email** | A row whose email already belongs to a lead updates that lead. | + +So re-running a corrected file updates rather than duplicates. It also means +a row is matched to a record you did not mean: if the demo company is still +in place, a real account that shares a demo account's name updates the demo +record. Clear the demo records first; see the +[Go-Live Checklist](./crm_admin_setup.md). + +## Leads that repeat someone you know + +A lead is never refused for repeating an email address. An import row whose +email matches an existing lead updates that lead, as above. When a new lead's +email belongs to a contact instead (or, for a lead created by hand or by a web +form, to an older lead), the lead is still created, and HotCRM sets its +**Duplicate Status** to **Suspected** and links it to that record: a contact +wins over a lead, and the oldest lead over newer ones. Review them in the **Suspected Duplicates** tab on **Leads**. A person +sets **Confirmed** after checking that the two are the same; nothing is merged +or deleted on its own. + +## Owners + +Each file can carry an owner column holding a user's email address. + +- An address that matches a user makes that user the owner. +- An address that matches no user fails the row. Nothing is guessed: invite + the user first, or clear the cell. +- A blank owner makes **you**, the person running the import, the owner. This + holds for leads too: imported leads are not spread across the sales reps the + way a web-form lead is. Reassign them after the import if they should be. + +## Common refusals + +A refused row names its row number and the field, so you can fix the file +and run it again. The ones you will meet most: + +- **A required value is missing** — for example a contact without its first + name, last name, email or account. +- **A value is not one of the options** — picklists take HotCRM's own labels or + codes, in any letter case, plus the common words other systems use. Anything + still unknown fails the row rather than being dropped. +- **A reference matches nothing** — an account name on a contact, or an owner + email, that names no existing record. +- **A website is not a full address** — write it with `https://`. + +Business rules run during an import exactly as they do on a save: a new lead +gets its follow-up deadline, and a row that breaks a rule the app enforces is +refused with that rule's own message. diff --git a/src/docs/crm_admin_data_import.zh.md b/src/docs/crm_admin_data_import.zh.md new file mode 100644 index 00000000..6b866ae5 --- /dev/null +++ b/src/docs/crm_admin_data_import.zh.md @@ -0,0 +1,53 @@ +--- +title: 导入数据——顺序、匹配与拒绝 +description: 客户、联系人和线索按什么顺序导入,一行数据如何匹配到已有记录,以及导入可能遇到的拒绝。 +--- + +# 导入数据——顺序、匹配与拒绝 + +写给要把现有客户数据导入 HotCRM 的管理员。本页讲 HotCRM 如何处理你的每一行数据;文件模板和导入界面的逐步操作,见产品文档里的导入指南。 + +## 按这个顺序导入 + +1. 先导入**客户**。 +2. 再导入**联系人**:每个联系人都要写明所属客户,而这个客户必须已经存在。 +3. 最后导入**线索**。 + +在**客户**、**联系人**或**线索**列表上打开**导入**并上传文件。如果文件的列就是 HotCRM 模板里的列,向导会自动选用 HotCRM 为这个对象提供的已保存映射,例如 **Account Import (CSV)**:它认得这些列标题,也会翻译其他系统使用的取值,所以不需要手动映射。在最后一步,**校验数据**会逐行检查且不写入任何内容;校验通过后再导入。 + +导入可以在同一个对话框的**历史记录**里撤销。请按相反的顺序撤销(先线索、再联系人、最后客户),这样就不会留下指向已删除客户的联系人。 + +## 一行数据会更新它匹配到的记录 + +每个已保存映射都按一个业务键把一行数据匹配到已有记录: + +| 对象 | 匹配依据 | 含义 | +|---|---|---| +| **客户** | **客户名称** | 一行写着已存在客户的名称,就会更新那个客户。客户名称在你的组织内唯一。 | +| **联系人** | **邮箱** | 一行的邮箱已经属于某个联系人,就会更新那个联系人,包括它所属的客户。一个邮箱地址在你的组织内只属于一个联系人。 | +| **线索** | **邮箱** | 一行的邮箱已经属于某条线索,就会更新那条线索。 | + +所以重新运行一份改正过的文件,会更新而不是重复创建。这也意味着一行可能匹配到你并不想动的记录:如果演示公司还在,一个与演示客户同名的真实客户会更新那条演示记录。请先清理演示记录,见[上线清单](./crm_admin_setup.md)。 + +## 重复已知人员的线索 + +线索从来不会因为邮箱重复而被拒绝。导入的一行如果邮箱匹配到已有线索,会像上面说的那样更新那条线索。如果新线索的邮箱属于某个联系人(或者,对于手工或网页表单创建的线索,属于一条更早的线索),线索仍会被创建,HotCRM 会把它的**重复状态**设为**疑似重复**,并关联到那条记录:联系人优先于线索,最早的线索优先于较新的线索。在**线索**的**疑似重复线索**标签页里复核它们。有人核对两条记录确是同一个人之后,再设为**已确认重复**;系统不会自动合并或删除任何记录。 + +## 负责人 + +每个文件都可以有一列负责人,填的是用户的邮箱地址。 + +- 邮箱匹配到某个用户,这个用户就是负责人。 +- 邮箱匹配不到任何用户,这一行会失败。系统不做猜测:先邀请这个用户,或者清空这个单元格。 +- 负责人留空,负责人就是**你**,也就是执行导入的人。线索也是如此:导入的线索不会像网页表单来的线索那样分给各位销售代表。如果需要,导入之后再重新分配。 + +## 常见的拒绝 + +被拒绝的行会写明行号和字段,方便你改好文件再运行一次。最常见的几种: + +- **缺少必填值**——例如联系人缺少名、姓、邮箱或所属客户。 +- **取值不在选项内**——选项列表接受 HotCRM 自己的标签或代码(不区分大小写),以及其他系统常用的说法。仍然无法识别的取值会让这一行失败,而不是被悄悄丢掉。 +- **引用匹配不到记录**——联系人上的客户名称,或负责人邮箱,找不到对应的已有记录。 +- **网址不完整**——请带上 `https://`。 + +导入时,业务规则与手工保存时完全一样地运行:新线索会拿到跟进期限;违反应用强制规则的行,会以那条规则自己的提示被拒绝。 diff --git a/src/docs/crm_admin_setup.md b/src/docs/crm_admin_setup.md new file mode 100644 index 00000000..fadbadfd --- /dev/null +++ b/src/docs/crm_admin_setup.md @@ -0,0 +1,99 @@ +--- +title: Go-Live Checklist — Setting HotCRM Up +description: The order to take HotCRM live in — demo data, people, positions, permission sets, scheduled work and your own data — and what each step changes. +order: 0 +# sources: the setup surface this doc walks through. Build ignores unknown +# frontmatter keys. +sources: + - position:crm_positions + - permission_set:crm_user + - permission_set:system_admin +--- + +# Go-Live Checklist — Setting HotCRM Up + +For the administrator taking HotCRM live. Work through the steps in order: each +one relies on the one before it. The rules behind each step (who sees what, +where each threshold lives) are on the other pages of this manual; this page is +the order to apply them in. + +## 1. Decide about the demo data + +A new HotCRM organization is filled with a demo company on its first start: +accounts, contacts, leads, deals, cases, quotes and the rest, so the dashboards +have something to show. The demo records are handed to the first +administrator. There is no switch in Setup for this; the build decides: + +- the standard build seeds the demo company into the organization; +- the multi-organization build gives each new organization the product + catalogue and nothing else. + +On the standard build, clear out the demo records you do not want **before** +you import your own: an import matches rows to the records already there, so +a real account that shares a demo account's name would update the demo record +instead of creating yours. See [Importing Data](./crm_admin_data_import.md). + +## 2. Bring people in + +Create your colleagues under **Setup → People & Organization → Users**, or +invite them under **Invitations**. A new user holds no HotCRM access until the +next two steps give them some; until then, HotCRM sends them to the platform's +access page instead of an empty app. + +## 3. Give each person a position + +Positions decide most of what a person can do, and they drive the automation +too. Open **Setup → Access Control → Positions**, open a position, and add its +holders. A person may hold more than one. Holding a position gives its +permission sets to the holder; a position with no permission set only widens +which records a holder can see. + +Three positions are routing pools, and the automation does nothing useful +until they are staffed: + +- **Sales Representative** — a new lead that arrives with no owner, such as + one from a web form, goes to the holder with the fewest open leads. +- **Service Agent** — new cases with no owner go to the holder with the fewest + open cases. While nobody holds it, new cases stay unassigned. +- **Service Manager** — an escalated case goes to the holder with the fewest + open cases. + +Two more carry approvals: a **Sales Manager** approves new accounts, lead +conversions and large deals, and a **Sales Director** also signs off the +largest deals. Put your regional reps in **NA Sales Team** or **EU Sales Team** +as well, so they can edit the accounts in their territory. +What each position opens up is in [Access](./crm_admin_access.md). + +## 4. Check the permission sets + +Positions hand out the everyday sets. Two sets are worth checking by hand under +**Setup → Access Control → Permission Sets**: + +- **System Administrator** — give it to the people who run HotCRM. It opens + this manual and grants list export, which the platform's own administrator + access does not. +- **CRM User** — HotCRM's floor for every member: their own leads, accounts, + deals, tasks and events, and read access to products, campaigns and + knowledge. Someone who holds no position needs at least this set to work in + HotCRM. + +## 5. Switch on scheduled work + +The daily and hourly sweeps (stalled deals, expiring quotes and contracts, SLA +breaches, renewal and task reminders) do not run until the deployment switches +scheduled work on. Until then nothing expires and no reminder goes out. How to +switch it on, and every schedule it starts, is in +[Automation](./crm_admin_automation.md). + +## 6. Bring in your data + +Import accounts first, then contacts, then leads. The order, the matching rules +and the refusals you may meet are in [Importing Data](./crm_admin_data_import.md). + +## 7. Check it as your users + +**Setup → People & Organization → Users** lets you impersonate a user from the +row menu. Sign in as one person of each job and check that HotCRM opens where +the [Getting Started](./crm_getting_started.md) page says it does for them, and +that the sidebar shows what they need. An entry missing for someone is almost +always a position or permission set missing from step 3 or 4. diff --git a/src/docs/crm_admin_setup.zh.md b/src/docs/crm_admin_setup.zh.md new file mode 100644 index 00000000..a5f0e7e8 --- /dev/null +++ b/src/docs/crm_admin_setup.zh.md @@ -0,0 +1,52 @@ +--- +title: 上线清单——配置 HotCRM +description: 让 HotCRM 上线的顺序——演示数据、人员、岗位、权限集、计划任务和你自己的数据——以及每一步改变了什么。 +--- + +# 上线清单——配置 HotCRM + +写给负责让 HotCRM 上线的管理员。请按顺序完成各步:每一步都依赖前一步。每一步背后的规则(谁能看到什么、每个门槛在哪里)写在本手册的其他页面里;这一页讲的是按什么顺序应用它们。 + +## 1. 决定如何处理演示数据 + +一个新的 HotCRM 组织在第一次启动时会被填入一家演示公司:客户、联系人、线索、商机、工单、报价单等等,让仪表盘有内容可看。这些演示记录归第一位管理员所有。设置里没有控制它的开关,由构建方式决定: + +- 标准构建会把演示公司写入组织; +- 多组织构建只给每个新组织写入产品目录,别的都不写。 + +在标准构建上,请在导入你自己的数据**之前**清理掉不需要的演示记录:导入会把每一行匹配到已有的记录上,所以一个与演示客户同名的真实客户会更新那条演示记录,而不是新建你的客户。见[导入数据](./crm_admin_data_import.md)。 + +## 2. 把人加进来 + +在**设置 → 人员与组织 → 用户**里创建同事,或者在**邀请**里邀请他们。新用户在接下来两步给他权限之前没有任何 HotCRM 访问权;在那之前,HotCRM 会把他引到平台的访问申请页,而不是给他一个空的应用。 + +## 3. 给每个人分配岗位 + +岗位决定了一个人能做的大部分事情,也驱动着自动化。打开**设置 → 访问控制 → 岗位**,打开一个岗位,添加它的任职人。一个人可以同时担任多个岗位。担任一个岗位,就获得这个岗位的权限集;没有权限集的岗位只会扩大任职人能看到的记录范围。 + +有三个岗位是分配池,没有人担任时,相关的自动化起不了作用: + +- **销售代表**——没有负责人的新线索(例如来自网页表单的线索)会分给手上未结线索最少的任职人。 +- **客服专员**——没有负责人的新工单会分给手上未关闭工单最少的任职人。没有人担任时,新工单保持未分配。 +- **服务经理**——升级的工单会分给手上未关闭工单最少的任职人。 + +还有两个岗位承担审批:**销售经理**审批新客户、线索转化和大额商机,**销售总监**还要签批最大的商机。另外,把各区域的销售代表加入**北美销售团队**或**欧洲销售团队**,他们才能编辑自己区域内的客户。每个岗位打开了哪些访问权,见[访问权限](./crm_admin_access.md)。 + +## 4. 检查权限集 + +日常使用的权限集由岗位分发。有两个权限集值得在**设置 → 访问控制 → 权限集**里手动检查: + +- **系统管理员**——分配给负责运维 HotCRM 的人。它打开这本手册,并授予列表导出权限,而平台自带的管理员权限不包含导出。 +- **CRM 用户**——HotCRM 给每个成员的底线权限:自己的线索、客户、商机、任务和活动,以及对产品、营销活动和知识库的读取权限。没有担任任何岗位的人,至少需要这个权限集才能在 HotCRM 里工作。 + +## 5. 开启计划任务 + +每天和每小时的扫描(停滞商机、报价单和合同过期、SLA 违约、续约和任务提醒)在部署开启计划任务之前都不会运行。在那之前,什么都不会过期,也不会发出任何提醒。如何开启,以及它启动的每个计划,见[自动化](./crm_admin_automation.md)。 + +## 6. 导入你的数据 + +先导入客户,再导入联系人,最后导入线索。顺序、匹配规则和可能遇到的拒绝,见[导入数据](./crm_admin_data_import.md)。 + +## 7. 以用户身份检查 + +**设置 → 人员与组织 → 用户**的行菜单里可以模拟某个用户登录。每种岗位各挑一个人登录,检查 HotCRM 是否打开在[快速上手](./crm_getting_started.md)所说的位置,以及侧边栏是否显示了他需要的内容。某人缺了某个入口,几乎总是第 3 步或第 4 步漏了一个岗位或权限集。 diff --git a/src/docs/crm_ai_skills.md b/src/docs/crm_ai_skills.md new file mode 100644 index 00000000..48edc60b --- /dev/null +++ b/src/docs/crm_ai_skills.md @@ -0,0 +1,100 @@ +--- +title: Using the Assistant — What It Can Do in HotCRM +description: The HotCRM skills the platform assistant uses, what each one does when you ask, and what it will not do on its own. +order: 3 +# sources: the skills this doc describes. Each section is pinned to its skill +# by test/docs-ai-skills.test.ts; build ignores unknown frontmatter keys. +sources: + - skill:live_data + - skill:lead_qualification + - skill:revenue_forecasting + - skill:customer_360 + - skill:email_drafting + - skill:case_triage +--- + +# Using the Assistant — What It Can Do in HotCRM + +For everyone whose deployment runs the platform's assistant. HotCRM does not +ship an assistant of its own: the assistant is the platform's, and it is +available on ObjectOS Cloud and Enterprise. A self-hosted open-source install +has none, and this page does not apply there. + +What HotCRM adds is **skills**: instructions that teach the assistant how this +CRM works. You never pick one. Ask in plain language, and the assistant uses +whichever skill fits; some start on their own when you are looking at the +record they are for. + +## The rules the skills follow + +- **It reads as you.** The assistant sees exactly the records and fields you + can see, and nothing else. It cannot show you a deal you are not allowed to + open. +- **It reads the current shape first.** Before it searches a record type, it + looks up that type's fields, so a field your administrator added a minute ago + is used straight away. +- **It cites what it read.** Answers name the records they come from, so you + can open them and check. When the data cannot support an answer, it says so + instead of estimating. +- **It changes data only through HotCRM's own buttons,** with the same + permissions and audit trail as clicking them yourself, and only the ones + named below. Everything else it recommends, and you do. + +## Live Data Access + +Answers questions about any CRM record: lists, single records, counts, sums +and averages. "How many open cases does Acme have?", "list my deals closing +this month". It reads only; it changes nothing. + +## Lead Qualification + +Scores a lead from 0 to 100 on budget, authority, need and timeline, with one +line per criterion naming what it read, and recommends one next step. It starts +on its own when you are on a lead. + +It can act, through two of the lead's buttons. With a score of 70 or more +and your agreement, it runs **Convert Lead**, and asks you to confirm first, +because a conversion cannot be undone from the assistant. Otherwise, or when +you want to nurture the lead, it runs **Schedule Follow-up** with the date it +recommends and says why that date. + +## Revenue Forecasting + +Summarises the pipeline by stage for the period you name (the current quarter +if you name none), lists the deals at risk (a close date already past, no +activity for 30 days or more, or a stage that has not moved for longer than +usual) and forecasts revenue as a range, from committed deals only up to the +full pipeline weighted by **Probability (%)**. It can draw a chart when a chart +says it better. It starts on its own on opportunities and dashboards, and it +reads only. + +These are the assistant's own estimates from live deals. The figures on the +dashboards are defined in [Metrics](./crm_analytics_metrics.md). + +## Customer 360 + +Builds one profile of an account: its contacts, open cases, open deals, and the +published knowledge articles that match those cases. The answer comes in three +parts (account snapshot, active work, risks and notes), and every risk names the +record behind it. It reads only. + +## Email Drafting + +Writes an email from the contact's real details and recent history: a short +subject naming the concrete value, a personal first line, one clear ask, and +two subject lines to choose from. It **cannot send**. Paste the draft into +**Send Email** on the contact, review it, and send it yourself. + +## Case Triage + +On a case, proposes a priority and the one reason that drove it, weighing the +customer's tier, whether they are blocked, the time left on the SLA and the +tone of their latest message. It cannot escalate or close a case: it points you +to **Escalate Case** or **Close Case** and offers the reason or resolution text +for you to paste. It starts on its own when you are on a case, on installs with +the service module. + +--- + +**Related:** where the assistant fits in your day — see +[Getting Started](./crm_getting_started.md). diff --git a/src/docs/crm_ai_skills.zh.md b/src/docs/crm_ai_skills.zh.md new file mode 100644 index 00000000..b4638f0b --- /dev/null +++ b/src/docs/crm_ai_skills.zh.md @@ -0,0 +1,49 @@ +--- +title: 使用助手——它在 HotCRM 里能做什么 +description: 平台助手使用的 HotCRM 技能、你提问时每项技能做什么,以及它不会自作主张做的事。 +--- + +# 使用助手——它在 HotCRM 里能做什么 + +写给部署中运行了平台助手的每个人。HotCRM 没有自带助手:助手是平台提供的,在 ObjectOS Cloud 和 Enterprise 上可用。自行部署的开源版本没有助手,本页对它不适用。 + +HotCRM 补充的是**技能**:教助手理解这套 CRM 如何运作的说明。你从来不用挑选技能。用平常的话提问,助手会使用合适的那一项;有些技能在你查看对应记录时会自动启用。 + +## 技能遵守的规则 + +- **它以你的身份读取。** 助手看到的记录和字段与你完全相同,不多也不少。它没法给你看一笔你无权打开的商机。 +- **它先读取当前结构。** 在查找某类记录之前,它会先查这类记录有哪些字段,所以管理员一分钟前新加的字段会立刻被用上。 +- **它注明出处。** 回答会写出依据的是哪些记录,方便你打开核对。数据不足以支撑结论时,它会直说,而不是估计。 +- **它只通过 HotCRM 自己的按钮修改数据**,权限和审计记录与你亲手点击完全一样,而且只限下文点名的那几个按钮。其余的事它只提建议,由你来做。 + +## Live Data Access + +回答关于任意 CRM 记录的问题:列表、单条记录、计数、求和与平均值。比如"Acme 有几个未关闭的工单?""列出我本月要成交的商机"。它只读取,不修改任何东西。 + +## Lead Qualification + +按预算、决策权、需求和时间线给线索打 0 到 100 分,每一项用一行说明它读到了什么,并推荐一个下一步。你打开线索时它会自动启用。 + +它可以通过线索上的两个按钮采取行动。分数在 70 分或以上、且你同意时,它会执行**转化线索**,并先请你确认,因为在助手里无法撤销一次转化。否则,或者你想继续培育这条线索时,它会执行**安排跟进**,填上它推荐的日期并说明理由。 + +## Revenue Forecasting + +按你指定的时间段(不指定则为本季度)按阶段汇总管道,列出有风险的商机(预计成交日期已过、30 天或更久没有活动、或者阶段停留得比平常久),并给出一个收入预测区间:下限只算已承诺的商机,上限是按**成交概率 (%)** 加权的全部管道。用图表更清楚时,它可以画图。你打开商机或仪表盘时它会自动启用,它只读取。 + +这些是助手根据实时商机给出的估计。仪表盘上的数字怎么算,见[指标](./crm_analytics_metrics.md)。 + +## Customer 360 + +为一个客户汇总出一份档案:联系人、未关闭的工单、未结商机,以及与这些工单相匹配的已发布知识文章。回答分三部分(客户概况、进行中的工作、风险与备注),每一条风险都点出背后的记录。它只读取。 + +## Email Drafting + +根据联系人的真实信息和近期往来写一封邮件:点明具体价值的简短主题、针对这个人的开头、一个明确的请求,并给出两个主题供你挑选。它**不能发送**。把草稿粘贴到联系人上的**发送邮件**里,检查后由你自己发送。 + +## Case Triage + +在工单上提出一个优先级,以及决定它的那一个理由,考虑的是客户分层、客户是否被卡住、SLA 剩余时间和客户最近一条消息的语气。它不能升级或关闭工单:它会引导你使用**升级工单**或**关闭工单**,并给出可以直接粘贴的原因或解决说明。在安装了服务模块的环境里,你打开工单时它会自动启用。 + +--- + +**相关:** 助手在你一天的工作中处于什么位置——见[快速上手](./crm_getting_started.md)。 diff --git a/src/docs/crm_getting_started.md b/src/docs/crm_getting_started.md index 807ee4b9..c643ab5c 100644 --- a/src/docs/crm_getting_started.md +++ b/src/docs/crm_getting_started.md @@ -80,7 +80,6 @@ not there. ## Help -**Help**, the last group in the sidebar, holds the manuals. The **User Manual** -is this one. Administrators also see the **Admin Manual**, and installs with -the revenue module see a **Revenue Manual** for quotes and pricing. The group -starts collapsed; click **Help** to open it. +**Help**, the last group in the sidebar, holds the manuals, this one included; +which guide to read for your job is in the [Overview](./crm_overview.md). The +group starts collapsed: click **Help** to open it. diff --git a/src/docs/crm_getting_started.zh.md b/src/docs/crm_getting_started.zh.md new file mode 100644 index 00000000..2f48c0b3 --- /dev/null +++ b/src/docs/crm_getting_started.zh.md @@ -0,0 +1,52 @@ +--- +title: 快速上手——第一天 +description: HotCRM 为你打开在哪里、我的工作队列、待我审批,以及帮助和助手在哪里。 +--- + +# 快速上手——第一天 + +写给第一次打开 HotCRM 的每个人。[概览](./crm_overview.md)讲的是四个业务领域各自做什么;这一页讲界面:应用打开在哪里、你自己的工作在哪里等着你,以及卡住时去哪里找答案。 + +## HotCRM 打开在哪里 + +应用会打开在你侧边栏显示的第一个入口上,而这个入口取决于你的岗位: + +| 你是 | HotCRM 打开在 | +|---|---| +| 销售代表 | **销售主页**——你自己的数字、你的线索、进行中的商机和待办任务,以及即将到来的活动 | +| 客服专员或服务经理 | **服务概览**——工单仪表盘,位于**服务**分组顶部 | +| 销售经理、市场人员或管理员 | **首页**——**Executive Overview** 仪表盘 | + +侧边栏只显示你有权查看的内容:销售代表看不到工单,客服专员看不到管道类仪表盘。如果你以为会有的入口不在,原因是你的权限,而不是故障——请联系你的管理员。 + +## 你自己的工作:我的工作 + +**我的工作**汇集了属于你的记录,你很少需要再去搜索: + +- **我的任务**——你的待办任务,最先要做的排在前面。系统为你创建的任务(停滞商机的跟进、工单升级)也会出现在这里;见[活动](./crm_sales_activities.md)。 +- **我的商机**和**我的线索**——你负责的商机和线索。 +- **我的日历**——你的会议和电话。 +- **待我审批**——等你处理的审批请求(见下文)。 +- **我的工单**——你负责的工单,供服务人员使用。 + +这些入口各自打开一个列表页,页面顶部的标签页提供同一批记录的其他视图,比如全部任务或管道看板。换一种视图就是那里的一个标签页,而不是侧边栏里再多一个入口。 + +## 审批:待我审批 + +有些记录会停下来等一个人处理:一笔大额商机、一个新客户、一次需要签批的线索转化。轮到**你**处理时,它会出现在**我的工作 › 待我审批**里;在那里打开它,批准或驳回。记录的负责人会收到结果通知。请求待审期间,相关记录会被锁定,不能编辑。每种审批由什么触发,写在该记录对应的页面里,例如[商机](./crm_sales_opportunities.md)。 + +## 通知 + +右上角的铃铛里是 HotCRM 发给你的提醒:分给你的新线索、紧急任务、大单成交、到期的提醒。很多提醒同时也会发邮件。每条提醒都链接到相关的记录。 + +## 查找记录 + +顶部的搜索框(**Ctrl+K**,Mac 上为 **⌘K**)可以按名称查找记录、列表、仪表盘和报表,也会列出你最近打开过的记录。 + +## 向助手提问 + +如果你的部署运行了平台助手(ObjectOS Cloud 和 Enterprise),你可以用平常的话问它关于 CRM 的问题:"我哪些商机有风险?""给我 Acme 的全貌""给这条线索打分"。它只读取你能读取的内容,不先问过你就不会修改任何东西。它在这里能做什么,见[使用助手](./crm_ai_skills.md)。自行部署的开源版本不带助手,所以没有这个面板。 + +## 帮助 + +**帮助**是侧边栏最后一个分组,里面是各本手册,包括你正在读的这本;按岗位该读哪一篇,见[概览](./crm_overview.md)。这个分组默认是收起的:点击**帮助**展开它。 diff --git a/src/docs/crm_sales_activities.zh.md b/src/docs/crm_sales_activities.zh.md new file mode 100644 index 00000000..a50196c5 --- /dev/null +++ b/src/docs/crm_sales_activities.zh.md @@ -0,0 +1,49 @@ +--- +title: 活动——任务、电话与会议 +description: 任务、电话和会议记录、安排跟进,以及它们背后的自动化:提醒、紧急提醒和活动时钟。 +--- + +# 活动——任务、电话与会议 + +写给销售代表、服务人员和他们的经理。HotCRM 记录两种活动:**任务**是还要做的事,**活动**是电话和会议,可能是已计划的,也可能是已举行的。本页讲每一种如何创建,以及作用在它们身上的规则。 + +## 记录电话或会议 + +线索、联系人、客户、商机和工单上都有三个按钮: + +- **记录通话**和**记录会议**用来记录已经发生的事。活动以**已举行**状态保存,你填写的人会成为它的参与者。 +- **安排会议**用来预约将来的会议。它以**已计划**状态保存,并出现在**我的日历**里。 + +活动会关联到你发起操作的那条记录,因此会出现在那条记录的相关列表里,以及**互动 › 活动**下的日历视图中。 + +## 已举行的活动让活动时钟保持真实 + +只有**已举行**的活动才算与客户的接触。保存一条已举行的活动时,HotCRM 会在相关记录上写下日期: + +- 客户的**最近活动日期**,即使你是在它的某个联系人、商机或工单上记录的电话,日期也会一路向上写到客户上; +- 线索或联系人的**最近联系时间**; +- 工单的**首次响应日期**,在它第一次有已举行的活动时写入。 + +只是**已计划**的会议不会改变这些日期,因为它还没有发生。这是有意为之:沉默客户的数字和客户流失报表读的就是这些日期,一个约在下个季度的会议不能让一个沉默的客户看起来很活跃。见[指标](./crm_analytics_metrics.md)。 + +## 在线索上安排跟进 + +线索上的**安排跟进**会询问下一步是什么、截止日期,以及可选的活动类型、优先级和备注。它会创建一条任务,由你负责、关联到这条线索,并把线索的**下次跟进日期**设为同一天,这样**线索**上按这个日期排序的 **🔥 高热度线索**标签页,就能反映你实际做出的承诺。 + +## 任务 + +创建优先级为**紧急**的任务会立即通知其负责人。例行的跟进任务(例如停滞商机提醒创建的任务)以**高**优先级创建。 + +任务上的**完成**会把任务标为已完成,并写下完成日期。你的待办任务在**我的工作 › 我的任务**里。 + +## 提醒(自动) + +给任务设置**提醒时间**后,在部署开启了计划任务(一项管理员设置)的环境里,**每小时**一次的扫描会在提醒时间到达后通知负责人,应用内和邮件各一份。每条任务只提醒一次。晚于任务截止日期的提醒会被拒绝:提醒必须在截止之前触发。 + +## 禁止致电 + +被标记为**禁止致电**的线索或联系人,不能再为其安排**电话**类型的任务:保存会被拒绝,并说明原因。记录一通已经打过的电话仍然可以。如果对方又同意接听电话了,先在记录上取消这个标记。 + +--- + +**相关:** 这些活动推动的商机——见[商机](./crm_sales_opportunities.md);去哪里找你的待办工作——见[快速上手](./crm_getting_started.md)。 diff --git a/src/sales/books/crm-admin-manual.book.ts b/src/sales/books/crm-admin-manual.book.ts index c2b7194b..44a663eb 100644 --- a/src/sales/books/crm-admin-manual.book.ts +++ b/src/sales/books/crm-admin-manual.book.ts @@ -4,8 +4,10 @@ import { defineBook } from '@objectstack/spec'; import { SystemAdminProfile } from '../profiles/system-admin.profile'; /** - * The Admin Manual (#2327) — who sees whose records, and where every automated - * threshold lives. Opened from *Help*, and only by an administrator. + * The Admin Manual (#2327) — the go-live order, who sees whose records, where + * every automated threshold lives, and how data comes in (#2329). Opened from + * *Help*, and only by an administrator. The pages order themselves by their + * `order:` frontmatter: the go-live checklist first, data import last. * * ## The audience is the administrator's permission set * @@ -27,7 +29,7 @@ import { SystemAdminProfile } from '../profiles/system-admin.profile'; export const CrmAdminManual = defineBook({ name: 'crm_admin_manual', label: 'HotCRM Admin Manual', - description: 'Access, sharing and every automated threshold — for administrators.', + description: 'Going live, access, every automated threshold and data import — for administrators.', icon: 'shield-check', order: 2, audience: { permissionSet: SystemAdminProfile.name }, diff --git a/src/sales/books/crm-manual.book.ts b/src/sales/books/crm-manual.book.ts index 31a58bfb..07fe01af 100644 --- a/src/sales/books/crm-manual.book.ts +++ b/src/sales/books/crm-manual.book.ts @@ -16,6 +16,10 @@ import { defineBook } from '@objectstack/spec'; * the UNION over the books that claim it (§6.7), so a group here matching * `crm_admin_*` would publish them to everyone. * + * Getting Started is the one curated group (`pages`): the overview, then the + * first-day page, then the assistant — an order a name glob cannot express + * (#2329). Analytics files its pages by rule like Sales. + * * The Service group reaches into the service module through `package`: its * docs live in `src/service/docs/` and belong to that package, and a group * rule matches only its own book's package unless it names another. Revenue @@ -29,8 +33,9 @@ export const CrmManual = defineBook({ icon: 'book-open', order: 1, groups: [ - { key: 'start', label: 'Getting Started', order: 1, pages: ['crm_overview'] }, + { key: 'start', label: 'Getting Started', order: 1, pages: ['crm_overview', 'crm_getting_started', 'crm_ai_skills'] }, { key: 'sales', label: 'Sales', order: 2, include: 'crm_sales_*' }, { key: 'service', label: 'Service', order: 3, include: 'crm_service_*', package: 'app.objectstack.hotcrm.service' }, + { key: 'analytics', label: 'Analytics', order: 4, include: 'crm_analytics_*' }, ], }); diff --git a/test/docs-ai-skills.test.ts b/test/docs-ai-skills.test.ts new file mode 100644 index 00000000..968f5f3d --- /dev/null +++ b/test/docs-ai-skills.test.ts @@ -0,0 +1,78 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { REPO_ROOT } from './helpers/repo-root'; +import stack, { packFor } from './helpers/composed-stack'; + +/** + * The assistant page describes the skills HotCRM ships, and only those (#2329). + * + * `src/docs/crm_ai_skills.md` gives each skill a section headed by its label, + * and tells the reader which buttons the assistant may press for them. Both + * are machine facts — the registered skills and the `action_*` tools each one + * declares — so this file reads them from the compiled stack: + * + * - every registered skill has exactly one `## LABEL` section, in both + * locales, and every section is a registered skill (the page's one rules + * section aside, which comes first); + * - a skill's section names, in bold, each action its tools reach, by the + * action's label (the zh-CN pack's on `.zh.md`). A skill that gains a + * state-changing tool turns the page red until the page says so. + * + * Skill labels stay English in both locales: the skills are not carried by + * the locale packs. + */ + +type AnyRec = Record; +const S = stack as AnyRec; +const SKILLS = (S.skills ?? []) as AnyRec[]; +const ACTIONS = (S.actions ?? []) as AnyRec[]; +const zh = packFor('zh-CN'); + +const LOCALES: { suffix: string; actionLabel: (a: AnyRec) => string }[] = [ + { suffix: '.md', actionLabel: (a) => String(a.label) }, + { suffix: '.zh.md', actionLabel: (a) => String(zh?.objects?.[a.objectName]?._actions?.[a.name]?.label) }, +]; + +/** `## Heading` → the text up to the next `## ` or the closing rule. */ +const sectionsOf = (markdown: string): Map => { + const out = new Map(); + const parts = markdown.split(/^## /m).slice(1); + for (const part of parts) { + const [heading, ...body] = part.split('\n'); + out.set(heading.trim(), body.join('\n').split(/^---$/m)[0]); + } + return out; +}; + +describe('the assistant page covers the registered skills (#2329)', () => { + it('reads real skills', () => { + expect(SKILLS.length).toBeGreaterThanOrEqual(5); + }); + + for (const L of LOCALES) { + const file = `src/docs/crm_ai_skills${L.suffix}`; + const sections = sectionsOf(readFileSync(join(REPO_ROOT, file), 'utf8')); + const headings = [...sections.keys()]; + + it(`${file} has one section per skill, and no section for a skill that is not shipped`, () => { + const labels = SKILLS.map((s) => String(s.label)); + expect(headings.slice(1).sort(), 'every heading after the rules section').toEqual([...labels].sort()); + }); + + it(`${file} names every button a skill may press`, () => { + const missing: string[] = []; + for (const skill of SKILLS) { + const text = sections.get(String(skill.label)) ?? ''; + for (const tool of ((skill.tools ?? []) as string[]).filter((t) => t.startsWith('action_'))) { + const action = ACTIONS.find((a) => `action_${a.name}` === tool); + if (!action) throw new Error(`${skill.name} declares ${tool}, which no registered action answers`); + if (!text.includes(`**${L.actionLabel(action)}**`)) missing.push(`${skill.label} → **${L.actionLabel(action)}**`); + } + } + expect(missing).toEqual([]); + }); + } +}); diff --git a/test/docs-import-matching.test.ts b/test/docs-import-matching.test.ts new file mode 100644 index 00000000..7721eec1 --- /dev/null +++ b/test/docs-import-matching.test.ts @@ -0,0 +1,48 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { REPO_ROOT } from './helpers/repo-root'; +import stack, { packFor } from './helpers/composed-stack'; + +/** + * The import page's matching table is the import mappings' upsert keys (#2329). + * + * `src/docs/crm_admin_data_import.md` tells an administrator which column a row + * is matched on — the reason re-running a file updates rather than duplicates, + * and the reason a leftover demo record gets overwritten. That is each saved + * mapping's `upsertKey`, so this file reads it from the compiled stack: one + * `| **OBJECT** | **FIELD** | … |` row per mapping, the object and field named + * by their labels (the zh-CN pack's on `.zh.md`), and no row for an object no + * mapping imports. + */ + +type AnyRec = Record; +const S = stack as AnyRec; +const MAPPINGS = (S.mappings ?? []) as AnyRec[]; +const OBJECTS = new Map((S.objects as AnyRec[]).map((o) => [o.name, o])); +const zh = packFor('zh-CN'); + +const LOCALES: { suffix: string; object: (o: string) => string; field: (o: string, f: string) => string }[] = [ + { suffix: '.md', object: (o) => String(OBJECTS.get(o)?.label), field: (o, f) => String(OBJECTS.get(o)?.fields?.[f]?.label) }, + { suffix: '.zh.md', object: (o) => String(zh?.objects?.[o]?.label), field: (o, f) => String(zh?.objects?.[o]?.fields?.[f]?.label) }, +]; + +describe('the import page states the key each mapping matches on (#2329)', () => { + it('reads real mappings', () => { + expect(MAPPINGS.filter((m) => m.mode === 'upsert').length).toBeGreaterThanOrEqual(3); + }); + + for (const L of LOCALES) { + const file = `src/docs/crm_admin_data_import${L.suffix}`; + const rows = [...readFileSync(join(REPO_ROOT, file), 'utf8').matchAll(/^\| \*\*([^*]+)\*\* \| \*\*([^*]+)\*\* \|/gm)] + .map(([, object, key]) => `${object} → ${key}`); + + it(`${file} has one row per upsert mapping, matched on its upsert key`, () => { + const expected = MAPPINGS.filter((m) => m.mode === 'upsert').map((m) => + `${L.object(m.targetObject)} → ${(m.upsertKey as string[]).map((f) => L.field(m.targetObject, f)).join(' + ')}`); + expect([...rows].sort()).toEqual([...expected].sort()); + }); + } +}); diff --git a/test/docs-lifecycle-diagrams.test.ts b/test/docs-lifecycle-diagrams.test.ts index 66ed6a95..838118a8 100644 --- a/test/docs-lifecycle-diagrams.test.ts +++ b/test/docs-lifecycle-diagrams.test.ts @@ -100,6 +100,8 @@ describe('the lifecycle diagrams in the package docs draw the real state machine it('no other package page draws a state diagram', () => { const others = [ 'docs/crm_overview', 'docs/crm_admin_access', 'docs/crm_admin_automation', 'service/docs/crm_service_knowledge', + 'docs/crm_getting_started', 'docs/crm_sales_activities', 'docs/crm_analytics_metrics', 'docs/crm_ai_skills', + 'docs/crm_admin_setup', 'docs/crm_admin_data_import', ].flatMap((p) => [`src/${p}.md`, `src/${p}.zh.md`]); expect(others.filter((f) => diagramsIn(readFileSync(join(REPO_ROOT, f), 'utf8')).length > 0)).toEqual([]); }); From 68351e0dc2a022f755d6eeec02752ed0d182d02e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 06:28:00 +0000 Subject: [PATCH 3/3] docs: getting-started assistant wording and the batch-2 changeset (#2329) The assistant paragraph now says it reads only what the member can read and changes records only through the member's own buttons, without promising a confirmation the Schedule Follow-up action does not ask for. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01J8VVAkXhMGs3b6W5rW9ioC --- .changeset/2329-in-app-docs-batch-2.md | 15 +++++++++++++++ src/docs/crm_getting_started.md | 5 +++-- src/docs/crm_getting_started.zh.md | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 .changeset/2329-in-app-docs-batch-2.md diff --git a/.changeset/2329-in-app-docs-batch-2.md b/.changeset/2329-in-app-docs-batch-2.md new file mode 100644 index 00000000..75f11abf --- /dev/null +++ b/.changeset/2329-in-app-docs-batch-2.md @@ -0,0 +1,15 @@ +--- +"hotcrm": minor +--- + +Clause-②: no +Metadata-protocol: none + +**Six more manual pages, each with a Chinese version.** The in-app manuals under **Help** gain the pages a new member and a new administrator reach for first: + +- User Manual, Getting Started: **Getting Started — Your First Day** (where HotCRM opens for each role, My Work, the approval inbox, notifications, search, Help) and **Using the Assistant** (what each HotCRM skill of the platform assistant does on ObjectOS Cloud and Enterprise, and which buttons it may press). +- User Manual, Sales: **Activities** (logging calls and meetings, the activity clock held meetings keep, Schedule Follow-up, tasks, reminders, Do Not Call). The Tasks section of the Opportunities page moves here. +- User Manual, new **Analytics** group: **Metrics** (what each dashboard measure, tile and report counts, from win rate to quiet accounts). +- Admin Manual: **Go-Live Checklist** (demo data, people, positions, permission sets, scheduled work, import, checking as a user), which the Admin Manual now opens on, and **Importing Data** (order, the key each row is matched on, suspected duplicate leads, owners, common refusals). + +The Automation page's list of AI skills is replaced by a link to Using the Assistant. The metrics, skills and import-matching statements are each checked against the compiled app, so a renamed measure, skill action or import key fails the build until the page follows. diff --git a/src/docs/crm_getting_started.md b/src/docs/crm_getting_started.md index c643ab5c..091cdb4c 100644 --- a/src/docs/crm_getting_started.md +++ b/src/docs/crm_getting_started.md @@ -73,8 +73,9 @@ recently. Where your deployment runs the platform's assistant (ObjectOS Cloud and Enterprise), you can ask it about your CRM in plain language: "which of my deals are at risk?", "give me the full picture of Acme", "qualify this lead". -It reads only what you can read and changes nothing without asking you first. -What it can do here is described in [Using the assistant](./crm_ai_skills.md). +It reads only what you can read, and the few changes it can make go through +the same buttons you would press. What it can do here, and which buttons, is in +[Using the assistant](./crm_ai_skills.md). A self-hosted open-source install ships without the assistant, so the panel is not there. diff --git a/src/docs/crm_getting_started.zh.md b/src/docs/crm_getting_started.zh.md index 2f48c0b3..09d883e7 100644 --- a/src/docs/crm_getting_started.zh.md +++ b/src/docs/crm_getting_started.zh.md @@ -45,7 +45,7 @@ description: HotCRM 为你打开在哪里、我的工作队列、待我审批, ## 向助手提问 -如果你的部署运行了平台助手(ObjectOS Cloud 和 Enterprise),你可以用平常的话问它关于 CRM 的问题:"我哪些商机有风险?""给我 Acme 的全貌""给这条线索打分"。它只读取你能读取的内容,不先问过你就不会修改任何东西。它在这里能做什么,见[使用助手](./crm_ai_skills.md)。自行部署的开源版本不带助手,所以没有这个面板。 +如果你的部署运行了平台助手(ObjectOS Cloud 和 Enterprise),你可以用平常的话问它关于 CRM 的问题:"我哪些商机有风险?""给我 Acme 的全貌""给这条线索打分"。它只读取你能读取的内容,它能做的少数修改也都通过你自己会按的那些按钮完成。它在这里能做什么、能按哪些按钮,见[使用助手](./crm_ai_skills.md)。自行部署的开源版本不带助手,所以没有这个面板。 ## 帮助