From 84bd6bfe5d9822133b7ebae34fdf5c9d41178bd9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 05:24:30 +0000 Subject: [PATCH 1/3] feat(analytics): dashboard captions state their base; one money and number format (#2370) [WIP] Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01J8VVAoAy1McNK4cq14Q2vC --- docs/README.md | 1 + docs/developers/number-formats.md | 60 ++++++++++ .../objects/opportunity_line_item.object.ts | 10 +- src/revenue/objects/price_book.object.ts | 6 + src/revenue/objects/quote.object.ts | 6 + src/revenue/objects/quote_line_item.object.ts | 22 +++- src/sales/apps/crm.app.ts | 11 +- src/sales/dashboards/crm.dashboard.ts | 41 +++---- src/sales/dashboards/executive.dashboard.ts | 3 + src/sales/dashboards/sales.dashboard.ts | 107 ++++++++---------- src/sales/pages/home.page.ts | 7 ++ src/sales/reports/lead.report.ts | 2 +- src/sales/reports/opportunity.report.ts | 44 +++---- src/sales/translations/en/app.ts | 10 +- src/sales/translations/es-ES/app.ts | 30 +++-- src/sales/translations/ja-JP/app.ts | 28 +++-- src/sales/translations/zh-CN/app.ts | 28 +++-- src/service/dashboards/service.dashboard.ts | 23 ++-- src/service/index.ts | 2 +- src/service/reports/case.report.ts | 10 +- test/app-navigation-shape.test.ts | 25 +++- test/case-sla-breach-on-close.test.ts | 44 +++---- test/pipeline-by-stage-chart.test.ts | 32 +++--- 23 files changed, 337 insertions(+), 215 deletions(-) create mode 100644 docs/developers/number-formats.md diff --git a/docs/README.md b/docs/README.md index 2825b37da..45268355b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -27,6 +27,7 @@ HotCRM also has product-facing documentation under [`content/docs/`](../content/ | Customer requirements and product disposition | [requirements/README.md](requirements/README.md) | | Where object and field metadata is declared | [developers/api_reference.md](developers/api_reference.md) | | ObjectStack code examples | [developers/code_examples.md](developers/code_examples.md) | +| How amounts, quantities, percentages and dates are shown, and which key carries each | [developers/number-formats.md](developers/number-formats.md) | ## Documentation Boundaries diff --git a/docs/developers/number-formats.md b/docs/developers/number-formats.md new file mode 100644 index 000000000..342117045 --- /dev/null +++ b/docs/developers/number-formats.md @@ -0,0 +1,60 @@ +# Money, number and date formats + +One convention for how HotCRM shows amounts, counts, percentages and dates, so the same +number reads the same way on a dashboard, a list, a record page and Sales Home (#2370). +Each rule names the metadata that carries it. The platform owns the rendering; this page +only says which declared key HotCRM sets, and which gaps were measured on platform 18.2.0 +and left to the platform. + +## The rules + +| What | How it reads | Carried by | +| --- | --- | --- | +| An amount of money on a record or in a list | with the currency and its minor unit: `$86,400.00` | a `Field.currency()` field, no `currencyConfig` (the tenant currency) | +| An amount on a KPI tile, chart or dashboard table | with the currency, whole units: `$1,290,000` | the dataset measure's `format: '0,0'`; the currency comes from the tenant | +| An amount on a Sales Home KPI (`object-metric`) | with the currency, whole units | the component's `format: '0,0'` | +| An amount computed by a formula | as an amount of money | the formula's `returnType: 'currency'` | +| A count of units (a line's quantity) | a whole number: `4` | `Field.number({ scale: 0 })` | +| A percentage on a record | the percentage, at the field's `scale` | `Field.percent()` | +| A ratio on a dashboard | a whole percentage: `67%` | the measure's `format: '0%'` | +| A date that bounds a commitment (quote date, expiry, a price book's validity) | always with the year, never relative: `Nov 27, '26` | the field's `format: 'short'` | +| Any other date | the console default: the year shown when it is not this year | nothing | + +## Currency: the tenant's, never written in metadata + +Every amount in HotCRM is in the tenant's default currency: no field declares a +`currencyConfig`, and no measure declares a `currency`. Multi-currency is the platform's +(objectstack epic #8345, REQ-0009 *Disposition* D); `price_book.object.ts` states the same +rule for price books. + +- The symbol is set once, by an administrator, in **Setup → Settings → Localization → + Default currency**. The platform fills it when an organization is created, from the + creator's language or region; when nothing can be derived it stays empty and every amount + reads as a plain number (`1,290,000`). A dev server seeded on a host with no locale is in + that state. +- With it set, the dashboards carry it with no metadata of ours: the analytics service puts + the tenant currency on a measure over a currency field (measured on 18.2.0: Executive + Overview's Total Revenue (YTD) read `$1,290,000` after the setting was saved, and `1,290,000` + before). +- ⛔ No `$` in a `format` pattern, no `currency: 'USD'` on a measure, no `prefix` on a + metric: each one pins a currency the tenant did not choose. + +## Whole units on KPI tiles + +A KPI is read at a glance, so it drops the cents. Dataset measures over an amount declare +`format: '0,0'` (`opportunity_metrics`, `forecast_metrics`, `product_metrics`, `account_metrics`). +An `object-metric` on a page has no measure, so it carries the same pattern itself; without it +Sales Home showed `1,220,000.00` beside the dashboards' `1,220,000`. + +## Measured on 18.2.0 and left to the platform + +These were measured while writing this page and are not worked around here (AGENTS.md, +*A bad platform default is fixed at the default*): + +- A formula with `returnType: 'currency'` still renders as a bare grouped number (`24,000` + beside `$6,000.00`) in the list, the related list and the record page. The key is declared + anyway: it is the spec's statement that the value is money. +- A `Field.percent()` draws a progress bar beside the percentage in a list and on a record + page. No field or page key turns it off. +- A metric tile prints the measure's label (`Total Amount`) under its value, not the widget's + `description`. diff --git a/src/revenue/objects/opportunity_line_item.object.ts b/src/revenue/objects/opportunity_line_item.object.ts index 06cb8e698..0d15ba41c 100644 --- a/src/revenue/objects/opportunity_line_item.object.ts +++ b/src/revenue/objects/opportunity_line_item.object.ts @@ -157,12 +157,15 @@ export const OpportunityLineItem = ObjectSchema.create({ description: "The product's title, kept in step with the product. It names this line.", }), + // A count of units, so a whole number (#2370 row 8; the convention is + // `docs/developers/number-formats.md`): `scale: 0` makes it read "4", not + // "4.00", on every face, and the engine refuses a fractional quantity. quantity: Field.number({ label: 'Quantity', group: 'pricing', required: true, storage: { notNull: true }, - scale: 2, + scale: 0, min: 0, defaultValue: 1, trackHistory: true, @@ -210,6 +213,11 @@ export const OpportunityLineItem = ObjectSchema.create({ total_price: Field.formula({ label: 'Total', group: 'pricing', + // An amount of money, declared as one (#2370 row 8): the spec's money + // formula, in the tenant currency like every amount here. Measured on + // 18.2.0: the console's formula cell does not read it yet and draws + // "24,000" beside "$6,000.00" (reported upstream with the card). + returnType: 'currency', expression: F`record.quantity * record.unit_price * (1 - record.discount / 100)`, }), diff --git a/src/revenue/objects/price_book.object.ts b/src/revenue/objects/price_book.object.ts index caf62d4b1..b14b401e2 100644 --- a/src/revenue/objects/price_book.object.ts +++ b/src/revenue/objects/price_book.object.ts @@ -91,11 +91,17 @@ export const PriceBook = ObjectSchema.create({ valid_from: Field.date({ label: 'Valid From', group: 'validity', + // A validity window is read as dates (#2370 row 9): the console's + // default style said "In 5 days" inside a week of today and dropped the + // year for this year's dates. `short` always reads "Nov 3, '26" + // (docs/developers/number-formats.md). Same on `valid_to`. + format: 'short', }), valid_to: Field.date({ label: 'Valid To', group: 'validity', + format: 'short', }), }, diff --git a/src/revenue/objects/quote.object.ts b/src/revenue/objects/quote.object.ts index d08dce710..40ed5aeec 100644 --- a/src/revenue/objects/quote.object.ts +++ b/src/revenue/objects/quote.object.ts @@ -212,6 +212,11 @@ export const Quote = ObjectSchema.create({ quote_date: Field.date({ label: 'Quote Date', group: 'terms', + // Dated with the year, never "Yesterday" (#2370 row 9): a quote's dates + // bound a commitment, and the console's default style read them + // relative within a week and dropped the year for this year's dates. + // `short` reads "Sep 14, '26" (docs/developers/number-formats.md). + format: 'short', required: true, storage: { notNull: true }, defaultValue: cel`today()`, @@ -236,6 +241,7 @@ export const Quote = ObjectSchema.create({ expiration_date: Field.date({ label: 'Expiration Date', group: 'terms', + format: 'short', // with the year, as `quote_date` above required: true, storage: { notNull: true }, defaultValue: cel`addDays(record.quote_date, 30)`, diff --git a/src/revenue/objects/quote_line_item.object.ts b/src/revenue/objects/quote_line_item.object.ts index 8314d4086..b0c61868f 100644 --- a/src/revenue/objects/quote_line_item.object.ts +++ b/src/revenue/objects/quote_line_item.object.ts @@ -101,13 +101,18 @@ export const QuoteLineItem = ObjectSchema.create({ // mechanism that panel reads first (#944; the long note is on // `crm_campaign_member.crm_campaign`). A SUPERSET of the fallback it // replaces (`highlightFields` minus this lookup: Product / Quantity / - // Sales Price / Total) plus `subtotal`, the line's price after its own - // discount — the formula was evaluated on every read and shown in no list. + // Sales Price / Total) plus the line's own discount. + // + // `discount`, not `subtotal` (#2370 row 8): `subtotal` was added here as + // "the line's price after its own discount", but a line carries no tax, + // so its expression is `total_price`'s and the panel showed one amount + // twice. What the panel lacked is the discount that turns Sales Price + // into Total, so that column takes the slot. // // ⛔ Not added to `highlightFields`: the detail page hoists the first // four of that strip, so a fifth entry never reaches the strip, and // reordering it would evict `total_price` from the record's header. - relatedListColumns: ['crm_product', 'quantity', 'unit_price', 'subtotal', 'total_price'], + relatedListColumns: ['crm_product', 'quantity', 'unit_price', 'discount', 'total_price'], }), // Left on the restricting default on purpose — see the twin object: a @@ -134,12 +139,15 @@ export const QuoteLineItem = ObjectSchema.create({ description: "The product's title, kept in step with the product. It names this line.", }), + // A count of units, so a whole number (#2370 row 8; the convention is + // `docs/developers/number-formats.md`): `scale: 0` makes it read "4", not + // "4.00", on every face, and the engine refuses a fractional quantity. quantity: Field.number({ label: 'Quantity', group: 'pricing', required: true, storage: { notNull: true }, - scale: 2, + scale: 0, min: 0, defaultValue: 1, trackHistory: true, @@ -177,6 +185,7 @@ export const QuoteLineItem = ObjectSchema.create({ subtotal: Field.formula({ label: 'Subtotal', group: 'pricing', + returnType: 'currency', // money, as `total_price` below expression: F`record.quantity * record.unit_price * (1 - record.discount / 100)`, }), @@ -186,6 +195,11 @@ export const QuoteLineItem = ObjectSchema.create({ total_price: Field.formula({ label: 'Total', group: 'pricing', + // An amount of money, declared as one (#2370 row 8): the spec's money + // formula, in the tenant currency like every amount here. Measured on + // 18.2.0: the console's formula cell does not read it yet and draws + // "24,000" beside "$6,000.00" (reported upstream with the card). + returnType: 'currency', expression: F`record.quantity * record.unit_price * (1 - record.discount / 100)`, }), diff --git a/src/sales/apps/crm.app.ts b/src/sales/apps/crm.app.ts index c2460a1e8..f5a53fad1 100644 --- a/src/sales/apps/crm.app.ts +++ b/src/sales/apps/crm.app.ts @@ -462,6 +462,11 @@ export const CrmApp = App.create({ // `test/app-navigation-shape.test.ts` requires an entry for each report // either package registers, so a new report without one goes red. // + // Each entry's label IS its report's own `label`, here and in every + // locale pack (#2370 row 6): the sidebar said "Pipeline Coverage" and + // the report page "Pipeline Coverage by Forecast × Quarter", two names + // for one report. The same test pins the pair. + // // The only group collapsed on load: opened, it lists one row per report // (ten for an administrator, seven for a rep, measured on 18.1.0), which // would push everything under it down a screen, and the *All reports* @@ -474,10 +479,10 @@ export const CrmApp = App.create({ children: [ { id: 'nav_report_opportunities_by_stage', type: 'report', reportName: 'opportunities_by_stage', label: 'Opportunities by Stage', icon: 'chart-column', visible: OPPORTUNITY_AUDIENCE }, { id: 'nav_report_won_by_owner', type: 'report', reportName: 'won_opportunities_by_owner', label: 'Won Opportunities by Owner', icon: 'trophy', visible: OPPORTUNITY_AUDIENCE }, - { id: 'nav_report_pipeline_coverage', type: 'report', reportName: 'pipeline_coverage_by_quarter', label: 'Pipeline Coverage', icon: 'columns-3', visible: OPPORTUNITY_AUDIENCE }, + { id: 'nav_report_pipeline_coverage', type: 'report', reportName: 'pipeline_coverage_by_quarter', label: 'Pipeline Coverage by Quarter', icon: 'columns-3', visible: OPPORTUNITY_AUDIENCE }, { id: 'nav_report_funnel_by_owner', type: 'report', reportName: 'opportunity_funnel_owner_stage', label: 'Opportunity Funnel by Owner', icon: 'funnel', visible: OPPORTUNITY_AUDIENCE }, - { id: 'nav_report_lead_inflow', type: 'report', reportName: 'lead_inflow_by_month_source', label: 'Lead Engagement', icon: 'trending-up', visible: READS_LEADS }, - { id: 'nav_report_accounts_by_industry', type: 'report', reportName: 'accounts_by_industry_type', label: 'Accounts by Industry & Type', icon: 'building', visible: READS_ACCOUNTS }, + { id: 'nav_report_lead_inflow', type: 'report', reportName: 'lead_inflow_by_month_source', label: 'Lead Engagement by Month', icon: 'trending-up', visible: READS_LEADS }, + { id: 'nav_report_accounts_by_industry', type: 'report', reportName: 'accounts_by_industry_type', label: 'Accounts by Industry and Type', icon: 'building', visible: READS_ACCOUNTS }, { id: 'nav_report_churn_signals', type: 'report', reportName: 'customer_churn_signals', label: 'Customer Churn Signals', icon: 'heart-pulse', visible: READS_ACCOUNTS }, ], }, diff --git a/src/sales/dashboards/crm.dashboard.ts b/src/sales/dashboards/crm.dashboard.ts index dd3549b15..b9858f0b1 100644 --- a/src/sales/dashboards/crm.dashboard.ts +++ b/src/sales/dashboards/crm.dashboard.ts @@ -9,8 +9,9 @@ import { avgDealSizeMetricWidget, pipelineByStageWidget } from './shared-widgets * Single-page snapshot of revenue, pipeline, and customer activity. Designed * to mirror the polished CRM dashboard reference at * https://github.com/objectstack-ai/objectui/tree/main/examples/crm/src/dashboards - * — KPI tiles, an area-chart revenue trend, a lead-source donut, a - * pipeline-by-stage bar chart, and a recent-deals table. + * — KPI tiles, a pipeline-by-stage bar chart, a lead-source donut, and a + * pipeline-by-owner table. The revenue trend it used to carry lives on + * Executive Overview alone (#2370 row 2). * * This dashboard intentionally uses the framework's first-class metadata fields * (colorVariant, chartConfig, header, dateRange, descriptions, action buttons) @@ -105,24 +106,10 @@ export const CrmOverviewDashboard: Dashboard = { }), // ─── Charts Row 1 ───────────────────────────────────────────────── - { - id: 'revenue_trends', - title: 'Revenue Trends', - description: 'Closed-won revenue over the last 12 months', - type: 'area', - filter: { stage: 'closed_won', close_date: { $gte: '{12_months_ago}' } }, - filterBindings: { dateRange: false }, // self-scoped to 12 months — the date picker must not narrow it - colorVariant: 'success', - dataset: 'opportunity_metrics', dimensions: ['close_date'], values: ['total_amount'], - layout: { x: 0, y: 2, w: 9, h: 4 }, - chartConfig: { - showLegend: false, - showDataLabels: false, - colors: ['#10B981'], - interaction: { tooltips: true }, - }, - options: { dateGranularity: 'month' }, - }, + // No revenue trend here (#2370 row 2): the same 12-month closed-won area + // chart was on Executive Overview, Sales Performance and this dashboard. + // Its one home is Executive Overview. + pipelineByStageWidget({ x: 0, y: 2, w: 6, h: 4 }), { id: 'lead_source', title: 'Lead Source', @@ -130,7 +117,7 @@ export const CrmOverviewDashboard: Dashboard = { type: 'donut', colorVariant: 'purple', dataset: 'opportunity_metrics', dimensions: ['lead_source'], values: ['total_amount'], - layout: { x: 9, y: 2, w: 3, h: 4 }, + layout: { x: 6, y: 2, w: 6, h: 4 }, chartConfig: { showLegend: true, showDataLabels: true, @@ -139,11 +126,15 @@ export const CrmOverviewDashboard: Dashboard = { }, // ─── Charts Row 2 ───────────────────────────────────────────────── - pipelineByStageWidget({ x: 0, y: 6, w: 6, h: 4 }), { id: 'top_products', - title: 'Top Products', - description: 'Total list-price revenue by product category', + // It sums CATALOG list prices, so it says so (#2370 row 3). It was + // titled "Top Products" with a "revenue" description, which read as + // sales. Summing what was sold is not on offer: a line's `total_price` + // is a `formula`, and a dataset refuses `sum` over a formula field + // (DATASET_INVALID, measured on 18.2.0); no stored per-line amount exists. + title: 'List Price by Category', + description: 'Catalog list prices summed per product category, not sales', type: 'bar', colorVariant: 'blue', // crm_product has no `close_date`, so the date picker would fail it with @@ -155,7 +146,7 @@ export const CrmOverviewDashboard: Dashboard = { // column records whoever created the entry. filterBindings: { dateRange: false, owner_id: false }, dataset: 'product_metrics', dimensions: ['category'], values: ['list_price_sum'], - layout: { x: 6, y: 6, w: 6, h: 4 }, + layout: { x: 0, y: 6, w: 12, h: 4 }, chartConfig: { showLegend: false, showDataLabels: true, diff --git a/src/sales/dashboards/executive.dashboard.ts b/src/sales/dashboards/executive.dashboard.ts index 059cff22f..1f942097c 100644 --- a/src/sales/dashboards/executive.dashboard.ts +++ b/src/sales/dashboards/executive.dashboard.ts @@ -165,6 +165,9 @@ export const ExecutiveDashboard: Dashboard = { }, // ─── Row 2: Revenue Analysis ────────────────────────────────────── + // The revenue trend's one home (#2370 row 2). CRM Overview and Sales + // Performance each carried the same 12-month closed-won chart; leadership + // reads it here, on the dashboard the app opens on. { id: 'revenue_trend', title: 'Revenue Trend', diff --git a/src/sales/dashboards/sales.dashboard.ts b/src/sales/dashboards/sales.dashboard.ts index d07984f08..77d71b433 100644 --- a/src/sales/dashboards/sales.dashboard.ts +++ b/src/sales/dashboards/sales.dashboard.ts @@ -169,33 +169,11 @@ export const SalesDashboard: Dashboard = { layout: { x: 8, y: 2, w: 4, h: 1 }, }, - // ─── Row 3: Pipeline & Trends ───────────────────────────────────── + // ─── Row 3: Pipeline ────────────────────────────────────────────── + // No revenue trend here (#2370 row 2): the same 12-month closed-won + // area chart sat on Executive Overview, CRM Overview and this dashboard. + // It has one home now, Executive Overview's Revenue Analysis row. pipelineByStageWidget({ x: 0, y: 4, w: 6, h: 4 }), - { - id: 'monthly_revenue_trend', - title: 'Monthly Revenue Trend', - description: 'Closed-won revenue, last 12 months', - type: 'area', - filter: { stage: 'closed_won', close_date: { $gte: '{12_months_ago}' } }, - filterBindings: { dateRange: false }, // self-scoped to 12 months — the date picker must not narrow it - colorVariant: 'success', - dataset: 'opportunity_metrics', dimensions: ['close_date'], values: ['total_amount'], - layout: { x: 6, y: 4, w: 6, h: 4 }, - chartConfig: { - showLegend: false, - showDataLabels: false, - colors: ['#10B981'], - // No quota annotation line: ChartAnnotationSchema only takes a STATIC - // value, and the real quotas live per-owner/per-period in - // crm_forecast.quota (seeded 500k–1.5M — the old hardcoded 100000 was - // fiction). Quota vs. actual is the quota_attainment_by_rep table - // below, bound to the forecast_metrics dataset. - interaction: { tooltips: true }, - }, - options: { dateGranularity: 'month' }, - }, - - // ─── Row 4: Performance Breakdown ───────────────────────────────── { id: 'pipeline_by_forecast_category', title: 'Pipeline by Forecast Category', @@ -204,13 +182,14 @@ export const SalesDashboard: Dashboard = { filter: { stage: { $nin: ['closed_won', 'closed_lost'] } }, colorVariant: 'blue', dataset: 'opportunity_metrics', dimensions: ['forecast_category'], values: ['total_amount'], - layout: { x: 0, y: 8, w: 6, h: 4 }, + layout: { x: 6, y: 4, w: 6, h: 4 }, chartConfig: { showLegend: false, showDataLabels: true, colors: ['#4F46E5'], }, }, + // ─── Row 4: Where pipeline comes from, and why deals are lost ───── { id: 'lead_source_breakdown', title: 'Lead Source', @@ -219,7 +198,7 @@ export const SalesDashboard: Dashboard = { filter: { stage: { $nin: ['closed_lost'] } }, colorVariant: 'purple', dataset: 'opportunity_metrics', dimensions: ['lead_source'], values: ['total_amount'], - layout: { x: 6, y: 8, w: 6, h: 4 }, + layout: { x: 0, y: 8, w: 6, h: 4 }, chartConfig: { showLegend: true, showDataLabels: true, @@ -227,6 +206,34 @@ export const SalesDashboard: Dashboard = { }, }, + { + // The loss-reason breakdown the fields were declared for and never got. + // + // `loss_reason` is mandatory on every `closed_lost` record (see + // crm_opportunity), so this chart has no "unattributed" slice to + // apologise for — an empty chart here means no losses in the window, + // never "nobody filled the field in". + // + // Counts, not amounts: the question a loss review asks is "how often did + // we lose for this reason", and one large lost deal should not outweigh + // five recurring feature gaps. `lost_amount` is declared on the dataset + // for the revenue view of the same question. + id: 'loss_reason_breakdown', + title: 'Why We Lose', + description: 'Lost deals by reason — last 12 months', + type: 'donut', + filter: { stage: 'closed_lost', close_date: { $gte: '{12_months_ago}' } }, + filterBindings: { dateRange: false }, + colorVariant: 'orange', + dataset: 'opportunity_metrics', dimensions: ['loss_reason'], values: ['opp_count'], + layout: { x: 6, y: 8, w: 6, h: 4 }, + chartConfig: { + showLegend: true, + showDataLabels: true, + colors: ['#EF4444', '#F59E0B', '#8B5CF6', '#06B6D4', '#4F46E5', '#10B981', '#64748B'], + }, + }, + // ─── Row 5: Rep Leaderboard ─────────────────────────────────────── // A dashboard `table` binds to an analytics cube and aggregates; it cannot // list raw deals (ADR-0021). The previous "Top Open Opportunities" table @@ -257,7 +264,12 @@ export const SalesDashboard: Dashboard = { { id: 'quota_attainment_by_rep', title: 'Quota Attainment by Rep', - description: 'Current-quarter quota, closed revenue and attainment per rep, from forecast snapshots', + // The description says when the table fills (#2370 row 5): on a fresh + // demo org it is empty, because the current quarter has one producer, + // the nightly `forecast_snapshot` sweep (#702, so the seed may not open + // it), and `quota` has no automated writer. The console shows a widget's + // description in its header, above the empty state. + description: 'Current-quarter quota, closed revenue and attainment per rep. Fills once the nightly forecast snapshot opens the quarter and a manager sets each quota', type: 'table', colorVariant: 'default', // crm_forecast has neither `close_date` nor `type`; opt out of the @@ -324,7 +336,8 @@ export const SalesDashboard: Dashboard = { // ─── Row 7: Win / Loss analysis ─────────────────────────────────── // - // Three widgets over the settled half of the pipeline (#593). Each of the + // Two tables over the settled half of the pipeline (#593); Why We Lose, + // the third, sits beside Lead Source in row 4 since #2370. Each of the // two win-rate breakdowns is a TABLE, not a bar chart, and that is the // whole design: a bar of "67%" is unfalsifiable, whereas a row reading // "Won 2 · Lost 1 · Settled 3 · 67%" carries its own proof. Perturb either @@ -337,7 +350,7 @@ export const SalesDashboard: Dashboard = { // "this quarter" on a demo database is computed from whichever one or two // deals happen to have settled inside it, which is noise wearing the // costume of a KPI. Twelve months is the same window the row-2 tiles and - // the revenue trend use, so the dashboard tells one consistent story. + // Why We Lose use, so the dashboard tells one consistent story. { id: 'win_rate_by_owner', title: 'Win / Loss by Rep', @@ -374,35 +387,7 @@ export const SalesDashboard: Dashboard = { limit: 12, }, }, - { - // The loss-reason breakdown the fields were declared for and never got. - // - // `loss_reason` is mandatory on every `closed_lost` record (see - // crm_opportunity), so this chart has no "unattributed" slice to - // apologise for — an empty chart here means no losses in the window, - // never "nobody filled the field in". - // - // Counts, not amounts: the question a loss review asks is "how often did - // we lose for this reason", and one large lost deal should not outweigh - // five recurring feature gaps. `lost_amount` is declared on the dataset - // for the revenue view of the same question. - id: 'loss_reason_breakdown', - title: 'Why We Lose', - description: 'Lost deals by reason — last 12 months', - type: 'donut', - filter: { stage: 'closed_lost', close_date: { $gte: '{12_months_ago}' } }, - filterBindings: { dateRange: false }, - colorVariant: 'orange', - dataset: 'opportunity_metrics', dimensions: ['loss_reason'], values: ['opp_count'], - layout: { x: 0, y: 24, w: 12, h: 4 }, - chartConfig: { - showLegend: true, - showDataLabels: true, - colors: ['#EF4444', '#F59E0B', '#8B5CF6', '#06B6D4', '#4F46E5', '#10B981', '#64748B'], - }, - }, - - // ─── Row 9: Pivot — Stage × Lead Source ─────────────────────────── + // ─── Row 8: Pivot — Stage × Lead Source ─────────────────────────── { id: 'pipeline_stage_by_source', title: 'Pipeline by Stage × Lead Source', @@ -411,7 +396,7 @@ export const SalesDashboard: Dashboard = { filter: { stage: { $nin: ['closed_won', 'closed_lost'] } }, colorVariant: 'default', dataset: 'opportunity_metrics', dimensions: ['stage', 'lead_source'], values: ['total_amount'], - layout: { x: 0, y: 28, w: 12, h: 4 }, + layout: { x: 0, y: 24, w: 12, h: 4 }, }, ], }; diff --git a/src/sales/pages/home.page.ts b/src/sales/pages/home.page.ts index 4f08ea50c..85c060f9e 100644 --- a/src/sales/pages/home.page.ts +++ b/src/sales/pages/home.page.ts @@ -178,6 +178,11 @@ export const SalesHomePage: Page = { // date picker or a fixed window and show smaller numbers (#2291). // Open Leads is a current-state count on every page and needs no // window. + // + // The two amounts carry `format: '0,0'`, the dashboards' KPI face + // (#2370 row 7): whole units, with the tenant currency the console + // adds. Without it they read "1,220,000.00" beside the dashboards' + // "1,220,000" (docs/developers/number-formats.md). children: [ { type: 'object-metric', @@ -187,6 +192,7 @@ export const SalesHomePage: Page = { label: 'Total Revenue (All Time)', icon: 'dollar-sign', aggregate: { field: 'amount', function: 'sum' }, + format: '0,0', filter: [{ field: 'stage', operator: 'equals', value: 'closed_won' }], }, }, @@ -209,6 +215,7 @@ export const SalesHomePage: Page = { label: 'Open Pipeline (All Time)', icon: 'briefcase', aggregate: { field: 'amount', function: 'sum' }, + format: '0,0', filter: [{ field: 'stage', operator: 'not_in', value: ['closed_won', 'closed_lost'] }], }, }, diff --git a/src/sales/reports/lead.report.ts b/src/sales/reports/lead.report.ts index b9ea6494c..5198d1f1e 100644 --- a/src/sales/reports/lead.report.ts +++ b/src/sales/reports/lead.report.ts @@ -13,7 +13,7 @@ import type { Report } from '@objectstack/spec/ui'; */ export const LeadInflowByMonthSourceReport: Report = { name: 'lead_inflow_by_month_source', - label: 'Lead Engagement by Month × Source', + label: 'Lead Engagement by Month', description: 'Contacted-lead volume per month, broken down by acquisition channel', dataset: 'lead_metrics', rows: ['lead_source'], columns: ['last_contacted_date'], values: ['lead_count'], type: 'matrix', diff --git a/src/sales/reports/opportunity.report.ts b/src/sales/reports/opportunity.report.ts index 56bbeb815..360965117 100644 --- a/src/sales/reports/opportunity.report.ts +++ b/src/sales/reports/opportunity.report.ts @@ -34,7 +34,7 @@ export const WonOpportunitiesByOwnerReport: Report = { */ export const PipelineCoverageByQuarterReport: Report = { name: 'pipeline_coverage_by_quarter', - label: 'Pipeline Coverage by Forecast × Quarter', + label: 'Pipeline Coverage by Quarter', description: 'Open pipeline amount by forecast category, bucketed by close quarter', dataset: 'opportunity_metrics', rows: ['forecast_category'], columns: ['close_quarter'], values: ['total_amount', 'opp_count'], type: 'matrix', @@ -42,28 +42,32 @@ export const PipelineCoverageByQuarterReport: Report = { }; /** - * Sales-rep funnel — two-level summary that mirrors how reps drill into their - * own book of business. Exercises multi-level `rows` grouping (owner → stage) - * over the dataset's measures. + * Sales-rep funnel — one row per rep, one column per stage (#2370 row 4). + * + * It was a two-level `summary` (owner → stage), and the 18.2.0 report page + * drew that as a table repeating the owner's name on every stage row with no + * per-owner subtotal (the spec has no subtotal key), over a chart of amount by + * stage summed across every rep, so the "by owner" half was nowhere on the + * chart. As a `matrix` the table carries each rep once, the stages run across + * in the stage field's option order, and the row's Total IS the rep's + * subtotal, with a grand total beneath. One measure, the amount: the summary's + * deal count and average probability would put three columns under each of + * seven stages; Opportunities by Stage carries those. + * + * The chart is one bar per rep, so the report's question ("whose pipeline?") + * is on the chart too. A bar split by stage within each rep is not + * expressible in 18.2.0: a report chart takes one `xAxis` dimension and one + * `yAxis` measure, and a chart series is a measure, never a dimension's + * values. Horizontal bars for the reason #2073 item 10 gave: every rep name + * drawn whole. "Funnel" in the label is the sales word for the stage-by-stage + * pipeline, not the chart's shape. */ export const OpportunityFunnelByOwnerStageReport: Report = { name: 'opportunity_funnel_owner_stage', - label: 'Opportunity Funnel by Owner → Stage', + label: 'Opportunity Funnel by Owner', description: 'Per-rep stage-by-stage pipeline funnel', - dataset: 'opportunity_metrics', rows: ['owner', 'stage'], values: ['total_amount', 'opp_count', 'avg_probability'], - type: 'summary', + dataset: 'opportunity_metrics', rows: ['owner'], columns: ['stage'], values: ['total_amount'], + type: 'matrix', runtimeFilter: { stage: { $ne: 'closed_lost' } }, - // A horizontal bar chart of amount by stage, not a funnel (#2073 item 10, the - // remedy #2073 item 2 gave the Pipeline by Stage tile). A funnel draws each - // stage as wide as its value, so it reads as one only when the values shrink - // stage by stage, and these do not: measured on 18.0.0-next.5, the shipped - // seed gives 139,500 / 174,000 / 173,000 / 237,000 / 1,294,000 / 1,290,000 - // from Prospecting to Closed Won, the shape pinched and flared, and the - // Negotiation label was not drawn. Horizontal bars run top to bottom in the - // stage field's option order, though the dataset answers alphabetically, and - // draw every stage name whole; a vertical `bar` rotates them and cuts each to - // 12 characters ("Qualificati…" on Opportunities by Stage above). "Funnel" in - // the label, description and title is the sales word for the stage-by-stage - // pipeline, not the chart's shape, so it stays. - chart: { type: 'horizontal-bar', title: 'Pipeline Funnel', showLegend: false, xAxis: 'stage', yAxis: 'total_amount' }, + chart: { type: 'horizontal-bar', title: 'Amount by Owner', showLegend: false, xAxis: 'owner', yAxis: 'total_amount' }, }; diff --git a/src/sales/translations/en/app.ts b/src/sales/translations/en/app.ts index 514e61835..2b2ee6ca5 100644 --- a/src/sales/translations/en/app.ts +++ b/src/sales/translations/en/app.ts @@ -83,10 +83,9 @@ export const appSurface: Omit = { active_deals: { title: 'Open Deals (Selected Period)', description: 'Open opportunities closing in the selected period' }, won_deals: { title: 'Deals Won (Selected Period)', description: 'Closed-won deals in the selected period' }, avg_deal_size: { title: 'Avg Deal Size (Selected Period)', description: 'Average value of closed-won deals in the selected period' }, - revenue_trends: { title: 'Revenue Trends', description: 'Closed-won revenue over the last 12 months' }, lead_source: { title: 'Lead Source', description: 'Pipeline value by acquisition channel' }, pipeline_by_stage: { title: 'Pipeline by Stage', description: 'Open opportunity value at each sales stage' }, - top_products: { title: 'Top Products', description: 'Total list-price revenue by product category' }, + top_products: { title: 'List Price by Category', description: 'Catalog list prices summed per product category, not sales' }, pipeline_by_owner: { title: 'Pipeline by Owner', description: 'Open pipeline value and deal count per sales rep' }, }, }, @@ -114,11 +113,10 @@ export const appSurface: Omit = { open_opportunities: { title: 'Open Deals (Selected Period)', description: 'Open opportunities closing in the selected period' }, avg_deal_size: { title: 'Avg Deal Size (QTD)', description: 'Average value of closed-won deals this quarter' }, pipeline_by_stage: { title: 'Pipeline by Stage', description: 'Open opportunity value at each sales stage' }, - monthly_revenue_trend: { title: 'Monthly Revenue Trend', description: 'Closed-won revenue, last 12 months' }, pipeline_by_forecast_category: { title: 'Pipeline by Forecast Category', description: 'Open pipeline grouped by sales forecast category' }, lead_source_breakdown: { title: 'Lead Source', description: 'Where our pipeline is coming from' }, open_pipeline_by_owner: { title: 'Open Pipeline by Owner', description: 'In-flight pipeline value, deal count and avg win probability per rep' }, - quota_attainment_by_rep: { title: 'Quota Attainment by Rep', description: 'Current-quarter quota, closed revenue and attainment per rep, from forecast snapshots' }, + quota_attainment_by_rep: { title: 'Quota Attainment by Rep', description: 'Current-quarter quota, closed revenue and attainment per rep. Fills once the nightly forecast snapshot opens the quarter and a manager sets each quota' }, pipeline_stage_by_source: { title: 'Pipeline by Stage × Lead Source', description: 'Cross-tab of open opportunity amount by stage and source' }, win_rate_12m: { title: 'Win Rate (12M)', description: 'Deals won as a share of all deals settled in the last 12 months' }, won_deals_12m: { title: 'Deals Won (12M)', description: 'The numerator of the win rate' }, @@ -135,12 +133,12 @@ export const appSurface: Omit = { open_cases: { title: 'Open Cases', description: 'Cases that are not yet closed' }, critical_cases: { title: 'Critical Cases', description: 'Open cases marked as critical priority' }, avg_resolution_time: { title: 'Avg Resolution Time', description: 'Mean time to close, in hours' }, - sla_violations: { title: 'SLA Violations', description: 'Cases that breached their SLA' }, + sla_violations: { title: 'Open Cases Past SLA', description: 'Open cases whose SLA deadline has passed' }, cases_by_status: { title: 'Cases by Status', description: 'Workload distribution across the pipeline' }, cases_by_priority: { title: 'Cases by Priority', description: 'Open case mix by urgency' }, cases_by_origin: { title: 'Cases by Origin', description: 'Where our cases are coming from' }, daily_case_volume: { title: 'Daily Case Volume', description: 'New cases created over the last 30 days' }, - sla_compliance_gauge: { title: 'SLA Compliance', description: 'Percent of cases resolved within SLA this period' }, + sla_compliance_gauge: { title: 'SLA Met (Closed Cases)', description: 'Share of closed cases that met their SLA' }, kb_deflection_rate: { title: 'KB Deflection Rate', description: 'Share of closed cases resolved with a knowledge article' }, kb_resolved_cases: { title: 'Resolved by KB', description: 'Closed cases pointing at the article that resolved them' }, closed_cases_total: { title: 'Closed Cases', description: 'The denominator behind the deflection rate' }, diff --git a/src/sales/translations/es-ES/app.ts b/src/sales/translations/es-ES/app.ts index 36c6833c2..0f6a8213d 100644 --- a/src/sales/translations/es-ES/app.ts +++ b/src/sales/translations/es-ES/app.ts @@ -69,14 +69,14 @@ export const appSurface: Omit = { group_reports: { label: 'Informes' }, nav_report_opportunities_by_stage: { label: 'Oportunidades por Etapa' }, - nav_report_won_by_owner: { label: 'Ganadas por Propietario' }, - nav_report_pipeline_coverage: { label: 'Cobertura del Pipeline' }, + nav_report_won_by_owner: { label: 'Oportunidades Ganadas por Propietario' }, + nav_report_pipeline_coverage: { label: 'Cobertura del Pipeline por Trimestre' }, nav_report_funnel_by_owner: { label: 'Embudo por Propietario' }, - nav_report_lead_inflow: { label: 'Interacción con Prospectos' }, + nav_report_lead_inflow: { label: 'Interacción con Prospectos por Mes' }, nav_report_accounts_by_industry: { label: 'Cuentas por Industria y Tipo' }, nav_report_churn_signals: { label: 'Señales de Abandono' }, nav_report_cases_by_status: { label: 'Casos por Estado y Prioridad' }, - nav_report_sla: { label: 'Cumplimiento de SLA' }, + nav_report_sla: { label: 'SLA de Casos Cerrados' }, nav_report_cases_opened_by_day: { label: 'Casos Abiertos por Día' }, group_help: { label: 'Ayuda' }, @@ -138,10 +138,9 @@ export const appSurface: Omit = { active_deals: { title: 'Negocios abiertos (período seleccionado)', description: 'Oportunidades abiertas con cierre previsto en el período seleccionado' }, won_deals: { title: 'Negocios ganados (período seleccionado)', description: 'Negocios cerrados ganados en el período seleccionado' }, avg_deal_size: { title: 'Tamaño medio del negocio (período seleccionado)', description: 'Valor medio de los negocios cerrados ganados en el período seleccionado' }, - revenue_trends: { title: 'Tendencia de ingresos', description: 'Ingresos cerrados ganados de los últimos 12 meses' }, lead_source: { title: 'Origen del prospecto', description: 'Valor del pipeline por canal de adquisición' }, pipeline_by_stage: { title: 'Pipeline por etapa', description: 'Valor de oportunidades abiertas en cada etapa de venta' }, - top_products: { title: 'Productos principales', description: 'Ingresos a precio de lista por categoría de producto' }, + top_products: { title: 'Precio de lista por categoría', description: 'Suma de los precios de lista del catálogo por categoría de producto, no ventas' }, pipeline_by_owner: { title: 'Pipeline por responsable', description: 'Valor del pipeline abierto y número de negocios por comercial' }, }, }, @@ -169,11 +168,10 @@ export const appSurface: Omit = { open_opportunities: { title: 'Negocios abiertos (período seleccionado)', description: 'Oportunidades abiertas con cierre previsto en el período seleccionado' }, avg_deal_size: { title: 'Tamaño medio del negocio (trimestre)', description: 'Valor medio de los negocios cerrados ganados este trimestre' }, pipeline_by_stage: { title: 'Pipeline por etapa', description: 'Valor de oportunidades abiertas en cada etapa de venta' }, - monthly_revenue_trend: { title: 'Tendencia mensual de ingresos', description: 'Ingresos cerrados ganados de los últimos 12 meses' }, pipeline_by_forecast_category: { title: 'Pipeline por categoría de pronóstico', description: 'Pipeline abierto agrupado por categoría de pronóstico de ventas' }, lead_source_breakdown: { title: 'Origen del prospecto', description: 'De dónde proviene nuestro pipeline' }, open_pipeline_by_owner: { title: 'Pipeline abierto por responsable', description: 'Valor del pipeline en curso, número de negocios y probabilidad media de cierre por comercial' }, - quota_attainment_by_rep: { title: 'Cumplimiento de cuota por representante', description: 'Cuota del trimestre actual, ingresos cerrados y cumplimiento por representante, según instantáneas de pronóstico' }, + quota_attainment_by_rep: { title: 'Cumplimiento de cuota por representante', description: 'Cuota del trimestre actual, ingresos cerrados y cumplimiento por representante. Se llena cuando la instantánea nocturna de pronóstico abre el trimestre y un gerente fija cada cuota' }, pipeline_stage_by_source: { title: 'Pipeline por Etapa × Origen', description: 'Tabla cruzada del importe de oportunidades abiertas por etapa y origen' }, win_rate_12m: { title: 'Tasa de éxito (12M)', description: 'Negocios ganados como porcentaje de todos los negocios resueltos en los últimos 12 meses' }, won_deals_12m: { title: 'Negocios ganados (12M)', description: 'El numerador de la tasa de éxito' }, @@ -190,12 +188,12 @@ export const appSurface: Omit = { open_cases: { title: 'Casos abiertos', description: 'Casos que aún no se han cerrado' }, critical_cases: { title: 'Casos críticos', description: 'Casos abiertos marcados como prioridad crítica' }, avg_resolution_time: { title: 'Tiempo medio de resolución', description: 'Tiempo medio hasta el cierre, en horas' }, - sla_violations: { title: 'Incumplimientos de SLA', description: 'Casos que incumplieron su SLA' }, + sla_violations: { title: 'Casos abiertos fuera de SLA', description: 'Casos abiertos cuyo plazo de SLA ya venció' }, cases_by_status: { title: 'Casos por estado', description: 'Distribución de la carga a lo largo del pipeline' }, cases_by_priority: { title: 'Casos por prioridad', description: 'Mezcla de casos abiertos por urgencia' }, cases_by_origin: { title: 'Casos por origen', description: 'De dónde provienen nuestros casos' }, daily_case_volume: { title: 'Volumen diario de casos', description: 'Casos nuevos creados en los últimos 30 días' }, - sla_compliance_gauge: { title: 'Cumplimiento de SLA', description: 'Porcentaje de casos resueltos dentro del SLA en este período' }, + sla_compliance_gauge: { title: 'SLA cumplido (casos cerrados)', description: 'Proporción de casos cerrados que cumplieron su SLA' }, kb_deflection_rate: { title: 'Tasa de Desvío por KB', description: 'Proporción de casos cerrados resueltos con un artículo de la base de conocimiento' }, kb_resolved_cases: { title: 'Resueltos por KB', description: 'Casos cerrados que señalan el artículo que los resolvió' }, closed_cases_total: { title: 'Casos Cerrados', description: 'El denominador de la tasa de desvío' }, @@ -891,7 +889,7 @@ export const appSurface: Omit = { description: 'Cuatro paneles apilados de alerta temprana para la retención proactiva: cuentas que un CSM marcó en riesgo o en abandono, cuentas activas sin actividad reciente, cuentas de alto valor silenciosas y oportunidades perdidas recientemente.', }, lead_inflow_by_month_source: { - label: 'Interacción con Prospectos por Mes × Origen', + label: 'Interacción con Prospectos por Mes', description: 'Volumen mensual de prospectos contactados, desglosado por canal de adquisición', }, opportunities_by_stage: { @@ -899,11 +897,11 @@ export const appSurface: Omit = { description: 'Resumen de oportunidades agrupadas por etapa', }, opportunity_funnel_owner_stage: { - label: 'Embudo de Oportunidades por Propietario → Etapa', + label: 'Embudo por Propietario', description: 'Embudo del pipeline etapa por etapa para cada representante', }, pipeline_coverage_by_quarter: { - label: 'Cobertura del Pipeline por Previsión × Trimestre', + label: 'Cobertura del Pipeline por Trimestre', description: 'Importe del pipeline abierto por categoría de previsión, agrupado por trimestre de cierre', }, won_opportunities_by_owner: { @@ -915,11 +913,11 @@ export const appSurface: Omit = { description: 'Resumen de casos por estado y prioridad', }, sla_performance: { - label: 'Informe de Cumplimiento de SLA', - description: 'Análisis del cumplimiento de los SLA', + label: 'SLA de Casos Cerrados', + description: 'Tasa de incumplimiento de SLA y tiempo medio de resolución de los casos cerrados, por prioridad', }, cases_opened_by_day_priority: { - label: 'Casos Abiertos por Prioridad × Día', + label: 'Casos Abiertos por Día', description: 'Entrada diaria de casos desglosada por prioridad', }, }, diff --git a/src/sales/translations/ja-JP/app.ts b/src/sales/translations/ja-JP/app.ts index 0e1f14813..0f13b3e78 100644 --- a/src/sales/translations/ja-JP/app.ts +++ b/src/sales/translations/ja-JP/app.ts @@ -70,13 +70,13 @@ export const appSurface: Omit = { group_reports: { label: 'レポート' }, nav_report_opportunities_by_stage: { label: 'ステージ別商談' }, nav_report_won_by_owner: { label: '担当者別成立商談' }, - nav_report_pipeline_coverage: { label: 'パイプラインカバレッジ' }, + nav_report_pipeline_coverage: { label: '四半期別パイプラインカバレッジ' }, nav_report_funnel_by_owner: { label: '担当者別商談ファネル' }, - nav_report_lead_inflow: { label: 'リード対応' }, + nav_report_lead_inflow: { label: '月別リード対応' }, nav_report_accounts_by_industry: { label: '業種・種別別取引先' }, nav_report_churn_signals: { label: '顧客離反シグナル' }, nav_report_cases_by_status: { label: 'ステータス・優先度別ケース' }, - nav_report_sla: { label: 'SLA 実績' }, + nav_report_sla: { label: 'クローズ済みケースの SLA' }, nav_report_cases_opened_by_day: { label: '日別新規ケース' }, group_help: { label: 'ヘルプ' }, @@ -138,10 +138,9 @@ export const appSurface: Omit = { active_deals: { title: 'オープン商談(選択期間)', description: '選択期間内に完了予定のオープン商談' }, won_deals: { title: '成立商談数(選択期間)', description: '選択期間に成立した商談の数' }, avg_deal_size: { title: '平均商談規模(選択期間)', description: '選択期間の成立済み商談の平均金額' }, - revenue_trends: { title: '売上トレンド', description: '過去12か月の成立済み売上' }, lead_source: { title: 'リードソース', description: '獲得チャネル別のパイプライン金額' }, pipeline_by_stage: { title: 'ステージ別パイプライン', description: '各営業ステージのオープン商談金額' }, - top_products: { title: '売れ筋製品', description: '製品カテゴリ別の定価ベース売上' }, + top_products: { title: 'カテゴリ別定価合計', description: '製品カテゴリごとのカタログ定価の合計(売上ではありません)' }, pipeline_by_owner: { title: '担当者別パイプライン', description: '営業担当者ごとのオープンパイプライン金額と商談数' }, }, }, @@ -169,11 +168,10 @@ export const appSurface: Omit = { open_opportunities: { title: 'オープン商談(選択期間)', description: '選択期間内に完了予定のオープン商談' }, avg_deal_size: { title: '平均商談規模(今四半期)', description: '今四半期の成立済み商談の平均金額' }, pipeline_by_stage: { title: 'ステージ別パイプライン', description: '各営業ステージのオープン商談金額' }, - monthly_revenue_trend: { title: '月次売上トレンド', description: '過去12か月の成立済み売上' }, pipeline_by_forecast_category: { title: '予測カテゴリ別パイプライン', description: '売上予測カテゴリ別のオープンパイプライン金額' }, lead_source_breakdown: { title: 'リードソース', description: 'パイプラインの流入元' }, open_pipeline_by_owner: { title: '担当者別オープンパイプライン', description: '営業担当者ごとの進行中パイプライン金額・商談数・平均勝率' }, - quota_attainment_by_rep: { title: '担当者別ノルマ達成状況', description: '予測スナップショットに基づく担当者別の今四半期ノルマ・成約収益・達成率' }, + quota_attainment_by_rep: { title: '担当者別ノルマ達成状況', description: '担当者別の今四半期ノルマ・成約収益・達成率。夜間の予測スナップショットが今四半期の行を作成し、マネージャーがノルマを設定すると表示されます' }, pipeline_stage_by_source: { title: 'ステージ × リードソース', description: 'ステージとソース別の進行中商談金額のクロス集計' }, win_rate_12m: { title: '勝率(直近12か月)', description: '直近12か月に決着した商談のうち成立した割合' }, won_deals_12m: { title: '成立商談数(直近12か月)', description: '勝率の分子' }, @@ -190,12 +188,12 @@ export const appSurface: Omit = { open_cases: { title: 'オープンケース', description: 'まだクローズされていないケース' }, critical_cases: { title: '重大ケース', description: '優先度「重大」のオープンケース' }, avg_resolution_time: { title: '平均解決時間', description: 'クローズまでの平均時間(時)' }, - sla_violations: { title: 'SLA 違反', description: 'SLA を超過したケース' }, + sla_violations: { title: 'SLA 超過中の未解決ケース', description: 'SLA 期限を過ぎた未解決のケース' }, cases_by_status: { title: 'ステータス別ケース', description: 'パイプライン全体のワークロード分布' }, cases_by_priority: { title: '優先度別ケース', description: '緊急度別のオープンケース構成' }, cases_by_origin: { title: '発生源別ケース', description: 'ケースの流入チャネル' }, daily_case_volume: { title: '日次ケース件数', description: '過去30日間の新規ケース' }, - sla_compliance_gauge: { title: 'SLA 達成率', description: '当期 SLA 内に解決したケースの割合' }, + sla_compliance_gauge: { title: 'SLA 達成率(クローズ済みケース)', description: 'クローズ済みケースのうち SLA を満たした割合' }, kb_deflection_rate: { title: 'ナレッジ解決率', description: 'クローズしたケースのうちナレッジ記事で解決した割合' }, kb_resolved_cases: { title: 'ナレッジで解決', description: '解決した記事が紐づくクローズ済みケース' }, closed_cases_total: { title: 'クローズ済みケース', description: 'ナレッジ解決率の分母' }, @@ -893,7 +891,7 @@ export const appSurface: Omit = { description: '能動的な顧客維持のための 4 つの早期警告パネル:CSM が離反リスクありまたは離反中とフラグを付けた取引先、最近活動のない有効な取引先、動きのない高価値取引先、最近失注した商談。', }, lead_inflow_by_month_source: { - label: '月 × ソース別リード対応', + label: '月別リード対応', description: '月ごとのコンタクト済みリード数を獲得チャネル別に表示', }, opportunities_by_stage: { @@ -901,11 +899,11 @@ export const appSurface: Omit = { description: 'ステージ別に集計した商談のサマリー', }, opportunity_funnel_owner_stage: { - label: '担当者 → ステージ別商談ファネル', + label: '担当者別商談ファネル', description: '営業担当者ごとのステージ別パイプラインファネル', }, pipeline_coverage_by_quarter: { - label: 'フォーキャスト区分 × 四半期別パイプラインカバレッジ', + label: '四半期別パイプラインカバレッジ', description: '進行中のパイプライン金額をフォーキャスト区分別に、完了予定の四半期ごとに集計', }, won_opportunities_by_owner: { @@ -917,11 +915,11 @@ export const appSurface: Omit = { description: 'ステータスと優先度別のケースのサマリー', }, sla_performance: { - label: 'SLA 実績レポート', - description: 'SLA 遵守状況の分析', + label: 'クローズ済みケースの SLA', + description: 'クローズ済みケースの優先度別 SLA 違反率と平均解決時間', }, cases_opened_by_day_priority: { - label: '優先度 × 日別新規ケース', + label: '日別新規ケース', description: '1 日ごとの新規ケース数を優先度別に表示', }, }, diff --git a/src/sales/translations/zh-CN/app.ts b/src/sales/translations/zh-CN/app.ts index 5efbe255d..f2b41d128 100644 --- a/src/sales/translations/zh-CN/app.ts +++ b/src/sales/translations/zh-CN/app.ts @@ -75,13 +75,13 @@ export const appSurface: Omit = { group_reports: { label: '报表' }, nav_report_opportunities_by_stage: { label: '各阶段商机' }, nav_report_won_by_owner: { label: '各负责人成交商机' }, - nav_report_pipeline_coverage: { label: '管道覆盖率' }, + nav_report_pipeline_coverage: { label: '季度管道覆盖率' }, nav_report_funnel_by_owner: { label: '各负责人商机漏斗' }, - nav_report_lead_inflow: { label: '线索跟进' }, + nav_report_lead_inflow: { label: '每月线索跟进' }, nav_report_accounts_by_industry: { label: '客户行业与类型' }, nav_report_churn_signals: { label: '客户流失信号' }, nav_report_cases_by_status: { label: '工单状态与优先级' }, - nav_report_sla: { label: 'SLA 达成' }, + nav_report_sla: { label: '已关闭工单的 SLA' }, nav_report_cases_opened_by_day: { label: '每日新建工单' }, group_help: { label: '帮助' }, @@ -143,10 +143,9 @@ export const appSurface: Omit = { active_deals: { title: '进行中商机(所选期间)', description: '预计在所选期间内成交的进行中商机' }, won_deals: { title: '赢单数(所选期间)', description: '所选期间内赢单的商机数' }, avg_deal_size: { title: '平均订单金额(所选期间)', description: '所选期间内已成交商机的平均金额' }, - revenue_trends: { title: '收入趋势', description: '过去 12 个月的已成交收入' }, lead_source: { title: '线索来源', description: '按获客渠道统计的管道金额' }, pipeline_by_stage: { title: '阶段管道分布', description: '按阶段统计的进行中商机金额' }, - top_products: { title: '热门产品', description: '按产品类别统计的目录价收入' }, + top_products: { title: '各类别目录价合计', description: '按产品类别汇总的目录价,不是销售额' }, pipeline_by_owner: { title: '按负责人统计管道', description: '各销售负责人的进行中管道金额与商机数' }, }, }, @@ -174,11 +173,10 @@ export const appSurface: Omit = { open_opportunities: { title: '进行中商机(所选期间)', description: '预计在所选期间内成交的进行中商机' }, avg_deal_size: { title: '平均订单金额(本季度)', description: '本季度已成交商机的平均金额' }, pipeline_by_stage: { title: '阶段管道分布', description: '按阶段统计的进行中商机金额' }, - monthly_revenue_trend: { title: '月度收入趋势', description: '过去 12 个月的已成交收入' }, pipeline_by_forecast_category: { title: '预测类别管道分布', description: '按预测类别统计的进行中管道金额' }, lead_source_breakdown: { title: '线索来源分布', description: '按来源统计的线索数量' }, open_pipeline_by_owner: { title: '按负责人统计进行中管道', description: '各销售负责人的进行中管道金额、商机数与平均赢单概率' }, - quota_attainment_by_rep: { title: '按销售代表统计配额达成', description: '来自预测快照的各销售代表本季度配额、已成交收入与达成率' }, + quota_attainment_by_rep: { title: '按销售代表统计配额达成', description: '各销售代表本季度配额、已成交收入与达成率。每晚的预测快照建好本季度记录、经理填好配额后才有数据' }, pipeline_stage_by_source: { title: '阶段 × 线索来源', description: '按阶段和来源交叉统计进行中商机金额' }, win_rate_12m: { title: '赢率(近 12 个月)', description: '近 12 个月已结单商机中赢单所占的比例' }, won_deals_12m: { title: '赢单数(近 12 个月)', description: '赢率的分子' }, @@ -195,12 +193,12 @@ export const appSurface: Omit = { open_cases: { title: '未关闭工单', description: '尚未关闭的工单' }, critical_cases: { title: '严重工单', description: '标记为严重优先级的未关闭工单' }, avg_resolution_time: { title: '平均解决时长', description: '关闭工单的平均处理时长(小时)' }, - sla_violations: { title: 'SLA 违约', description: '已超出 SLA 的工单' }, + sla_violations: { title: '已超 SLA 的未结工单', description: 'SLA 截止时间已过、尚未解决的工单' }, cases_by_status: { title: '按状态分布', description: '工单在各处理阶段的分布' }, cases_by_priority: { title: '按优先级分布', description: '未关闭工单按紧急程度的分布' }, cases_by_origin: { title: '按来源分布', description: '工单的来源渠道' }, daily_case_volume: { title: '每日工单量', description: '过去 30 天的新建工单' }, - sla_compliance_gauge: { title: 'SLA 达成率', description: '本期 SLA 内解决工单的占比' }, + sla_compliance_gauge: { title: 'SLA 达成率(已关闭工单)', description: '已关闭工单中达成 SLA 的占比' }, kb_deflection_rate: { title: '知识库转移率', description: '已关闭工单中由知识文章解决的占比' }, kb_resolved_cases: { title: '知识库解决数', description: '已关联解决文章的已关闭工单' }, closed_cases_total: { title: '已关闭工单', description: '转移率的分母' }, @@ -898,7 +896,7 @@ export const appSurface: Omit = { description: '四个堆叠的预警面板,用于主动挽留客户:客户成功经理标记为有风险或正在流失的客户、近期没有活动的活跃客户、沉寂的高价值客户,以及最近丢单的商机。', }, lead_inflow_by_month_source: { - label: '按月份 × 来源统计线索跟进', + label: '每月线索跟进', description: '每月已联系线索的数量,按获客渠道细分', }, opportunities_by_stage: { @@ -906,11 +904,11 @@ export const appSurface: Omit = { description: '按阶段分组汇总商机', }, opportunity_funnel_owner_stage: { - label: '各负责人商机漏斗(按阶段)', + label: '各负责人商机漏斗', description: '每位销售代表逐阶段的管道漏斗', }, pipeline_coverage_by_quarter: { - label: '按预测类别 × 季度统计管道覆盖率', + label: '季度管道覆盖率', description: '按预测类别统计进行中的管道金额,并按预计成交季度分段', }, won_opportunities_by_owner: { @@ -922,11 +920,11 @@ export const appSurface: Omit = { description: '按状态和优先级汇总工单', }, sla_performance: { - label: 'SLA 达成报表', - description: 'SLA 达成情况分析', + label: '已关闭工单的 SLA', + description: '已关闭工单按优先级的 SLA 违约率与平均解决时长', }, cases_opened_by_day_priority: { - label: '按优先级 × 日期统计新建工单', + label: '每日新建工单', description: '每日新建工单数,按优先级细分', }, }, diff --git a/src/service/dashboards/service.dashboard.ts b/src/service/dashboards/service.dashboard.ts index b8f25435a..444d263e8 100644 --- a/src/service/dashboards/service.dashboard.ts +++ b/src/service/dashboards/service.dashboard.ts @@ -121,26 +121,33 @@ export const ServiceDashboard: Dashboard = { }, { id: 'sla_violations', - title: 'SLA Violations', - description: 'Cases that breached their SLA', + // The title states its base (#2370 row 1): this tile counts OPEN cases, + // the gauge below counts CLOSED ones, and the two sat side by side as + // "SLA Violations 4" and "SLA Compliance 100%" with neither saying so. + title: 'Open Cases Past SLA', + description: 'Open cases whose SLA deadline has passed', type: 'metric', // `dimension` names the window to shift: this filter also dates // `sla_due_date` (`{now}`, inside `$or`), so without it the console // would still ask for a comparison when the picker is at All time and // the executor would refuse it ("compareTo needs a dated window"). compareTo: { kind: 'previousPeriod', dimension: 'created_at' }, - // Counted at READ time (#2070): a case already flagged, OR a case still - // open whose deadline has passed. The second branch is the selection + // Counted at READ time (#2070), over OPEN cases only (#2370 row 1): a + // case still open that is already flagged, OR whose deadline has passed. + // The open set and the deadline branch are the selection // `case_sla_monitor` sweeps — same status set, same strict `$lt` at the // current instant (`{now}` is that instant in a query filter; `{NOW()}` // in a flow template) — so the tile reads true on a deployment that runs // no scheduled work, where nothing flags an open breach. It counts; it // does not act: escalation and the owner's alert still need the sweep. + // A case resolved or closed late is no longer counted here: it is a + // missed SLA on the gauge, whose base is closed cases. // `test/case-sla-breach-on-close.test.ts` pins the two to one selection. filter: { + status: { $nin: ['resolved', 'closed'] }, $or: [ { is_sla_violated: true }, - { status: { $nin: ['resolved', 'closed'] }, sla_due_date: { $lt: '{now}' } }, + { sla_due_date: { $lt: '{now}' } }, ], }, colorVariant: 'warning', @@ -230,8 +237,10 @@ export const ServiceDashboard: Dashboard = { }, { id: 'sla_compliance_gauge', - title: 'SLA Compliance', - description: 'Percent of cases resolved within SLA this period', + // Base in the title (#2370 row 1): closed cases, beside the open ones + // the Open Cases Past SLA tile counts. + title: 'SLA Met (Closed Cases)', + description: 'Share of closed cases that met their SLA', type: 'gauge', filter: { is_closed: true }, colorVariant: 'success', diff --git a/src/service/index.ts b/src/service/index.ts index d1aaf3ef0..9a173ebd8 100644 --- a/src/service/index.ts +++ b/src/service/index.ts @@ -271,7 +271,7 @@ export const createHotCrmServiceStack = ( priority: 30, items: [ { id: 'nav_report_cases_by_status', type: 'report', reportName: 'cases_by_status_priority', label: 'Cases by Status and Priority', icon: 'chart-column', visible: CASE_READ_AUDIENCE }, - { id: 'nav_report_sla', type: 'report', reportName: 'sla_performance', label: 'SLA Performance', icon: 'timer', visible: CASE_READ_AUDIENCE }, + { id: 'nav_report_sla', type: 'report', reportName: 'sla_performance', label: 'SLA on Closed Cases', icon: 'timer', visible: CASE_READ_AUDIENCE }, { id: 'nav_report_cases_opened_by_day', type: 'report', reportName: 'cases_opened_by_day_priority', label: 'Cases Opened by Day', icon: 'calendar-days', visible: CASE_READ_AUDIENCE }, ], }, diff --git a/src/service/reports/case.report.ts b/src/service/reports/case.report.ts index 887173f42..fb2e67910 100644 --- a/src/service/reports/case.report.ts +++ b/src/service/reports/case.report.ts @@ -16,8 +16,10 @@ export const CasesByStatusPriorityReport: Report = { export const SlaPerformanceReport: Report = { name: 'sla_performance', - label: 'SLA Performance Report', - description: 'Analysis of SLA compliance', + // Says its base (#2370 rows 1 and 6): closed cases only, as `runtimeFilter` + // below scopes it; the sidebar entry carries the same name. + label: 'SLA on Closed Cases', + description: 'SLA violation rate and average resolution time of closed cases, by priority', // 'avg_sla_violated' (not a duplicated 'case_count') — the table showed two // identical Cases columns and no violation measure; the chart's yAxis must // name a dataset MEASURE, not a raw field ('is_sla_violated' leaked the @@ -25,7 +27,7 @@ export const SlaPerformanceReport: Report = { dataset: 'case_metrics', rows: ['priority'], values: ['case_count', 'avg_sla_violated', 'avg_resolution'], type: 'summary', runtimeFilter: { is_closed: true }, - chart: { type: 'column', title: 'SLA Violations by Priority', showLegend: false, xAxis: 'priority', yAxis: 'avg_sla_violated' } + chart: { type: 'column', title: 'SLA Violation Rate by Priority', showLegend: false, xAxis: 'priority', yAxis: 'avg_sla_violated' } }; /** @@ -45,7 +47,7 @@ export const SlaPerformanceReport: Report = { */ export const CasesOpenedByDayPriorityReport: Report = { name: 'cases_opened_by_day_priority', - label: 'Cases Opened by Priority × Day', + label: 'Cases Opened by Day', description: 'Daily case inflow split by priority', dataset: 'case_metrics', rows: ['priority'], columns: ['created_at'], values: ['case_count'], type: 'matrix', diff --git a/test/app-navigation-shape.test.ts b/test/app-navigation-shape.test.ts index b72461888..5798bf0ed 100644 --- a/test/app-navigation-shape.test.ts +++ b/test/app-navigation-shape.test.ts @@ -9,7 +9,7 @@ import { MarketingUserProfile } from '../src/sales/profiles/marketing-user.profi import { CrmUserProfile } from '../src/sales/profiles/crm-user.profile'; import { CrmPositions } from '../src/sales/sharing/positions'; import { DemoOrgStaffing } from '../src/sales/sharing/demo-staffing'; -import composedStack, { servedApp } from './helpers/composed-stack'; +import composedStack, { servedApp, localePacks } from './helpers/composed-stack'; /** The sets a persona's position declares (`permissionSets`, #2200): what a holder of it is granted. */ const SETS: Record = Object.fromEntries( @@ -269,6 +269,29 @@ describe('every shipped report has one entry, under Reports, gated on reading wh expect(bad, `reports without exactly one nav entry:\n ${bad.join('\n ')}`).toEqual([]); }); + // One name per report (#2370 row 6): the sidebar said "Pipeline Coverage" + // and the report page "Pipeline Coverage by Forecast × Quarter". The entry's + // label is the report's own, in English and in every locale pack that + // carries either one. + it('each entry is labelled with its report\'s own name, in every locale', () => { + const bad: string[] = []; + for (const { n } of REPORT_ENTRIES) { + const report = REPORTS.find((r) => r.name === n.reportName); + if (n.label !== report?.label) bad.push(`en ${n.id}: "${n.label}" vs report "${report?.label}"`); + } + for (const [locale, pack] of localePacks) { + const nav = pack?.apps?.[(CrmApp as AnyRec).name]?.navigation ?? {}; + for (const { n } of REPORT_ENTRIES) { + const entry = nav[n.id]?.label; + const report = pack?.reports?.[n.reportName]?.label; + if ((entry !== undefined || report !== undefined) && entry !== report) { + bad.push(`${locale} ${n.id}: "${entry}" vs report "${report}"`); + } + } + } + expect(bad, `report entries named differently from their report:\n ${bad.join('\n ')}`).toEqual([]); + }); + it('every report entry is in the Reports group, which the app declares collapsed', () => { expect(REPORT_ENTRIES.filter(({ group }) => group !== 'group_reports').map(({ n }) => n.id)).toEqual([]); const declared = NAV.find((n) => n.id === 'group_reports'); diff --git a/test/case-sla-breach-on-close.test.ts b/test/case-sla-breach-on-close.test.ts index e7ef44ed0..3ef3e5ae1 100644 --- a/test/case-sla-breach-on-close.test.ts +++ b/test/case-sla-breach-on-close.test.ts @@ -35,10 +35,11 @@ import { hotcrmStack, signUpPerson, systemUpdate, runShippedHook, runScheduledFl * The truth table is spelled out longhand rather than recomputed from the * hook: the expected verdict of each row is what `caseSlaBreached` answers. * - * Section 4 is the parity pin for the rule's THIRD reader. The SLA Violations - * tile counts at read time — a flagged case, or an open case past its due date - * — so it reads true where no sweep runs (#2070, acceptance 3, ruled C). Its - * open-case branch is pinned to the selection `case_sla_monitor` makes, first + * Section 4 is the parity pin for the rule's THIRD reader. The Open Cases Past + * SLA tile (titled SLA Violations until #2370) counts at read time — an open + * case that is flagged or past its due date — so it reads true where no sweep + * runs (#2070, acceptance 3, ruled C). Its open set and deadline branch are + * pinned to the selection `case_sla_monitor` makes, first * as metadata and then by running the sweep over the cases the tile counted: * the sweep must flag exactly those, and the tile's number must not move. */ @@ -250,10 +251,13 @@ describe('the SLA surfaces count the late finishes, through the dataset query pa expect(Number(row!.sla_compliance_rate)).toBeCloseTo(1 / 3, 5); }); - it('the SLA Violations tile counts every late finish, resolved or closed', async () => { + it('the Open Cases Past SLA tile counts none of the late finishes: its base is open cases (#2370)', async () => { + // It counted all three until #2370 row 1, which put "SLA Violations 4" + // beside "SLA Compliance 100%" with neither title naming its base. The + // late closes are the gauge's to count, as missed SLAs over closed cases. const tile = widget('sla_violations'); const [row] = await query({ measures: tile.values, filter: tile.filter }); - expect(row!.case_count).toBe(3); + expect(Number(row?.case_count ?? 0)).toBe(0); }); }); @@ -280,24 +284,26 @@ describe('the sweep, the close-time stamp and the tile draw one boundary', () => expect(sweepSelection().sla_due_date).toEqual({ $lt: '{now}' }); }); - it('the SLA Violations tile counts the flag, or exactly the sweep\'s own selection', () => { + it('the Open Cases Past SLA tile counts the sweep\'s open set, flagged or past due', () => { // The sweep selects "open, past due, not yet flagged"; the tile counts - // "flagged, or open and past due" — the same selection with the flag - // clause folded into the other branch. Since 18.0.0-next.0 the two spell - // the instant the same way too: a flow filter operand refuses the - // `{NOW()}` template token, and the sweep reads the query engine's own - // `{now}` placeholder, which the tile reads. So the tile's second branch - // IS the sweep's selection, compared whole: a `$lte` on either side, a - // different status set, another spelling of the instant, or a third - // clause on the sweep turns this red. - const { is_sla_violated: unflagged, ...selection } = sweepSelection(); + // "open, and flagged or past due" — the same open set, with the flag + // clause beside the deadline one. Since 18.0.0-next.0 the two spell the + // instant the same way too: a flow filter operand refuses the `{NOW()}` + // template token, and the sweep reads the query engine's own `{now}` + // placeholder, which the tile reads. So the tile's status set and its + // deadline branch ARE the sweep's, compared whole: a `$lte` on either + // side, a different status set, another spelling of the instant, or a + // third clause on the sweep turns this red. Open only since #2370 row 1. + const { is_sla_violated: unflagged, status, sla_due_date, ...rest } = sweepSelection(); expect(unflagged, 'the sweep no longer selects unflagged cases only').toBe(false); - const operators = Object.keys(selection.sla_due_date ?? {}); + expect(rest, 'the sweep selects on a clause the tile does not carry').toEqual({}); + const operators = Object.keys(sla_due_date ?? {}); expect(operators, 'the sweep compares sla_due_date with something other than one operator').toHaveLength(1); expect(tileFilter()).toEqual({ + status, $or: [ { is_sla_violated: true }, - selection, + { sla_due_date }, ], }); }); @@ -305,7 +311,7 @@ describe('the sweep, the close-time stamp and the tile draw one boundary', () => // ──────────────── 5. the tile counts an open breach nothing flagged, and the sweep agrees ── -describe('the SLA Violations tile counts open breaches at read time, with no sweep', () => { +describe('the Open Cases Past SLA tile counts open breaches at read time, with no sweep', () => { /** * Cases in every state the tile has to tell apart, none of them flagged by * a sweep — nothing in this file has run one yet. Each row's verdict is diff --git a/test/pipeline-by-stage-chart.test.ts b/test/pipeline-by-stage-chart.test.ts index b0522f258..39821fd2c 100644 --- a/test/pipeline-by-stage-chart.test.ts +++ b/test/pipeline-by-stage-chart.test.ts @@ -74,32 +74,32 @@ describe('the Pipeline by Stage tile', () => { }); /** - * The same remedy on a report: the chart of **Opportunity Funnel by Owner → - * Stage** (#2073 item 10). + * The same remedy on a report: **Opportunity Funnel by Owner** (#2073 item 10), + * reshaped by #2370 row 4. * - * Its chart sums amount by stage over every stage but Closed Lost, so Closed Won - * is drawn too. Measured on 18.0.0-next.5, opened from its Reports entry on a - * fresh boot: 139,500 / 174,000 / 173,000 / 237,000 / 1,294,000 / 1,290,000 from - * Prospecting to Closed Won. As a funnel it pinched and flared (trapezoids 139 / - * 139 / 189 / 1034 / 1034 / 1031 px wide), and the Negotiation label was not - * drawn. As horizontal bars, all six stages are drawn top to bottom in the stage - * field's option order, each with its whole name, in en-US and zh-CN alike. - * - * The report keeps its label and description: "funnel" there is the sales word - * for the stage-by-stage pipeline, not the chart's shape. + * #2073 item 10 made its chart horizontal bars, because as a funnel the + * amounts by stage pinched and flared and the Negotiation label was not drawn + * (measured on 18.0.0-next.5). #2370 then made the report answer its own + * question: a matrix with each rep once down the side and the stages across + * (the row Total is the rep's subtotal), and one bar per rep on the chart. + * Measured on 18.2.0 from its Reports entry: three rep rows (Marta Quinn, + * Nina Reyes, Emil Roth), six stage columns from Prospecting to Closed Won in + * the stage field's option order, a Total column and a Total row + * (3,307,500), and three rep names drawn whole beside their bars. */ -describe('the Opportunity Funnel by Owner → Stage report', () => { +describe('the Opportunity Funnel by Owner report', () => { const reports: AnyRec[] = (stack as any).reports ?? []; const report = reports.find((r) => r.name === 'opportunity_funnel_owner_stage'); - it('charts amount by stage as a horizontal bar chart', () => { + it('is a rep × stage matrix of amount, charted as one horizontal bar per rep', () => { expect(report, 'no opportunity_funnel_owner_stage report registered').toBeTruthy(); - expect(report!.chart).toMatchObject({ type: 'horizontal-bar', xAxis: 'stage', yAxis: 'total_amount' }); + expect(report!).toMatchObject({ type: 'matrix', rows: ['owner'], columns: ['stage'], values: ['total_amount'] }); + expect(report!.chart).toMatchObject({ type: 'horizontal-bar', xAxis: 'owner', yAxis: 'total_amount' }); expect(report!.dataset).toBe('opportunity_metrics'); expect(report!.runtimeFilter).toEqual({ stage: { $ne: 'closed_lost' } }); }); - it('declares no order, the state the stage-order bars were measured in', () => { + it('declares no order, the state the stage-order columns were measured in', () => { expect(report!.order).toBeUndefined(); }); }); From 31bc1561fdfcc96a9107f8e70dba88ac4cd1bade Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 05:33:00 +0000 Subject: [PATCH 2/3] docs(#2370): dashboards and reports pages match the renamed tiles and reports Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01J8VVAoAy1McNK4cq14Q2vC --- .changeset/2370-dashboards-and-formats.md | 15 +++++++++++++ content/docs/analytics/dashboards.mdx | 18 +++++++-------- content/docs/analytics/dashboards.zh-Hans.mdx | 18 +++++++-------- content/docs/analytics/dashboards.zh-Hant.mdx | 18 +++++++-------- content/docs/analytics/index.mdx | 2 +- content/docs/analytics/index.zh-Hans.mdx | 2 +- content/docs/analytics/index.zh-Hant.mdx | 2 +- content/docs/analytics/reports.mdx | 22 +++++++++---------- content/docs/analytics/reports.zh-Hans.mdx | 22 +++++++++---------- content/docs/analytics/reports.zh-Hant.mdx | 22 +++++++++---------- content/docs/getting-started/quick-tour.mdx | 6 ++--- .../getting-started/quick-tour.zh-Hans.mdx | 6 ++--- .../getting-started/quick-tour.zh-Hant.mdx | 6 ++--- content/docs/marketing/index.mdx | 2 +- content/docs/marketing/index.zh-Hans.mdx | 2 +- content/docs/marketing/index.zh-Hant.mdx | 2 +- content/docs/sales/pipeline-management.mdx | 12 +++++----- .../sales/pipeline-management.zh-Hans.mdx | 12 +++++----- .../sales/pipeline-management.zh-Hant.mdx | 12 +++++----- content/docs/service/cases.mdx | 4 ++-- content/docs/service/cases.zh-Hans.mdx | 4 ++-- content/docs/service/cases.zh-Hant.mdx | 4 ++-- content/docs/service/index.mdx | 6 ++--- content/docs/service/index.zh-Hans.mdx | 6 ++--- content/docs/service/index.zh-Hant.mdx | 6 ++--- content/docs/service/sla-and-escalation.mdx | 14 ++++++------ .../service/sla-and-escalation.zh-Hans.mdx | 16 +++++++------- .../service/sla-and-escalation.zh-Hant.mdx | 16 +++++++------- docs/architecture/module-split-plan.md | 2 +- src/docs/crm_analytics_metrics.md | 6 ++--- src/docs/crm_analytics_metrics.zh.md | 6 ++--- src/revenue/index.ts | 2 +- src/sales/index.ts | 2 +- test/case-creation-date.test.ts | 4 ++-- test/case-sla-breach-on-close.test.ts | 2 +- test/docs-analytics-vocabulary.test.ts | 2 +- test/docs-quick-tour-navigation.test.ts | 3 ++- test/docs-service-index-analytics.test.ts | 8 +++---- test/line-item-conventions.test.ts | 13 +++++++++++ test/seed-sla-share.test.ts | 2 +- 40 files changed, 176 insertions(+), 153 deletions(-) create mode 100644 .changeset/2370-dashboards-and-formats.md diff --git a/.changeset/2370-dashboards-and-formats.md b/.changeset/2370-dashboards-and-formats.md new file mode 100644 index 000000000..d9c0e8afd --- /dev/null +++ b/.changeset/2370-dashboards-and-formats.md @@ -0,0 +1,15 @@ +--- +"hotcrm": minor +--- + +**Dashboards say what they count, and amounts, quantities and dates read one way (#2370).** + +- The two SLA tiles on Customer Service state their base. **Open Cases Past SLA** counts open cases only, so a case closed late leaves the count when it closes. **SLA Met (Closed Cases)** is the rate over closed cases. The SLA report is now **SLA on Closed Cases**. +- **Revenue Trend** appears once, on Executive Overview. CRM Overview and Sales Performance no longer repeat it. +- The CRM Overview tile that summed catalog list prices is now **List Price by Category**, with a description saying it is not sales. +- **Opportunity Funnel by Owner** is split by owner: one row per rep, one column per stage, a Total per rep, and a bar chart of amount by rep. +- **Quota Attainment by Rep** says when it fills: after the nightly forecast snapshot opens the quarter and a manager sets each quota. +- Each report's sidebar entry carries the report's own title, in every language the app ships. +- Sales Home's amount KPIs read in whole units, like the dashboards. Line item quantities are whole numbers, and a fractional quantity is refused. Quote dates and price book validity show the year and are never relative. A quote line's related list shows the discount instead of a second subtotal. + +The currency symbol on every amount comes from **Setup → Settings → Localization → Default currency**. See `docs/developers/number-formats.md`. diff --git a/content/docs/analytics/dashboards.mdx b/content/docs/analytics/dashboards.mdx index f1c59e6b4..3f189570f 100644 --- a/content/docs/analytics/dashboards.mdx +++ b/content/docs/analytics/dashboards.mdx @@ -37,10 +37,9 @@ The cross-functional home dashboard — revenue, pipeline and deal mix on one pa - **Open Deals (Selected Period)** — open opportunities whose close date falls in the selected period. - **Deals Won (Selected Period)** — closed-won deals in the selected period. - **Avg Deal Size (Selected Period)** — average value of the deals closed won in the selected period. -- **Revenue Trends** — closed-won revenue over the last 12 months (area chart). -- **Lead Source** — pipeline value by acquisition channel (donut). - **Pipeline by Stage** — open opportunity value at each sales stage (horizontal bar chart). -- **Top Products** — total list-price revenue by product category (bar). +- **Lead Source** — pipeline value by acquisition channel (donut). +- **List Price by Category** — catalog list prices summed per product category. It reads the price book, not sales: no tile on this page sums sold line items (bar). - **Pipeline by Owner** — open pipeline value, deal count and average deal size per rep (table). **Controls**: a date-range picker over the opportunity close date (defaults to this quarter) and an Owner filter. @@ -61,11 +60,10 @@ The sales team's daily operational dashboard. - **Deals Won (12M)** — the numerator behind that percentage. - **Deals Lost (12M)** — the other half of its denominator, so both numbers are always on screen next to the rate. - **Pipeline by Stage** — open opportunity value at each sales stage (horizontal bar chart). -- **Monthly Revenue Trend** — closed-won revenue, last 12 months. - **Pipeline by Forecast Category** — open pipeline grouped by forecast category. - **Lead Source** — where the pipeline is coming from (donut). - **Open Pipeline by Owner** — in-flight value, deal count and average win probability per rep (table). -- **Quota Attainment by Rep** — current-quarter quota, closed revenue and attainment per rep, read from the forecast snapshots. +- **Quota Attainment by Rep** — current-quarter quota, closed revenue and attainment per rep, read from the forecast snapshots. It stays empty until the nightly forecast snapshot has opened the current quarter and a manager has set each rep's quota; the demo data does not open one for you. - **Win / Loss by Rep** — won, lost, settled, win rate and won revenue per rep (table). - **Win / Loss by Lead Source** — the same breakdown by acquisition channel: which sources produce deals that actually close. - **Why We Lose** — lost deals by loss reason. Every closed-lost deal carries a reason (it is [required at close](/docs/sales/opportunities)), so this chart has no "unattributed" slice — an empty chart means no losses in the window, not an unfilled field. @@ -106,12 +104,12 @@ The service team's daily ops view. - **Open Cases** — cases that are not yet closed. - **Critical Cases** — open cases at critical priority. - **Avg Resolution Time** — mean time to close, in hours. -- **SLA Violations** — cases that breached their SLA. +- **Open Cases Past SLA** — open cases whose SLA deadline has passed. Its base is open cases only: a case closed late leaves this count when it closes. - **Cases by Status** — workload distribution across the pipeline (donut). - **Cases by Priority** — open case mix by urgency (pie). - **Cases by Origin** — Email / Phone / Web / Chat / Portal (bar). - **Daily Case Volume** — new cases created over the last 30 days. -- **SLA Compliance** — share of cases resolved within SLA, against a 95% target line (gauge). +- **SLA Met (Closed Cases)** — share of closed cases that met their SLA, against a 95% target line (gauge). Its base is closed cases only: an open case is in neither the numerator nor the denominator. - **My Open Cases by Priority** — the signed-in user's own open cases and their SLA-violation rate per priority (table). It ignores the Agent filter: it always shows your own cases. **Controls**: a date-range picker over the date a case was created (defaults to the last 90 days), Agent and Priority filters, and a one-minute refresh — service desks need fresh numbers. It is the only picker that windows a case date rather than the opportunity close date, and the only one that does not default to this quarter, so every number on this page is bounded by the last 90 days until you widen it. Daily Case Volume opts out of the picker and keeps its own 30-day window, so the chart under that title always covers the 30 days it names. @@ -126,7 +124,7 @@ The exec / finance view of the business. - **Active Accounts** — customers with at least one active relationship. - **Total Contacts** — people in the address book. - **Open Leads** — unconverted leads in the funnel. -- **Revenue Trend** — closed-won revenue over the last 12 months. +- **Revenue Trend** — closed-won revenue over the last 12 months. This is the only dashboard that carries the trend, so it has one home. - **Revenue by Industry** — YTD closed-won revenue split by customer industry (donut). - **Pipeline by Stage** — open opportunity value at each sales stage (horizontal bar chart). - **New Accounts** — account creation cadence over the last 6 months. @@ -144,7 +142,7 @@ This dashboard is the basis for the weekly leadership review. A dashboard filter is matched against the **object** behind each widget's dataset, not against that dataset's declared dimensions, and it is ANDed into the query of every widget bound to it. That is why Customer Service can offer an Agent filter even though `case_metrics` declares no owner dimension: `owner_id` is a field on the case, and the filter needs nothing more than that. Filtering is all it buys — a widget still cannot **group by** a field the dataset does not declare, which is how [SLA & Escalation](/docs/service/sla-and-escalation) can say, correctly, that no report and no dashboard widget breaks cases down by agent, on the same app whose service desk filters by one. It cuts the other way too: a filter naming a field a widget's own object lacks would ask that widget's query for a column that does not exist, so a widget that cannot answer a filter opts out of it by name (`filterBindings`) instead of quietly ignoring it. -Some tiles opt out of the dashboard's own filters on purpose. The 12-month revenue trends, Top Products and Quota Attainment by Rep keep the window they are labelled with, so a narrower date picker cannot silently redefine the number under its own title. +Some tiles opt out of the dashboard's own filters on purpose. Revenue Trend, List Price by Category and Quota Attainment by Rep keep the window they are labelled with, so a narrower date picker cannot silently redefine the number under its own title. ## Where the numbers come from @@ -158,5 +156,5 @@ The window is not shared, and each KPI title says which one it reads. *(Selected - ✅ Before a pipeline review, read **Win / Loss by Rep** next to **Why We Lose** — the first says who is converting, the second says what is beating you. - ✅ Work the **Quiet 90+ Days** tile weekly. An account with no logged interaction in a quarter is either dormant or already gone, and the tile is only as good as the interactions your team logs. -- ✅ On the service desk, watch **SLA Violations** (the count of breaches) alongside the **SLA Compliance** tile (the rate against target) — a small count on a small volume is not the same as a healthy rate. +- ✅ On the service desk, read the two SLA tiles as two different populations: **Open Cases Past SLA** counts open cases that are already late (work to do now), while **SLA Met (Closed Cases)** is the rate over cases already closed (how the desk did). A late case moves from the first to the second when it closes. - ✅ Read **Interactions on Deals** against **Open Deals**: the ratio, not either number alone, is the coverage question. diff --git a/content/docs/analytics/dashboards.zh-Hans.mdx b/content/docs/analytics/dashboards.zh-Hans.mdx index 76ce4badf..57f5a3f4d 100644 --- a/content/docs/analytics/dashboards.zh-Hans.mdx +++ b/content/docs/analytics/dashboards.zh-Hans.mdx @@ -37,10 +37,9 @@ HotCRM 内置五个仪表盘。每个都为特定角色而构建,回答该角 - **Open Deals (Selected Period)** — 预计成交日期落在所选周期内的进行中商机。 - **Deals Won (Selected Period)** — 所选周期内成交赢单的交易。 - **Avg Deal Size (Selected Period)** — 所选周期内成交赢单交易的平均金额。 -- **Revenue Trends** — 近 12 个月的成交赢单收入(面积图)。 -- **Lead Source** — 按获客渠道统计的管道金额(环形图)。 - **Pipeline by Stage** — 各销售阶段的进行中商机金额(条形图)。 -- **Top Products** — 按产品类别统计的标价收入合计(柱状图)。 +- **Lead Source** — 按获客渠道统计的管道金额(环形图)。 +- **List Price by Category** — 按产品类别合计的目录标价。它读的是价格表,不是销售:本页没有任何磁贴汇总已售出的明细行(柱状图)。 - **Pipeline by Owner** — 每位销售代表的进行中管道金额、交易数和平均交易规模(表格)。 **控件**:一个基于商机预计成交日期的日期范围选择器(默认本季度),以及一个 Owner 筛选器。 @@ -61,11 +60,10 @@ HotCRM 内置五个仪表盘。每个都为特定角色而构建,回答该角 - **Deals Won (12M)** — 这个百分比背后的分子。 - **Deals Lost (12M)** — 分母的另一半,好让分子和分母始终陈列在赢率旁边。 - **Pipeline by Stage** — 各销售阶段的进行中商机金额(条形图)。 -- **Monthly Revenue Trend** — 近 12 个月的成交赢单收入。 - **Pipeline by Forecast Category** — 按预测类别分组的进行中管道。 - **Lead Source** — 管道是从哪里来的(环形图)。 - **Open Pipeline by Owner** — 每位销售代表进行中的金额、交易数和平均赢单概率(表格)。 -- **Quota Attainment by Rep** — 每位销售代表当季的配额、已成交收入与达成率,读取自预测快照。 +- **Quota Attainment by Rep** — 每位销售代表当季的配额、已成交收入与达成率,读取自预测快照。要等夜间预测快照开出当前季度、并由经理为每位代表设好配额之后它才有数据;演示数据不会替你开出当前季度。 - **Win / Loss by Rep** — 每位销售代表的赢单数、丢单数、已结案数、赢率和赢单金额(表格)。 - **Win / Loss by Lead Source** — 同一份拆分换成按获客渠道来看:哪些来源带来的商机真正能成交。 - **Why We Lose** — 按丢单原因统计的丢单数。每条丢单商机都必须填写原因([关单时强制](/zh-Hans/docs/sales/opportunities)),因此图中不存在"未归因"切片——图为空表示该时间窗内没有丢单,而不是没人填字段。 @@ -106,12 +104,12 @@ HotCRM 内置五个仪表盘。每个都为特定角色而构建,回答该角 - **Open Cases** — 尚未关闭的工单。 - **Critical Cases** — 优先级为 critical 的未结工单。 - **Avg Resolution Time** — 平均关单时长(小时)。 -- **SLA Violations** — 违反了 SLA 的工单。 +- **Open Cases Past SLA** — SLA 截止时间已过的未结工单。它的基数只有未结工单:逾期结案的工单在结案时就离开这个计数。 - **Cases by Status** — 工作量在流程各状态上的分布(环形图)。 - **Cases by Priority** — 未结工单按紧急程度的构成(饼图)。 - **Cases by Origin** — Email / Phone / Web / Chat / Portal(柱状图)。 - **Daily Case Volume** — 近 30 天每天新建的工单数。 -- **SLA Compliance** — 在 SLA 内解决的工单占比,对比 95% 的目标线(仪表图)。 +- **SLA Met (Closed Cases)** — 已结案工单中满足 SLA 的占比,对比 95% 的目标线(仪表图)。它的基数只有已结案工单:未结工单既不在分子也不在分母里。 - **My Open Cases by Priority** — 当前登录用户自己名下、每个优先级的未结工单数及其 SLA 违约率(表格)。它不受 Agent 筛选影响,始终显示你自己的工单。 **控件**:一个基于工单创建日期的日期范围选择器(默认最近 90 天),外加 Agent 和 Priority 筛选器,以及每分钟一次的刷新——服务台需要新鲜的数字。它是唯一一个按工单日期、而不是按商机预计成交日期取窗的选择器,也是唯一一个不默认本季度的,所以在你把窗口放宽之前,这一页上的每一个数字都被最近 90 天限定着。Daily Case Volume 退出了这个选择器,保留自带的 30 天时间窗,因此那张图始终覆盖它标题上写的 30 天。 @@ -126,7 +124,7 @@ HotCRM 内置五个仪表盘。每个都为特定角色而构建,回答该角 - **Active Accounts** — 至少还有一段有效关系的客户。 - **Total Contacts** — 通讯录里的人。 - **Open Leads** — 漏斗中尚未转化的线索。 -- **Revenue Trend** — 近 12 个月的成交赢单收入。 +- **Revenue Trend** — 近 12 个月的成交赢单收入。只有这个仪表盘放了收入趋势,它只有这一个位置。 - **Revenue by Industry** — 年初至今的成交赢单收入按客户行业拆分(环形图)。 - **Pipeline by Stage** — 各销售阶段的进行中商机金额(条形图)。 - **New Accounts** — 近 6 个月的客户新建节奏。 @@ -144,7 +142,7 @@ HotCRM 内置五个仪表盘。每个都为特定角色而构建,回答该角 仪表盘的筛选器匹配的是每个部件所绑数据集背后的那个**对象**,而不是该数据集声明的维度,并且它会被 AND 进每个受它约束的部件的查询里。所以 Customer Service 能提供 Agent 筛选器,哪怕 `case_metrics` 没有声明任何负责人维度:`owner_id` 是工单上的字段,筛选器要的就只是这个。但它买到的只有筛选这一件事——部件依然不能按数据集没有声明的字段**分组**,所以 [SLA 与升级](/zh-Hans/docs/service/sla-and-escalation) 说"没有任何报表、也没有任何仪表盘部件能按它分组"是对的,它和同一个应用里按 Agent 筛选的服务台仪表盘并不矛盾。反过来同样成立:筛选器若点名了某个部件自己的对象没有的字段,那个部件的查询就会去要一个并不存在的列,所以答不了某个筛选器的部件会按名字显式退出它(`filterBindings`),而不是默默忽略它。 -有些磁贴刻意不接受所在仪表盘的筛选器。12 个月的收入趋势、Top Products 和 Quota Attainment by Rep 都保持自己标题上写明的那个时间窗,这样更窄的日期选择器就无法在标题不变的情况下悄悄改掉数字的定义。 +有些磁贴刻意不接受所在仪表盘的筛选器。Revenue Trend、List Price by Category 和 Quota Attainment by Rep 都保持自己标题上写明的那个时间窗,这样更窄的日期选择器就无法在标题不变的情况下悄悄改掉数字的定义。 ## 数字是从哪里来的 @@ -158,5 +156,5 @@ HotCRM 内置五个仪表盘。每个都为特定角色而构建,回答该角 - ✅ 管道评审之前,把 **Win / Loss by Rep** 和 **Why We Lose** 放在一起读——前者说谁在转化,后者说是什么在打败你。 - ✅ 每周都处理一次 **Quiet 90+ Days** 磁贴。一个季度都没有任何已记录互动的客户,要么已经休眠,要么早就走了;而这个磁贴的价值上限,就是你的团队真正记录下来的互动量。 -- ✅ 在服务台,把 **SLA Violations**(违约的绝对条数)和 **SLA Compliance** 磁贴(对比目标的达标率)一起看——小基数上的一个小数字,和一个健康的达标率不是一回事。 +- ✅ 在服务台,把两个 SLA 磁贴当作两个不同的群体来读:**Open Cases Past SLA** 数的是已经逾期的未结工单(现在要处理的活),**SLA Met (Closed Cases)** 是已结案工单上的达标率(服务台做得怎样)。一张逾期工单结案时,就从前者移到后者。 - ✅ 把 **Interactions on Deals** 对着 **Open Deals** 读:覆盖度问题的答案是这两者的比值,而不是其中任何一个单独的数字。 diff --git a/content/docs/analytics/dashboards.zh-Hant.mdx b/content/docs/analytics/dashboards.zh-Hant.mdx index ae0d2eb0b..76af9ed20 100644 --- a/content/docs/analytics/dashboards.zh-Hant.mdx +++ b/content/docs/analytics/dashboards.zh-Hant.mdx @@ -39,10 +39,9 @@ HotCRM 內建五個儀表板。每個都為特定角色而建構,回答該角 - **Open Deals (Selected Period)** — 預計成交日期落在所選週期內的進行中商機。 - **Deals Won (Selected Period)** — 所選週期內成交贏單的交易。 - **Avg Deal Size (Selected Period)** — 所選週期內成交贏單交易的平均金額。 -- **Revenue Trends** — 近 12 個月的成交贏單收入(面積圖)。 -- **Lead Source** — 按獲客通路統計的管道金額(環圈圖)。 - **Pipeline by Stage** — 各銷售階段的進行中商機金額(橫條圖)。 -- **Top Products** — 按產品類別統計的標價收入合計(長條圖)。 +- **Lead Source** — 按獲客通路統計的管道金額(環圈圖)。 +- **List Price by Category** — 按產品類別合計的目錄標價。它讀的是價目表,不是銷售:本頁沒有任何磁貼彙總已售出的明細列(長條圖)。 - **Pipeline by Owner** — 每位銷售代表的進行中管道金額、交易數和平均交易規模(表格)。 **控制項**:一個以商機預計成交日期為準的日期範圍選擇器(預設本季),以及一個 Owner 篩選器。 @@ -63,11 +62,10 @@ HotCRM 內建五個儀表板。每個都為特定角色而建構,回答該角 - **Deals Won (12M)** — 這個百分比背後的分子。 - **Deals Lost (12M)** — 分母的另一半,好讓分子和分母始終陳列在贏率旁邊。 - **Pipeline by Stage** — 各銷售階段的進行中商機金額(橫條圖)。 -- **Monthly Revenue Trend** — 近 12 個月的成交贏單收入。 - **Pipeline by Forecast Category** — 按預測類別分組的進行中管道。 - **Lead Source** — 管道是從哪裡來的(環圈圖)。 - **Open Pipeline by Owner** — 每位銷售代表進行中的金額、交易數和平均贏單機率(表格)。 -- **Quota Attainment by Rep** — 每位銷售代表當季的配額、已成交收入與達成率,讀取自預測快照。 +- **Quota Attainment by Rep** — 每位銷售代表當季的配額、已成交收入與達成率,讀取自預測快照。要等夜間預測快照開出當季、並由經理為每位代表設好配額之後它才有資料;示範資料不會替你開出當季。 - **Win / Loss by Rep** — 每位銷售代表的贏單數、丟單數、已結案數、贏率和贏單金額(表格)。 - **Win / Loss by Lead Source** — 同一份拆分換成按獲客通路來看:哪些來源帶來的商機真正能成交。 - **Why We Lose** — 按丟單原因統計的丟單數。每筆丟單商機都必須填寫原因([結案時強制](/zh-Hant/docs/sales/opportunities)),因此圖中不存在"未歸因"切片——圖為空表示該時間窗內沒有丟單,而不是沒人填欄位。 @@ -108,12 +106,12 @@ HotCRM 內建五個儀表板。每個都為特定角色而建構,回答該角 - **Open Cases** — 尚未結案的工單。 - **Critical Cases** — 優先順序為 critical 的未結工單。 - **Avg Resolution Time** — 平均結案時長(小時)。 -- **SLA Violations** — 違反了 SLA 的工單。 +- **Open Cases Past SLA** — SLA 截止時間已過的未結工單。它的基數只有未結工單:逾期結案的工單在結案時就離開這個計數。 - **Cases by Status** — 工作量在流程各狀態上的分布(環圈圖)。 - **Cases by Priority** — 未結工單按緊急程度的組成(圓餅圖)。 - **Cases by Origin** — Email / Phone / Web / Chat / Portal(長條圖)。 - **Daily Case Volume** — 近 30 天每天新建的工單數。 -- **SLA Compliance** — 在 SLA 內解決的工單占比,對比 95% 的目標線(量表圖)。 +- **SLA Met (Closed Cases)** — 已結案工單中符合 SLA 的占比,對比 95% 的目標線(量表圖)。它的基數只有已結案工單:未結工單既不在分子也不在分母裡。 - **My Open Cases by Priority** — 目前登入使用者自己名下、每個優先順序的未結工單數及其 SLA 違約率(表格)。它不受 Agent 篩選影響,一律顯示你自己的工單。 **控制項**:一個以工單創建日期為準的日期範圍選擇器(預設最近 90 天),外加 Agent 和 Priority 篩選器,以及每分鐘一次的重新整理——服務台需要新鮮的數字。它是唯一一個以工單日期、而不是以商機預計成交日期取窗的選擇器,也是唯一一個不預設本季的,所以在你把時間窗放寬之前,這一頁上的每一個數字都被最近 90 天限定著。Daily Case Volume 退出了這個選擇器,保留自帶的 30 天時間窗,因此那張圖始終涵蓋它標題上寫的 30 天。 @@ -128,7 +126,7 @@ HotCRM 內建五個儀表板。每個都為特定角色而建構,回答該角 - **Active Accounts** — 至少還有一段有效關係的客戶。 - **Total Contacts** — 通訊錄裡的人。 - **Open Leads** — 漏斗中尚未轉化的潛在客戶。 -- **Revenue Trend** — 近 12 個月的成交贏單收入。 +- **Revenue Trend** — 近 12 個月的成交贏單收入。只有這個儀表板放了收入趨勢,它只有這一個位置。 - **Revenue by Industry** — 年初至今的成交贏單收入按客戶產業拆分(環圈圖)。 - **Pipeline by Stage** — 各銷售階段的進行中商機金額(橫條圖)。 - **New Accounts** — 近 6 個月的客戶新建節奏。 @@ -146,7 +144,7 @@ HotCRM 內建五個儀表板。每個都為特定角色而建構,回答該角 儀表板的篩選器比對的是每個部件所綁資料集背後的那個**物件**,而不是該資料集宣告的維度,並且它會被 AND 進每個受它約束的部件的查詢裡。所以 Customer Service 能提供 Agent 篩選器,哪怕 `case_metrics` 沒有宣告任何負責人維度:`owner_id` 是工單上的欄位,篩選器要的就只是這個。但它買到的只有篩選這一件事——部件依然不能按資料集沒有宣告的欄位**分組**,所以 [SLA 與升級](/zh-Hant/docs/service/sla-and-escalation) 說"沒有任何報表、也沒有任何儀表板部件能按它分組"是對的,它和同一個應用裡按 Agent 篩選的服務台儀表板並不矛盾。反過來同樣成立:篩選器若點名了某個部件自己的物件沒有的欄位,那個部件的查詢就會去要一個並不存在的欄位,所以答不了某個篩選器的部件會按名字顯式退出它(`filterBindings`),而不是默默忽略它。 -有些磁貼刻意不接受所在儀表板的篩選器。12 個月的收入趨勢、Top Products 和 Quota Attainment by Rep 都保持自己標題上寫明的那個時間窗,這樣更窄的日期選擇器就無法在標題不變的情況下悄悄改掉數字的定義。 +有些磁貼刻意不接受所在儀表板的篩選器。Revenue Trend、List Price by Category 和 Quota Attainment by Rep 都保持自己標題上寫明的那個時間窗,這樣更窄的日期選擇器就無法在標題不變的情況下悄悄改掉數字的定義。 ## 數字是從哪裡來的 @@ -160,5 +158,5 @@ HotCRM 內建五個儀表板。每個都為特定角色而建構,回答該角 - ✅ 管道評審之前,把 **Win / Loss by Rep** 和 **Why We Lose** 放在一起讀——前者說誰在轉化,後者說是什麼在打敗你。 - ✅ 每週都處理一次 **Quiet 90+ Days** 磁貼。一季都沒有任何已記錄互動的客戶,要麼已經休眠,要麼早就走了;而這個磁貼的價值上限,就是你的團隊真正記錄下來的互動量。 -- ✅ 在服務台,把 **SLA Violations**(違約的絕對筆數)和 **SLA Compliance** 磁貼(對比目標的達標率)一起看——小基數上的一個小數字,和一個健康的達標率不是一回事。 +- ✅ 在服務台,把兩個 SLA 磁貼當作兩個不同的群體來讀:**Open Cases Past SLA** 數的是已經逾期的未結工單(現在要處理的工作),**SLA Met (Closed Cases)** 是已結案工單上的達標率(服務台做得怎樣)。一張逾期工單結案時,就從前者移到後者。 - ✅ 把 **Interactions on Deals** 對著 **Open Deals** 讀:覆蓋度問題的答案是這兩者的比值,而不是其中任何一個單獨的數字。 diff --git a/content/docs/analytics/index.mdx b/content/docs/analytics/index.mdx index d1b05c697..6e3e110bb 100644 --- a/content/docs/analytics/index.mdx +++ b/content/docs/analytics/index.mdx @@ -48,7 +48,7 @@ This means **one dashboard** works for the whole org — what each person sees d In the HotCRM app, analytics lives in two groups. **Insights** opens itself when the app loads; **Reports** is collapsed: - **Insights** — **CRM Overview**, the cross-functional dashboard, and **Forecasts**, the forecast records themselves. -- **Reports** — every report the app ships, one entry each: **Opportunities by Stage**, **Won Opportunities by Owner**, **Pipeline Coverage**, **Opportunity Funnel by Owner**, **Lead Engagement**, **Accounts by Industry & Type**, **Customer Churn Signals**, **Cases by Status and Priority**, **SLA Performance** and **Cases Opened by Day**. A few entries carry a shorter name than the report's own title; [Reports](/docs/analytics/reports) uses the titles. +- **Reports** — every report the app ships, one entry each: **Opportunities by Stage**, **Won Opportunities by Owner**, **Pipeline Coverage by Quarter**, **Opportunity Funnel by Owner**, **Lead Engagement by Month**, **Accounts by Industry and Type**, **Customer Churn Signals**, **Cases by Status and Priority**, **SLA on Closed Cases** and **Cases Opened by Day**. Each entry carries its report's own title; see [Reports](/docs/analytics/reports). You see the entry for a report when you can read the records it counts. A service agent, who reads no opportunities, gets the lead, account and case reports; a user who reads no cases does not get the three case reports. diff --git a/content/docs/analytics/index.zh-Hans.mdx b/content/docs/analytics/index.zh-Hans.mdx index 5f1bbc3f0..9982a0396 100644 --- a/content/docs/analytics/index.zh-Hans.mdx +++ b/content/docs/analytics/index.zh-Hans.mdx @@ -48,7 +48,7 @@ HotCRM 分析将你的 CRM 活动转化为**决策**。三个层次协同工作 在 HotCRM 应用里,分析内容分在两个分组下。**Insights** 在应用加载时自动展开,**Reports** 是折叠的: - **Insights** —— 跨职能的那块仪表盘 **CRM Overview**,以及预测记录本身 **Forecasts**。 -- **Reports** —— 应用自带的每一张报表,各占一个条目:**Opportunities by Stage**、**Won Opportunities by Owner**、**Pipeline Coverage**、**Opportunity Funnel by Owner**、**Lead Engagement**、**Accounts by Industry & Type**、**Customer Churn Signals**、**Cases by Status and Priority**、**SLA Performance**、**Cases Opened by Day**。有几个条目的名字比报表自身的标题短;[报表](/zh-Hans/docs/analytics/reports) 一页用的是标题。 +- **Reports** —— 应用自带的每一张报表,各占一个条目:**Opportunities by Stage**、**Won Opportunities by Owner**、**Pipeline Coverage by Quarter**、**Opportunity Funnel by Owner**、**Lead Engagement by Month**、**Accounts by Industry and Type**、**Customer Churn Signals**、**Cases by Status and Priority**、**SLA on Closed Cases**、**Cases Opened by Day**。每个条目都用报表自身的标题;见 [报表](/zh-Hans/docs/analytics/reports)。 能读取一张报表统计的记录,才看得到它的条目。服务坐席读不了商机,看到的是线索、客户与工单报表;读不了工单的人看不到那三张工单报表。 diff --git a/content/docs/analytics/index.zh-Hant.mdx b/content/docs/analytics/index.zh-Hant.mdx index fc9f91d54..38362cb6c 100644 --- a/content/docs/analytics/index.zh-Hant.mdx +++ b/content/docs/analytics/index.zh-Hant.mdx @@ -50,7 +50,7 @@ HotCRM 分析將你的 CRM 活動轉化為**決策**。三個層次協同運作 在 HotCRM 應用裡,分析內容分在兩個群組下。**Insights** 在應用載入時自動展開,**Reports** 是摺疊的: - **Insights** —— 跨職能的那塊儀表板 **CRM Overview**,以及預測記錄本身 **Forecasts**。 -- **Reports** —— 應用自帶的每一張報表,各佔一個條目:**Opportunities by Stage**、**Won Opportunities by Owner**、**Pipeline Coverage**、**Opportunity Funnel by Owner**、**Lead Engagement**、**Accounts by Industry & Type**、**Customer Churn Signals**、**Cases by Status and Priority**、**SLA Performance**、**Cases Opened by Day**。有幾個條目的名字比報表自身的標題短;[報表](/zh-Hant/docs/analytics/reports) 一頁用的是標題。 +- **Reports** —— 應用自帶的每一張報表,各佔一個條目:**Opportunities by Stage**、**Won Opportunities by Owner**、**Pipeline Coverage by Quarter**、**Opportunity Funnel by Owner**、**Lead Engagement by Month**、**Accounts by Industry and Type**、**Customer Churn Signals**、**Cases by Status and Priority**、**SLA on Closed Cases**、**Cases Opened by Day**。每個條目都用報表自身的標題;見 [報表](/zh-Hant/docs/analytics/reports)。 能讀取一張報表統計的記錄,才看得到它的條目。服務專員讀不了商機,看到的是線索、客戶與工單報表;讀不了工單的人看不到那三張工單報表。 diff --git a/content/docs/analytics/reports.mdx b/content/docs/analytics/reports.mdx index 077787b78..64c0b77e8 100644 --- a/content/docs/analytics/reports.mdx +++ b/content/docs/analytics/reports.mdx @@ -11,7 +11,7 @@ Reports are filterable, exportable lists with grouping and summary totals. Where Every report below has an entry in the sidebar's **Reports** group. The group is collapsed when the app loads, so click it open first; ⌘K also finds a report by its sidebar name. You only get an entry for a report whose records you can read: a service agent, who reads no opportunities, has no entry for the four opportunity reports, and a user who reads no cases has none for the three case reports. -Some sidebar names are shorter than the report titles this page uses: **Pipeline Coverage** opens **Pipeline Coverage by Forecast × Quarter**, **Opportunity Funnel by Owner** opens **Opportunity Funnel by Owner → Stage**, **Lead Engagement** opens **Lead Engagement by Month × Source**, **Accounts by Industry & Type** opens **Accounts by Industry and Type**, **SLA Performance** opens **SLA Performance Report**, and **Cases Opened by Day** opens **Cases Opened by Priority × Day**. +Each sidebar entry carries its report's own title, in every language the app ships, so the name you click is the name on the report you open. `test/app-navigation-shape.test.ts` holds the two together. ## Standard reports @@ -25,8 +25,8 @@ 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** | 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 | +| **Pipeline Coverage by 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** | A matrix of open pipeline amount, excluding closed-lost: one row per rep, one column per stage, and a **Total** column that is each rep's whole book — with a horizontal bar chart of amount by rep | 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. @@ -34,7 +34,7 @@ The eight names this section used to list are published nowhere. Two of them are - **Pipeline by Stage** is a dashboard **tile** and a chart title, not a report. The tile is the open-pipeline horizontal bar chart that **CRM Overview**, **Sales Performance** and **Executive Overview** all share (`src/sales/dashboards/shared-widgets.ts`); the chart is the bar chart on **Opportunities by Stage** above, whose title reads **Pipeline by Stage**. Searching the report library for the name finds nothing; the report you want is **Opportunities by Stage**. - **Stale Opportunities** is a **list view** on the opportunity — **⚠️ Stale Opportunities · Longest in Stage First**, and that is what its tab reads (`src/sales/views/opportunity.view.ts`) — and it does not apply the 14-day cut this table claimed. It lists **every** open deal, ordered by **Stage Entry Date** ascending, so the longest-parked sit on top; the 14 days belongs to the `opportunity_stagnation` flow that nudges owners, not to the view. Analytics cannot rebuild it either: **Days in Current Stage** is a formula evaluated after the query, so nothing can group, filter or sort on it, and no dataset exposes it. -- *Top Performing Reps* exists, under its real name: **Won Opportunities by Owner** is the bookings leaderboard, and **Opportunity Funnel by Owner → Stage** is the same ranking with each rep's stages underneath it. `opportunity_metrics` declares an **Owner** dimension — unlike `case_metrics`, which is why the service side has no agent ranking. +- *Top Performing Reps* exists, under its real name: **Won Opportunities by Owner** is the bookings leaderboard, and **Opportunity Funnel by Owner** is the same ranking with each rep's stages underneath it. `opportunity_metrics` declares an **Owner** dimension — unlike `case_metrics`, which is why the service side has no agent ranking. Of the five that are simply absent, three are a custom report away and two are not: @@ -50,7 +50,7 @@ Of the five that are simply absent, three are a custom report away and two are n | Report | What it shows | | --- | --- | -| **Lead Engagement by Month × Source** | A matrix of contacted-lead volume — lead source down the rows, the month of **Last Contacted** across the columns. Leads nobody has contacted are excluded; they have no engagement month | +| **Lead Engagement by Month** | A matrix of contacted-lead volume — lead source down the rows, the month of **Last Contacted** across the columns. Leads nobody has contacted are excluded; they have no engagement month | The three reports this section used to list are published nowhere, and two of them named a lead status that does not exist: @@ -66,8 +66,8 @@ The lead count is the one lead number anything else reads: the **Open Leads** ti | Report | What it shows | | --- | --- | | **Cases by Status and Priority** | Case count and average resolution time, grouped by status and then priority, with a bar chart of cases by status | -| **SLA Performance Report** | Case count, **SLA Violation Rate** and average resolution time, by priority — over closed cases only | -| **Cases Opened by Priority × Day** | A matrix of daily inflow: priority down the rows, the day each case was opened across the columns | +| **SLA on Closed Cases** | Case count, **SLA Violation Rate** and average resolution time, by priority — over closed cases only | +| **Cases Opened by Day** | A matrix of daily inflow: priority down the rows, the day each case was opened across the columns | The five other reports this section used to list are published nowhere, and most of them ask the semantic layer for something it does not carry. `case_metrics` (`src/service/datasets/case.dataset.ts`) reads `crm_case` alone — no join, no second object — and each bullet below names the one thing that stops it. What the dataset does answer is on [Cubes](/docs/analytics/cubes); the dimensions and measures themselves are declared in `src/service/datasets/case.dataset.ts`, which is the only place that stays current. @@ -77,7 +77,7 @@ The five other reports this section used to list are published nowhere, and most - *Reopened Cases* counts something nothing records. Cases do reopen — the state machine allows **Resolved** and **Closed** back to **In Progress** — but no field marks a case as having been reopened, and no measure counts it. - *CSAT by Agent* has neither half. There is no satisfaction data at all — the case object's *Customer Satisfaction* rating was retired because nothing in the product ever collected it — and there is no agent dimension to group by. -**SLA Performance Report carries no first-response figure either**, and its SLA number is a violation *rate* — the average of the **SLA Violated** flag — rather than an on-time percentage. (The on-time percentage itself is declared: **SLA Compliance Rate**, which the Service dashboard's SLA gauge plots. This report simply does not carry it.) Nothing in this app measures first response at all: **First Response Date** is stamped on a case the first time a call or meeting that already took place is logged on it, and it is then never compared against a target; `case_metrics` declares no first-response measure, so no report and no dashboard tile can report one. See [SLA & Escalation](/docs/service/sla-and-escalation). +**SLA on Closed Cases carries no first-response figure either**, and its SLA number is a violation *rate* — the average of the **SLA Violated** flag — rather than an on-time percentage. (The on-time percentage itself is declared: **SLA Compliance Rate**, which the Service dashboard's SLA gauge plots. This report simply does not carry it.) Nothing in this app measures first response at all: **First Response Date** is stamped on a case the first time a call or meeting that already took place is logged on it, and it is then never compared against a target; `case_metrics` declares no first-response measure, so no report and no dashboard tile can report one. See [SLA & Escalation](/docs/service/sla-and-escalation). ### 💰 Customer and revenue reports @@ -152,8 +152,8 @@ The most common way to consume reports is by **email subscription**: A common pattern: - **Opportunities by Stage** → Mondays 8 AM to the sales team. -- **SLA Performance Report** → daily 6 PM to the service manager. -- **Cases Opened by Priority × Day** → Fridays 4 PM to the customer success team. +- **SLA on Closed Cases** → daily 6 PM to the service manager. +- **Cases Opened by Day** → Fridays 4 PM to the customer success team. ## Tips for users @@ -163,7 +163,7 @@ A common pattern: ## Tips for managers -- ✅ Schedule **Opportunity Funnel by Owner → Stage** weekly to your team — it forces follow-up discipline, rep by rep. For the stalled deals themselves, send people to the **⚠️ Stale Opportunities · Longest in Stage First** tab on Opportunities: it is a list view, so it cannot be scheduled, but it is where the parked deals are ranked. +- ✅ Schedule **Opportunity Funnel by Owner** weekly to your team — it forces follow-up discipline, rep by rep. For the stalled deals themselves, send people to the **⚠️ Stale Opportunities · Longest in Stage First** tab on Opportunities: it is a list view, so it cannot be scheduled, but it is where the parked deals are ranked. - ✅ There is no win/loss report to schedule, but the analysis is a custom report away — group `opportunity_metrics` by **Loss Reason** and read **Lost Deals** next to **Win Rate**. Run it monthly with sales coaches; it is the single best coaching tool. ## Tips for admins diff --git a/content/docs/analytics/reports.zh-Hans.mdx b/content/docs/analytics/reports.zh-Hans.mdx index a6946a44c..878348cf3 100644 --- a/content/docs/analytics/reports.zh-Hans.mdx +++ b/content/docs/analytics/reports.zh-Hans.mdx @@ -11,7 +11,7 @@ description: 标准报表库——HotCRM 就商机、线索、工单与客户发 下面每一份报表在侧边栏的 **Reports** 分组里都有一个条目。这个分组在应用加载时是折叠的,先把它点开;⌘K 也能按侧边栏上的名字找到一份报表。只有你能读取其记录的报表才会出现条目:服务坐席读不了商机,看不到那四张商机报表的条目;读不了工单的人看不到那三张工单报表的条目。 -有几个侧边栏名字比本页用的报表标题短:**Pipeline Coverage** 打开 **Pipeline Coverage by Forecast × Quarter**,**Opportunity Funnel by Owner** 打开 **Opportunity Funnel by Owner → Stage**,**Lead Engagement** 打开 **Lead Engagement by Month × Source**,**Accounts by Industry & Type** 打开 **Accounts by Industry and Type**,**SLA Performance** 打开 **SLA Performance Report**,**Cases Opened by Day** 打开 **Cases Opened by Priority × Day**。 +每个侧边栏条目都用它所打开报表自己的标题,应用自带的每种语言都是如此,所以你点的名字就是打开的报表上的名字。`test/app-navigation-shape.test.ts` 保证两者一致。 ## 标准报表 @@ -25,8 +25,8 @@ 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** | 每位代表的商机盘子再按阶段拆开,带金额、商机数与平均概率,排除已丢单的交易 —— 附一张按阶段的金额条形图,汇总所有代表 | +| **Pipeline Coverage by Quarter** | 未结管道的矩阵:行是预测类别,列是预计成交季度,每格给出金额与商机数 | +| **Opportunity Funnel by Owner** | 未结管道金额的矩阵,排除已丢单的交易:每位代表一行,每个阶段一列,另有一列 **Total** 是该代表的全部盘子 —— 附一张按代表的金额条形图 | 四份都绑定 `opportunity_metrics`(`src/sales/datasets/opportunity.dataset.ts`),这也正是「管道」在这里和在仪表盘上是同一个数字的原因。 @@ -34,7 +34,7 @@ HotCRM 开箱即用地发布 **十份** 报表 —— `src/sales/reports/index.t - **Pipeline by Stage** 是仪表盘**磁贴**标题和图表标题,不是报表。磁贴是 **CRM Overview**、**Sales Performance** 与 **Executive Overview** 三个仪表盘共用的未结管道条形图(`src/sales/dashboards/shared-widgets.ts`);图表则是上面 **Opportunities by Stage** 那张柱状图,它的标题就写作 **Pipeline by Stage**。在报表库里按这个名字搜,一份都搜不到;你要的那份叫 **Opportunities by Stage**。 - **Stale Opportunities** 是商机上的**列表视图** —— **⚠️ Stale Opportunities · Longest in Stage First**,标签页上写的也是这个名字(`src/sales/views/opportunity.view.ts`)—— 而且它并不施加本表所说的 14 天筛选。它列出**所有**未结交易,按 **Stage Entry Date** 升序排,停留最久的排在最上面;那 14 天属于负责催办的 `opportunity_stagnation` 流程,不属于这个视图。分析侧也重建不出来:**Days in Current Stage** 是查询之后才求值的公式,任何东西都无法按它分组、筛选或排序,也没有任何数据集把它暴露出来。 -- *Top Performing Reps* 是存在的,只是真名不同:**Won Opportunities by Owner** 就是赢单排行榜,**Opportunity Funnel by Owner → Stage** 则是同一份排名再把每人的各阶段摊开。`opportunity_metrics` 声明了 **Owner** 维度 —— 这一点和 `case_metrics` 不同,也正是服务侧没有客服排行的原因。 +- *Top Performing Reps* 是存在的,只是真名不同:**Won Opportunities by Owner** 就是赢单排行榜,**Opportunity Funnel by Owner** 则是同一份排名再把每人的各阶段摊开。`opportunity_metrics` 声明了 **Owner** 维度 —— 这一点和 `case_metrics` 不同,也正是服务侧没有客服排行的原因。 剩下五个只是单纯不存在,其中三个自己建得出来、两个建不出来: @@ -50,7 +50,7 @@ HotCRM 开箱即用地发布 **十份** 报表 —— `src/sales/reports/index.t | 报表 | 显示内容 | | --- | --- | -| **Lead Engagement by Month × Source** | 已联系线索数量的矩阵 —— 行是线索来源,列是 **Last Contacted** 所在的月份。从未联系过的线索被排除在外:它们没有互动月份 | +| **Lead Engagement by Month** | 已联系线索数量的矩阵 —— 行是线索来源,列是 **Last Contacted** 所在的月份。从未联系过的线索被排除在外:它们没有互动月份 | 本节曾经列出的三份报表,一份都没有发布,其中两份还用了一个并不存在的线索状态: @@ -66,8 +66,8 @@ HotCRM 开箱即用地发布 **十份** 报表 —— `src/sales/reports/index.t | 报表 | 显示内容 | | --- | --- | | **Cases by Status and Priority** | 工单数与平均解决时长,先按状态、再按优先级分组,并带一张「按状态的工单数」柱状图 | -| **SLA Performance Report** | 工单数、**SLA 违约率** 与平均解决时长,按优先级 —— 只统计已关闭的工单 | -| **Cases Opened by Priority × Day** | 每日新增工单的矩阵:行是优先级,列是工单开出来的那一天 | +| **SLA on Closed Cases** | 工单数、**SLA 违约率** 与平均解决时长,按优先级 —— 只统计已关闭的工单 | +| **Cases Opened by Day** | 每日新增工单的矩阵:行是优先级,列是工单开出来的那一天 | 本节曾经列出的另外五份报表哪儿都没有发布,而且其中多数是在向语义层要它根本没有的东西。`case_metrics`(`src/service/datasets/case.dataset.ts`)只读 `crm_case` 一个对象 —— 不联接、也不跨第二个对象 —— 而下面每一条都点名了卡住它的那一件事。它能答的那些问题在 [多维数据集](/zh-Hans/docs/analytics/cubes) 上;维度与度量本身则声明在 `src/service/datasets/case.dataset.ts` 里,那是唯一会保持最新的地方。 @@ -77,7 +77,7 @@ HotCRM 开箱即用地发布 **十份** 报表 —— `src/sales/reports/index.t - *Reopened Cases* 数的是没有任何东西在记的事。工单确实会重开 —— 状态机允许 **Resolved** 与 **Closed** 回到 **In Progress** —— 但没有任何字段标记一张工单曾被重开,也没有任何度量在数它。 - *CSAT by Agent* 两头都不成立:根本没有满意度数据 —— 工单对象上的*客户满意度*评分因为产品里从来没有任何环节采集它而被退役 —— 也没有「客服」这一维可分组。 -**SLA Performance Report 里同样没有任何首次响应数字**,而且它那个 SLA 数字是 **SLA Violated** 标志的平均值,也就是违约 *率*,不是准时率。(准时率本身是有声明的:**SLA Compliance Rate**,服务仪表盘的 SLA 仪表盘图画的就是它。只是这份报表没有带上它。)本应用根本不度量首次响应:**First Response Date** 只在工单上第一次记录 *已经发生过* 的通话或会议时被盖上,此后从不与任何目标比较;`case_metrics` 没有声明任何首次响应度量,所以没有报表、也没有仪表盘磁贴能报出一个来。参见 [SLA 与升级](/zh-Hans/docs/service/sla-and-escalation)。 +**SLA on Closed Cases 里同样没有任何首次响应数字**,而且它那个 SLA 数字是 **SLA Violated** 标志的平均值,也就是违约 *率*,不是准时率。(准时率本身是有声明的:**SLA Compliance Rate**,服务仪表盘的 SLA 仪表盘图画的就是它。只是这份报表没有带上它。)本应用根本不度量首次响应:**First Response Date** 只在工单上第一次记录 *已经发生过* 的通话或会议时被盖上,此后从不与任何目标比较;`case_metrics` 没有声明任何首次响应度量,所以没有报表、也没有仪表盘磁贴能报出一个来。参见 [SLA 与升级](/zh-Hans/docs/service/sla-and-escalation)。 ### 💰 客户与收入报表 @@ -152,8 +152,8 @@ HotCRM 开箱即用地发布 **十份** 报表 —— `src/sales/reports/index.t 一种常见模式: - **Opportunities by Stage** → 周一早上 8 点发给销售团队。 -- **SLA Performance Report** → 每日下午 6 点发给服务经理。 -- **Cases Opened by Priority × Day** → 周五下午 4 点发给客户成功团队。 +- **SLA on Closed Cases** → 每日下午 6 点发给服务经理。 +- **Cases Opened by Day** → 周五下午 4 点发给客户成功团队。 ## 给用户的提示 @@ -163,7 +163,7 @@ HotCRM 开箱即用地发布 **十份** 报表 —— `src/sales/reports/index.t ## 给经理的提示 -- ✅ 每周向你的团队安排 **Opportunity Funnel by Owner → Stage**——它能逐人强制跟进纪律。至于停滞的交易本身,请把人指到商机的 **⚠️ Stale Opportunities · Longest in Stage First** 标签页:它是列表视图,定时推送不了,但停摆最久的交易就排在那儿。 +- ✅ 每周向你的团队安排 **Opportunity Funnel by Owner**——它能逐人强制跟进纪律。至于停滞的交易本身,请把人指到商机的 **⚠️ Stale Opportunities · Longest in Stage First** 标签页:它是列表视图,定时推送不了,但停摆最久的交易就排在那儿。 - ✅ 没有赢单/丢单报表可以安排,但这份分析只差一份自建报表——把 `opportunity_metrics` 按 **Loss Reason** 分组,并把 **Lost Deals** 与 **Win Rate** 并排看。每月与销售教练一起跑,它是最好的单一辅导工具。 ## 给管理员的提示 diff --git a/content/docs/analytics/reports.zh-Hant.mdx b/content/docs/analytics/reports.zh-Hant.mdx index 1415bba74..1e9615cfa 100644 --- a/content/docs/analytics/reports.zh-Hant.mdx +++ b/content/docs/analytics/reports.zh-Hant.mdx @@ -13,7 +13,7 @@ description: 標準報表庫——HotCRM 就商機、潛在客戶、工單與客 下面每一份報表在側邊欄的 **Reports** 群組裡都有一個條目。這個群組在應用載入時是摺疊的,先把它點開;⌘K 也能按側邊欄上的名字找到一份報表。只有你能讀取其記錄的報表才會出現條目:服務專員讀不了商機,看不到那四張商機報表的條目;讀不了工單的人看不到那三張工單報表的條目。 -有幾個側邊欄名字比本頁用的報表標題短:**Pipeline Coverage** 打開 **Pipeline Coverage by Forecast × Quarter**,**Opportunity Funnel by Owner** 打開 **Opportunity Funnel by Owner → Stage**,**Lead Engagement** 打開 **Lead Engagement by Month × Source**,**Accounts by Industry & Type** 打開 **Accounts by Industry and Type**,**SLA Performance** 打開 **SLA Performance Report**,**Cases Opened by Day** 打開 **Cases Opened by Priority × Day**。 +每個側邊欄條目都用它所打開報表自己的標題,應用內建的每種語言都是如此,所以你點的名字就是打開的報表上的名字。`test/app-navigation-shape.test.ts` 保證兩者一致。 ## 標準報表 @@ -27,8 +27,8 @@ 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** | 每位代表的商機盤子再按階段拆開,帶金額、商機數與平均機率,排除已丟單的交易 —— 附一張按階段的金額橫條圖,彙總所有代表 | +| **Pipeline Coverage by Quarter** | 未結管道的矩陣:列是預測類別,欄是預計成交季度,每格給出金額與商機數 | +| **Opportunity Funnel by Owner** | 未結管道金額的矩陣,排除已丟單的交易:每位代表一列,每個階段一欄,另有一欄 **Total** 是該代表的全部盤子 —— 附一張按代表的金額橫條圖 | 四份都繫結 `opportunity_metrics`(`src/sales/datasets/opportunity.dataset.ts`),這也正是「管道」在這裡和在儀表板上是同一個數字的原因。 @@ -36,7 +36,7 @@ HotCRM 開箱即用地發布 **十份** 報表 —— `src/sales/reports/index.t - **Pipeline by Stage** 是儀表板**磁貼**標題和圖表標題,不是報表。磁貼是 **CRM Overview**、**Sales Performance** 與 **Executive Overview** 三個儀表板共用的未結管道橫條圖(`src/sales/dashboards/shared-widgets.ts`);圖表則是上面 **Opportunities by Stage** 那張長條圖,它的標題就寫作 **Pipeline by Stage**。在報表庫裡按這個名字搜,一份都搜不到;你要的那份叫 **Opportunities by Stage**。 - **Stale Opportunities** 是商機上的**清單檢視** —— **⚠️ Stale Opportunities · Longest in Stage First**,標籤頁上寫的也是這個名字(`src/sales/views/opportunity.view.ts`)—— 而且它並不施加本表所說的 14 天篩選。它列出**所有**未結交易,按 **Stage Entry Date** 升冪排,停留最久的排在最上面;那 14 天屬於負責催辦的 `opportunity_stagnation` 流程,不屬於這個檢視。分析側也重建不出來:**Days in Current Stage** 是查詢之後才求值的公式,任何東西都無法按它分組、篩選或排序,也沒有任何資料集把它暴露出來。 -- *Top Performing Reps* 是存在的,只是真名不同:**Won Opportunities by Owner** 就是贏單排行榜,**Opportunity Funnel by Owner → Stage** 則是同一份排名再把每人的各階段攤開。`opportunity_metrics` 宣告了 **Owner** 維度 —— 這一點和 `case_metrics` 不同,也正是服務側沒有客服排行的原因。 +- *Top Performing Reps* 是存在的,只是真名不同:**Won Opportunities by Owner** 就是贏單排行榜,**Opportunity Funnel by Owner** 則是同一份排名再把每人的各階段攤開。`opportunity_metrics` 宣告了 **Owner** 維度 —— 這一點和 `case_metrics` 不同,也正是服務側沒有客服排行的原因。 剩下五個只是單純不存在,其中三個自己建得出來、兩個建不出來: @@ -52,7 +52,7 @@ HotCRM 開箱即用地發布 **十份** 報表 —— `src/sales/reports/index.t | 報表 | 顯示內容 | | --- | --- | -| **Lead Engagement by Month × Source** | 已聯絡潛在客戶數量的矩陣 —— 橫列是潛在客戶來源,直欄是 **Last Contacted** 所在的月份。從未聯絡過的潛在客戶被排除在外:它們沒有互動月份 | +| **Lead Engagement by Month** | 已聯絡潛在客戶數量的矩陣 —— 橫列是潛在客戶來源,直欄是 **Last Contacted** 所在的月份。從未聯絡過的潛在客戶被排除在外:它們沒有互動月份 | 本節曾經列出的三份報表,一份都沒有發布,其中兩份還用了一個並不存在的潛在客戶狀態: @@ -68,8 +68,8 @@ HotCRM 開箱即用地發布 **十份** 報表 —— `src/sales/reports/index.t | 報表 | 顯示內容 | | --- | --- | | **Cases by Status and Priority** | 工單數與平均解決時長,先按狀態、再按優先順序分組,並帶一張「按狀態的工單數」長條圖 | -| **SLA Performance Report** | 工單數、**SLA 違約率** 與平均解決時長,按優先順序 —— 只統計已關閉的工單 | -| **Cases Opened by Priority × Day** | 每日新增工單的矩陣:橫列是優先順序,直欄是工單開出來的那一天 | +| **SLA on Closed Cases** | 工單數、**SLA 違約率** 與平均解決時長,按優先順序 —— 只統計已關閉的工單 | +| **Cases Opened by Day** | 每日新增工單的矩陣:橫列是優先順序,直欄是工單開出來的那一天 | 本節曾經列出的另外五份報表哪兒都沒有發布,而且其中多數是在向語義層要它根本沒有的東西。`case_metrics`(`src/service/datasets/case.dataset.ts`)只讀 `crm_case` 一個物件 —— 不聯結、也不跨第二個物件 —— 而下面每一條都點名了卡住它的那一件事。它能答的那些問題在 [多維資料集](/zh-Hant/docs/analytics/cubes) 上;維度與度量本身則宣告在 `src/service/datasets/case.dataset.ts` 裡,那是唯一會保持最新的地方。 @@ -79,7 +79,7 @@ HotCRM 開箱即用地發布 **十份** 報表 —— `src/sales/reports/index.t - *Reopened Cases* 數的是沒有任何東西在記的事。工單確實會重開 —— 狀態機允許 **Resolved** 與 **Closed** 回到 **In Progress** —— 但沒有任何欄位標記一張工單曾被重開,也沒有任何度量在數它。 - *CSAT by Agent* 兩頭都不成立:根本沒有滿意度資料 —— 工單物件上的*客戶滿意度*評分因為產品裡從來沒有任何環節採集它而被退役 —— 也沒有「客服」這一維可分組。 -**SLA Performance Report 裡同樣沒有任何首次回應數字**,而且它那個 SLA 數字是 **SLA Violated** 標誌的平均值,也就是違約 *率*,不是準時率。(準時率本身是有宣告的:**SLA Compliance Rate**,服務儀表板的 SLA 儀表圖畫的就是它。只是這份報表沒有帶上它。)本應用根本不度量首次回應:**First Response Date** 只在工單上第一次記錄 *已經發生過* 的通話或會議時被蓋上,此後從不與任何目標比較;`case_metrics` 沒有宣告任何首次回應度量,所以沒有報表、也沒有儀表板磁貼能報出一個來。參見 [SLA 與升級](/zh-Hant/docs/service/sla-and-escalation)。 +**SLA on Closed Cases 裡同樣沒有任何首次回應數字**,而且它那個 SLA 數字是 **SLA Violated** 標誌的平均值,也就是違約 *率*,不是準時率。(準時率本身是有宣告的:**SLA Compliance Rate**,服務儀表板的 SLA 儀表圖畫的就是它。只是這份報表沒有帶上它。)本應用根本不度量首次回應:**First Response Date** 只在工單上第一次記錄 *已經發生過* 的通話或會議時被蓋上,此後從不與任何目標比較;`case_metrics` 沒有宣告任何首次回應度量,所以沒有報表、也沒有儀表板磁貼能報出一個來。參見 [SLA 與升級](/zh-Hant/docs/service/sla-and-escalation)。 ### 💰 客戶與收入報表 @@ -154,8 +154,8 @@ HotCRM 開箱即用地發布 **十份** 報表 —— `src/sales/reports/index.t 一種常見模式: - **Opportunities by Stage** → 週一早上 8 點發給銷售團隊。 -- **SLA Performance Report** → 每日下午 6 點發給服務經理。 -- **Cases Opened by Priority × Day** → 週五下午 4 點發給客戶成功團隊。 +- **SLA on Closed Cases** → 每日下午 6 點發給服務經理。 +- **Cases Opened by Day** → 週五下午 4 點發給客戶成功團隊。 ## 給使用者的提示 @@ -165,7 +165,7 @@ HotCRM 開箱即用地發布 **十份** 報表 —— `src/sales/reports/index.t ## 給經理的提示 -- ✅ 每週向你的團隊安排 **Opportunity Funnel by Owner → Stage**——它能逐人強制跟進紀律。至於停滯的交易本身,請把人指到商機的 **⚠️ Stale Opportunities · Longest in Stage First** 標籤頁:它是清單檢視,定時推送不了,但停擺最久的交易就排在那兒。 +- ✅ 每週向你的團隊安排 **Opportunity Funnel by Owner**——它能逐人強制跟進紀律。至於停滯的交易本身,請把人指到商機的 **⚠️ Stale Opportunities · Longest in Stage First** 標籤頁:它是清單檢視,定時推送不了,但停擺最久的交易就排在那兒。 - ✅ 沒有贏單/丟單報表可以安排,但這份分析只差一份自建報表——把 `opportunity_metrics` 按 **Loss Reason** 分組,並把 **Lost Deals** 與 **Win Rate** 並排看。每月與銷售教練一起跑,它是最好的單一輔導工具。 ## 給管理員的提示 diff --git a/content/docs/getting-started/quick-tour.mdx b/content/docs/getting-started/quick-tour.mdx index a78d7d21f..b5b937f97 100644 --- a/content/docs/getting-started/quick-tour.mdx +++ b/content/docs/getting-started/quick-tour.mdx @@ -30,7 +30,7 @@ For a manager or an administrator it is the daily start-of-day landing page. Four things this page used to promise are on no tile here, and one name it used belongs to a different dashboard: - **CRM Overview** is a real dashboard, but it is not the one **Home** opens. It sits under **Insights**, further down the sidebar, so it is a click away rather than the first thing you see. **Open Deals (Selected Period)** (a count of open opportunities), **Deals Won (Selected Period)** and **Pipeline by Owner** are its tiles, not this dashboard's. -- *Cases awaiting your response* are on neither dashboard: no tile on **Executive Overview** or on **CRM Overview** counts a case. **Open Cases**, **Critical Cases** and **SLA Violations** are on **Service Overview**, under **Service** — see [Service](/docs/service/index). +- *Cases awaiting your response* are on neither dashboard: no tile on **Executive Overview** or on **CRM Overview** counts a case. **Open Cases**, **Critical Cases** and **Open Cases Past SLA** are on **Service Overview**, under **Service** — see [Service](/docs/service/index). - *Top accounts by pipeline value* is not a tile in this app at all. **Accounts by Industry** groups by industry rather than ranking customers, and the nearest ranking anywhere — **Pipeline by Owner** on **CRM Overview** — ranks sales reps rather than customers. - *Recent activity (last calls, meetings, emails)* is not here either. **Interactions Logged**, **Meetings Booked** and **Customer Minutes** are on **Sales Activity**, under **Activity**. - *Team-level rollups for managers* are not something this dashboard does. Every tile runs the same query for everyone; what differs is which records you are allowed to read, and that comes from your permission set. Positions in this app are flat — there is no reporting line for visibility to roll up — and the **Sales Manager** set grants `viewAllRecords` outright, so a manager reads org-wide totals rather than a team slice. @@ -56,12 +56,12 @@ Inside HotCRM, the left nav opens with two pinned items — **Home**, which is t | **Activity** | Events, Sales Activity | | **Marketing** | Campaigns | | **Insights** | CRM Overview, Forecasts | -| **Reports** | Opportunities by Stage, Won Opportunities by Owner, Pipeline Coverage, Opportunity Funnel by Owner, Lead Engagement, Accounts by Industry & Type, Customer Churn Signals, Cases by Status and Priority, SLA Performance, Cases Opened by Day | +| **Reports** | Opportunities by Stage, Won Opportunities by Owner, Pipeline Coverage by Quarter, Opportunity Funnel by Owner, Lead Engagement by Month, Accounts by Industry and Type, Customer Churn Signals, Cases by Status and Priority, SLA on Closed Cases, Cases Opened by Day | | **Help** | User Manual, Admin Manual, Revenue Manual, Marketing Manual | **Reports** and **Help** are collapsed when the app loads — **Service**, **Sales**, **My Work**, **Activity**, **Marketing** and **Insights** open themselves. Click **Reports** open before deciding a report isn't there. **Help** opens the in-app manuals; **Admin Manual** is shown to administrators only. -The entries built on opportunities — **Home**, **Sales Home**, **Sales Performance**, **CRM Overview** and the four opportunity reports under **Reports** — are left out of the sidebar for anyone who cannot read opportunities, and for a service agent or service manager by position: and as they own no deals, **Opportunities** and **My Deals**, which were always empty for them, are not shown to them either. Every other report asks the same of its own records: a service agent still gets the lead, account and case reports, and a user who cannot read cases does not get **Service Overview** or the three case reports. **Home** is left out for a sales rep as well, which is why a rep's sidebar starts at **Sales Home**. A sales rep works no cases, so a rep's sidebar also leaves out **Service Overview**, **Cases**, **My Cases** and the three case reports — **Cases by Status and Priority**, **SLA Performance** and **Cases Opened by Day**. A rep's **Knowledge** is the last entry of **Sales** instead, opening the published articles, so a rep's sidebar has no **Service** group at all. Everyone else finds **Knowledge** under **Service**. **Service** is the first group because the app opens on the first entry a sidebar shows: for a service agent or service manager that is **Service Overview**. +The entries built on opportunities — **Home**, **Sales Home**, **Sales Performance**, **CRM Overview** and the four opportunity reports under **Reports** — are left out of the sidebar for anyone who cannot read opportunities, and for a service agent or service manager by position: and as they own no deals, **Opportunities** and **My Deals**, which were always empty for them, are not shown to them either. Every other report asks the same of its own records: a service agent still gets the lead, account and case reports, and a user who cannot read cases does not get **Service Overview** or the three case reports. **Home** is left out for a sales rep as well, which is why a rep's sidebar starts at **Sales Home**. A sales rep works no cases, so a rep's sidebar also leaves out **Service Overview**, **Cases**, **My Cases** and the three case reports — **Cases by Status and Priority**, **SLA on Closed Cases** and **Cases Opened by Day**. A rep's **Knowledge** is the last entry of **Sales** instead, opening the published articles, so a rep's sidebar has no **Service** group at all. Everyone else finds **Knowledge** under **Service**. **Service** is the first group because the app opens on the first entry a sidebar shows: for a service agent or service manager that is **Service Overview**. One sidebar row per destination is deliberate. Where an object offers several ways of looking at the same records, those live as tabs across the top of that object's list page, not as extra rows in the nav — so the sidebar stays short enough to scan. diff --git a/content/docs/getting-started/quick-tour.zh-Hans.mdx b/content/docs/getting-started/quick-tour.zh-Hans.mdx index 45519ae78..e812a6186 100644 --- a/content/docs/getting-started/quick-tour.zh-Hans.mdx +++ b/content/docs/getting-started/quick-tour.zh-Hans.mdx @@ -32,7 +32,7 @@ description: HotCRM 的 5 分钟演练 —— 主页、你的第一条线索、 本节过去承诺的 4 样东西,这块盘上没有任何磁贴对应;另有一个名字,它属于另一块盘: - **CRM Overview** 是一块真实存在的仪表盘,但它不是 **Home** 打开的那块。它挂在 **Insights** 分组下,在侧边栏靠下的位置——所以它要点一下才打开,不是"第一眼看到"的东西。**Open Deals (Selected Period)**(进行中商机的条数)、**Deals Won (Selected Period)** 与 **Pipeline by Owner** 是它的磁贴,不是这块盘的。 -- *等待你响应的工单*:两块盘上都没有。**Executive Overview** 与 **CRM Overview** 都没有任何磁贴在数工单。**Open Cases**、**Critical Cases**、**SLA Violations** 在 **Service** 分组下的 **Service Overview** 上——参见 [Service](/zh-Hans/docs/service/index)。 +- *等待你响应的工单*:两块盘上都没有。**Executive Overview** 与 **CRM Overview** 都没有任何磁贴在数工单。**Open Cases**、**Critical Cases**、**Open Cases Past SLA** 在 **Service** 分组下的 **Service Overview** 上——参见 [Service](/zh-Hans/docs/service/index)。 - *按管道价值排名的顶级客户*:本应用里根本没有这样一个磁贴。**Accounts by Industry** 是按行业归组,不是给客户排名;全应用最接近的排名是 **CRM Overview** 上的 **Pipeline by Owner**,那是按销售代表排,不是按客户排。 - *近期活动(最近的通话、会议、邮件)*:也不在这里。**Interactions Logged**、**Meetings Booked** 与 **Customer Minutes** 在 **Activity** 分组下的 **Sales Activity** 上。 - *给经理看的团队级汇总*:这块盘不做这件事。每个磁贴对所有人跑的都是同一个查询,变的是你被允许读到哪些记录,而那由你的权限集决定。本应用的岗位是扁平的——没有可供可见性向上汇总的汇报线——而 **Sales Manager** 权限集直接授予 `viewAllRecords`,所以经理读到的是全组织的合计,而不是某个团队的切片。 @@ -58,14 +58,14 @@ description: HotCRM 的 5 分钟演练 —— 主页、你的第一条线索、 | **Activity** | Events、Sales Activity | | **Marketing** | Campaigns | | **Insights** | CRM Overview、Forecasts | -| **Reports** | Opportunities by Stage、Won Opportunities by Owner、Pipeline Coverage、Opportunity Funnel by Owner、Lead Engagement、Accounts by Industry & Type、Customer Churn Signals、Cases by Status and Priority、SLA Performance、Cases Opened by Day | +| **Reports** | Opportunities by Stage、Won Opportunities by Owner、Pipeline Coverage by Quarter、Opportunity Funnel by Owner、Lead Engagement by Month、Accounts by Industry and Type、Customer Churn Signals、Cases by Status and Priority、SLA on Closed Cases、Cases Opened by Day | | **Help** | User Manual、Admin Manual、Revenue Manual、Marketing Manual | 上表用的是源码里的英文 label,也就是英文界面上看到的名字。简体中文界面下,它们显示为语言包里的译名——例如 **Inbox** 显示为「待我审批」、**Knowledge** 显示为「知识库」、**My Work** 显示为「我的工作」。 **Reports** 和 **Help** 这两个分组在应用加载时是折叠的——**Service**、**Sales**、**My Work**、**Activity**、**Marketing**、**Insights** 都会自动展开。先把 **Reports** 点开,再判断某份报表在不在这里。**Help** 打开应用内手册;**Admin Manual** 只对管理员显示。 -建立在商机之上的条目——**Home**、**Sales Home**、**Sales Performance**、**CRM Overview**,以及 **Reports** 下的四张商机报表——不能读取商机的人在侧边栏里看不到;服务坐席与服务经理也按岗位看不到它们:他们名下没有商机,**Opportunities** 与 **My Deals** 对他们总是空的,所以这两个条目也不对他们显示。其余报表对各自的数据也是同样的要求:服务坐席照样看得到线索、客户与工单报表,而不能读取工单的人看不到 **Service Overview** 和那三张工单报表。销售代表也看不到 **Home**,所以销售代表的侧边栏从 **Sales Home** 开始。销售代表不处理工单,所以销售代表的侧边栏也看不到 **Service Overview**、**Cases**、**My Cases** 以及那三张工单报表——**Cases by Status and Priority**、**SLA Performance**、**Cases Opened by Day**。销售代表的 **Knowledge** 改为 **Sales** 的最后一项,打开已发布的文章,所以销售代表的侧边栏里没有 **Service** 分组。其他人仍在 **Service** 下找到 **Knowledge**。**Service** 排在所有分组之首,是因为应用打开时落在侧边栏显示的第一个条目上:对服务坐席与服务经理来说,那就是 **Service Overview**。 +建立在商机之上的条目——**Home**、**Sales Home**、**Sales Performance**、**CRM Overview**,以及 **Reports** 下的四张商机报表——不能读取商机的人在侧边栏里看不到;服务坐席与服务经理也按岗位看不到它们:他们名下没有商机,**Opportunities** 与 **My Deals** 对他们总是空的,所以这两个条目也不对他们显示。其余报表对各自的数据也是同样的要求:服务坐席照样看得到线索、客户与工单报表,而不能读取工单的人看不到 **Service Overview** 和那三张工单报表。销售代表也看不到 **Home**,所以销售代表的侧边栏从 **Sales Home** 开始。销售代表不处理工单,所以销售代表的侧边栏也看不到 **Service Overview**、**Cases**、**My Cases** 以及那三张工单报表——**Cases by Status and Priority**、**SLA on Closed Cases**、**Cases Opened by Day**。销售代表的 **Knowledge** 改为 **Sales** 的最后一项,打开已发布的文章,所以销售代表的侧边栏里没有 **Service** 分组。其他人仍在 **Service** 下找到 **Knowledge**。**Service** 排在所有分组之首,是因为应用打开时落在侧边栏显示的第一个条目上:对服务坐席与服务经理来说,那就是 **Service Overview**。 一个去处只占一行,这是刻意的。同一个对象如果有多种查看方式,它们是该对象列表页顶部的标签页,而不是导航里另起一行——这样侧边栏才短到能一眼扫完。 diff --git a/content/docs/getting-started/quick-tour.zh-Hant.mdx b/content/docs/getting-started/quick-tour.zh-Hant.mdx index 0a4ecd8b8..dbc86fa07 100644 --- a/content/docs/getting-started/quick-tour.zh-Hant.mdx +++ b/content/docs/getting-started/quick-tour.zh-Hant.mdx @@ -34,7 +34,7 @@ description: HotCRM 的 5 分鐘演練 —— 主頁、你的第一條線索、 本節過去承諾的 4 樣東西,這塊儀表板上沒有任何磁貼對應;另有一個名字,它屬於另一塊儀表板: - **CRM Overview** 是一塊真實存在的儀表板,但它不是 **Home** 打開的那塊。它掛在 **Insights** 群組下,在側邊欄靠下的位置——所以它要點一下才打開,不是"第一眼看到"的東西。**Open Deals (Selected Period)**(進行中商機的筆數)、**Deals Won (Selected Period)** 與 **Pipeline by Owner** 是它的磁貼,不是這塊的。 -- *等待你回應的工單*:兩塊儀表板上都沒有。**Executive Overview** 與 **CRM Overview** 都沒有任何磁貼在數工單。**Open Cases**、**Critical Cases**、**SLA Violations** 在 **Service** 群組下的 **Service Overview** 上——參見 [Service](/zh-Hant/docs/service/index)。 +- *等待你回應的工單*:兩塊儀表板上都沒有。**Executive Overview** 與 **CRM Overview** 都沒有任何磁貼在數工單。**Open Cases**、**Critical Cases**、**Open Cases Past SLA** 在 **Service** 群組下的 **Service Overview** 上——參見 [Service](/zh-Hant/docs/service/index)。 - *按管道價值排名的頂級客戶*:本應用裡根本沒有這樣一個磁貼。**Accounts by Industry** 是按產業歸組,不是給客戶排名;全應用最接近的排名是 **CRM Overview** 上的 **Pipeline by Owner**,那是按業務代表排,不是按客戶排。 - *近期活動(最近的通話、會議、郵件)*:也不在這裡。**Interactions Logged**、**Meetings Booked** 與 **Customer Minutes** 在 **Activity** 群組下的 **Sales Activity** 上。 - *給經理看的團隊級彙總*:這塊儀表板不做這件事。每個磁貼對所有人跑的都是同一個查詢,變的是你被允許讀到哪些記錄,而那由你的權限集決定。本應用的職位是扁平的——沒有可供可見性向上彙總的匯報線——而 **Sales Manager** 權限集直接授予 `viewAllRecords`,所以經理讀到的是全組織的合計,而不是某個團隊的切片。 @@ -58,14 +58,14 @@ description: HotCRM 的 5 分鐘演練 —— 主頁、你的第一條線索、 | **Activity** | Events、Sales Activity | | **Marketing** | Campaigns | | **Insights** | CRM Overview、Forecasts | -| **Reports** | Opportunities by Stage、Won Opportunities by Owner、Pipeline Coverage、Opportunity Funnel by Owner、Lead Engagement、Accounts by Industry & Type、Customer Churn Signals、Cases by Status and Priority、SLA Performance、Cases Opened by Day | +| **Reports** | Opportunities by Stage、Won Opportunities by Owner、Pipeline Coverage by Quarter、Opportunity Funnel by Owner、Lead Engagement by Month、Accounts by Industry and Type、Customer Churn Signals、Cases by Status and Priority、SLA on Closed Cases、Cases Opened by Day | | **Help** | User Manual、Admin Manual、Revenue Manual、Marketing Manual | 上表用的是原始碼裡的英文 label,也就是英文介面上看到的名字。本應用提供 en / zh-CN / ja-JP / es-ES 四種介面語言,沒有繁體語言包——簡體中文介面下這些名字會顯示為語言包裡的譯名,例如 **Inbox** 顯示為「待我审批」、**Knowledge** 顯示為「知识库」、**My Work** 顯示為「我的工作」。 **Reports** 和 **Help** 這兩個群組在應用載入時是摺疊的——**Service**、**Sales**、**My Work**、**Activity**、**Marketing**、**Insights** 都會自動展開。先把 **Reports** 點開,再判斷某份報表在不在這裡。**Help** 開啟應用內手冊;**Admin Manual** 只對管理員顯示。 -建立在商機之上的條目——**Home**、**Sales Home**、**Sales Performance**、**CRM Overview**,以及 **Reports** 下的四張商機報表——不能讀取商機的人在側邊欄裡看不到;服務專員與服務經理也按職位看不到它們:他們名下沒有商機,**Opportunities** 與 **My Deals** 對他們總是空的,所以這兩個條目也不對他們顯示。其餘報表對各自的資料也是同樣的要求:服務專員照樣看得到線索、客戶與工單報表,而不能讀取工單的人看不到 **Service Overview** 和那三張工單報表。銷售代表也看不到 **Home**,所以銷售代表的側邊欄從 **Sales Home** 開始。銷售代表不處理工單,所以銷售代表的側邊欄也看不到 **Service Overview**、**Cases**、**My Cases** 以及那三張工單報表——**Cases by Status and Priority**、**SLA Performance**、**Cases Opened by Day**。銷售代表的 **Knowledge** 改為 **Sales** 的最後一項,開啟已發布的文章,所以銷售代表的側邊欄裡沒有 **Service** 群組。其他人仍在 **Service** 下找到 **Knowledge**。**Service** 排在所有群組之首,是因為應用打開時落在側邊欄顯示的第一個條目上:對服務專員與服務經理來說,那就是 **Service Overview**。 +建立在商機之上的條目——**Home**、**Sales Home**、**Sales Performance**、**CRM Overview**,以及 **Reports** 下的四張商機報表——不能讀取商機的人在側邊欄裡看不到;服務專員與服務經理也按職位看不到它們:他們名下沒有商機,**Opportunities** 與 **My Deals** 對他們總是空的,所以這兩個條目也不對他們顯示。其餘報表對各自的資料也是同樣的要求:服務專員照樣看得到線索、客戶與工單報表,而不能讀取工單的人看不到 **Service Overview** 和那三張工單報表。銷售代表也看不到 **Home**,所以銷售代表的側邊欄從 **Sales Home** 開始。銷售代表不處理工單,所以銷售代表的側邊欄也看不到 **Service Overview**、**Cases**、**My Cases** 以及那三張工單報表——**Cases by Status and Priority**、**SLA on Closed Cases**、**Cases Opened by Day**。銷售代表的 **Knowledge** 改為 **Sales** 的最後一項,開啟已發布的文章,所以銷售代表的側邊欄裡沒有 **Service** 群組。其他人仍在 **Service** 下找到 **Knowledge**。**Service** 排在所有群組之首,是因為應用打開時落在側邊欄顯示的第一個條目上:對服務專員與服務經理來說,那就是 **Service Overview**。 一個去處只佔一列,這是刻意的。同一個物件如果有多種檢視方式,它們是該物件列表頁頂部的標籤頁,而不是導覽裡另起一列——這樣側邊欄才短到能一眼掃完。 diff --git a/content/docs/marketing/index.mdx b/content/docs/marketing/index.mdx index 43e5fb1d2..18677dbde 100644 --- a/content/docs/marketing/index.mdx +++ b/content/docs/marketing/index.mdx @@ -66,7 +66,7 @@ Every opportunity has a *Campaign* lookup field. Whenever sales closes a deal th ## Standard reports -- **Lead Engagement (by Month × Source)** — which channels' leads is the team working, month by month? It counts each lead in the month it was last contacted, not the month it arrived. +- **Lead Engagement by Month** (lead source down the rows) — which channels' leads is the team working, month by month? It counts each lead in the month it was last contacted, not the month it arrived. - **Campaign ROI** *(via campaign list views)* — actual revenue vs. actual cost per campaign. See [Analytics › Reports](/docs/analytics/reports). diff --git a/content/docs/marketing/index.zh-Hans.mdx b/content/docs/marketing/index.zh-Hans.mdx index 41d228c3c..fae22d8d7 100644 --- a/content/docs/marketing/index.zh-Hans.mdx +++ b/content/docs/marketing/index.zh-Hans.mdx @@ -66,7 +66,7 @@ Marketing 模块让你能够**规划营销活动、加入合适的线索和联 ## 标准报表 -- **Lead Engagement (by Month × Source)** — 各渠道的线索每月跟进了多少?它把每条线索计在最近一次联系的月份,而不是线索进来的月份。 +- **Lead Engagement by Month**(按行列出线索来源)— 各渠道的线索每月跟进了多少?它把每条线索计在最近一次联系的月份,而不是线索进来的月份。 - **Campaign ROI** *(通过营销活动列表视图)* — 每个营销活动的实际收入对比实际成本。 请参阅 [分析 › 报表](/zh-Hans/docs/analytics/reports)。 diff --git a/content/docs/marketing/index.zh-Hant.mdx b/content/docs/marketing/index.zh-Hant.mdx index 800886e8b..d8ca4cf52 100644 --- a/content/docs/marketing/index.zh-Hant.mdx +++ b/content/docs/marketing/index.zh-Hant.mdx @@ -68,7 +68,7 @@ Marketing 模組讓你能夠**規劃行銷活動、加入合適的潛在客戶 ## 標準報表 -- **Lead Engagement (by Month × Source)** — 各渠道的潛在客戶每月跟進了多少?它把每位潛在客戶計在最近一次聯絡的月份,而不是進來的月份。 +- **Lead Engagement by Month**(按列列出潛在客戶來源)— 各渠道的潛在客戶每月跟進了多少?它把每位潛在客戶計在最近一次聯絡的月份,而不是進來的月份。 - **Campaign ROI** *(透過行銷活動清單檢視)* — 每個行銷活動的實際收入對比實際成本。 請參閱 [分析 › 報表](/zh-Hant/docs/analytics/reports)。 diff --git a/content/docs/sales/pipeline-management.mdx b/content/docs/sales/pipeline-management.mdx index 24c1eb544..d8ad33bf7 100644 --- a/content/docs/sales/pipeline-management.mdx +++ b/content/docs/sales/pipeline-management.mdx @@ -69,12 +69,12 @@ The AI assistant surfaces at-risk deals when you ask *"what's slipping?"* or *"h ## Pipeline reports worth bookmarking -All five are in the sidebar's **Reports** group, which holds every report the app ships and is collapsed when the app loads. Names here are the labels the report library lists, which is not always the shorter name on the nav entry. +All five are in the sidebar's **Reports** group, which holds every report the app ships and is collapsed when the app loads. Names here are the report titles, and each nav entry reads the same. | Report | What it shows | | --- | --- | -| **Pipeline Coverage by Forecast × Quarter** | Are we covering 3x the quota for the upcoming quarter? A matrix of open pipeline: forecast category down the rows, close quarter across the columns. The sidebar entry for it reads **Pipeline Coverage**. | -| **Opportunity Funnel by Owner → Stage** | Per-rep funnel health — spot leaky stages. | +| **Pipeline Coverage by Quarter** | Are we covering 3x the quota for the upcoming quarter? A matrix of open pipeline: forecast category down the rows, close quarter across the columns. | +| **Opportunity Funnel by Owner** | Per-rep funnel health — spot leaky stages. | | **Opportunities by Stage** | Bar chart of pipeline by stage. | | **Won Opportunities by Owner** | Leaderboard for quarter-end recognition. | | **Customer Churn Signals** | Four stacked panels for proactive retention: **CSM-Flagged Accounts** (accounts a CSM has flagged by hand), **At-Risk Accounts** (no activity in 60+ days), **Silent High-Value Accounts** (strategic and enterprise accounts quiet for 90+ days) and **Recently Lost Opportunities** (closed-lost in the last 30 days). | @@ -87,7 +87,7 @@ See [Analytics › Reports](/docs/analytics/reports) for details. | --- | --- | | **Daily** | Open **Opportunities** and switch to its **Sales Pipeline** tab. Find stalled cards (in-stage > 14 days). Push or close. | | **Weekly** | Run *Pipeline Coverage*. Ask the AI assistant: *"what's at risk?"* | -| **Monthly** | Run *Opportunity Funnel by Owner → Stage*. Coach reps with leaky stages 1:1. | +| **Monthly** | Run *Opportunity Funnel by Owner*. Coach reps with leaky stages 1:1. | | **Quarter-end** | Run *Won Opportunities by Owner* for commissions. Archive lost deals with a *loss reason*. | ## Forecasting roll-up @@ -96,7 +96,7 @@ The Sales Manager / Director view of the forecast is built from: - The **Opportunity Metrics** dataset (`opportunity_metrics`) — measure **Total Amount** (sum of amount), dimension **Stage** - The **Sales Performance** dashboard (`sales_dashboard`) — widgets for pipeline by stage, win rate, and open pipeline by owner -- The **Pipeline Coverage** report (`pipeline_coverage_by_quarter`, whose own label reads **Pipeline Coverage by Forecast × Quarter**) — a matrix of open pipeline: forecast category down the rows, close quarter across the columns +- The **Pipeline Coverage by Quarter** report (`pipeline_coverage_by_quarter`) — a matrix of open pipeline: forecast category down the rows, close quarter across the columns Together they answer: *"do we have enough pipeline to make the quarter, and where are the risks?"* @@ -110,7 +110,7 @@ What that dashboard will not give you is a *top deals* ranking, and no renaming ## Tips for sales managers -- ✅ Run *Opportunity Funnel by Owner → Stage* monthly to catch reps with leaky funnels. +- ✅ Run *Opportunity Funnel by Owner* monthly to catch reps with leaky funnels. - ✅ Use *Pipeline Coverage* in QBRs to set the next quarter's targets. - ✅ Coach reps to close every deal with its reason — *Win Reason* on a win, *Loss Reason* on a loss, and the free-text *Loss/Win Details* on either — they are the only competitive data the app keeps. diff --git a/content/docs/sales/pipeline-management.zh-Hans.mdx b/content/docs/sales/pipeline-management.zh-Hans.mdx index 8a757fd11..91d6a7cd3 100644 --- a/content/docs/sales/pipeline-management.zh-Hans.mdx +++ b/content/docs/sales/pipeline-management.zh-Hans.mdx @@ -69,12 +69,12 @@ description: 销售管道的日常运作方式——推进交易、自动概率 ## 值得收藏的销售管道报告 -这五张都在侧边栏的 **Reports** 分组里:应用自带的每一张报表都在这个分组下,它在应用加载时是折叠的。这里用的是报表库列出的 label,它未必等于导航项上那个更短的名字。 +这五张都在侧边栏的 **Reports** 分组里:应用自带的每一张报表都在这个分组下,它在应用加载时是折叠的。这里用的是报表标题,每个导航项上写的也是同一个名字。 | 报告 | 它显示什么 | | --- | --- | -| **Pipeline Coverage by Forecast × Quarter** | 我们对即将到来的季度是否覆盖了 3 倍配额?未结管道的矩阵:行是预测类别,列是预计成交季度。它在侧边栏上的导航项写作 **Pipeline Coverage**。 | -| **Opportunity Funnel by Owner → Stage** | 每位销售代表的漏斗健康——发现漏水的阶段。 | +| **Pipeline Coverage by Quarter** | 我们对即将到来的季度是否覆盖了 3 倍配额?未结管道的矩阵:行是预测类别,列是预计成交季度。 | +| **Opportunity Funnel by Owner** | 每位销售代表的漏斗健康——发现漏水的阶段。 | | **Opportunities by Stage** | 按阶段的销售管道柱状图。 | | **Won Opportunities by Owner** | 用于季末表彰的排行榜。 | | **Customer Churn Signals** | 面向主动挽留、上下叠放的四块面板:**CSM-Flagged Accounts**(CSM 手动标记的客户)、**At-Risk Accounts**(60 天以上无活动)、**Silent High-Value Accounts**(战略级与企业级客户沉默 90 天以上)与 **Recently Lost Opportunities**(最近 30 天丢单)。 | @@ -87,7 +87,7 @@ description: 销售管道的日常运作方式——推进交易、自动概率 | --- | --- | | **每日** | 打开**商机**,切到**销售流水线**标签页。找出停滞的卡片(阶段内 > 14 天)。推进或关闭。 | | **每周** | 运行 *Pipeline Coverage*。向 AI 助手提问:*"what's at risk?"* | -| **每月** | 运行 *Opportunity Funnel by Owner → Stage*。对漏斗漏水的销售代表进行一对一辅导。 | +| **每月** | 运行 *Opportunity Funnel by Owner*。对漏斗漏水的销售代表进行一对一辅导。 | | **季末** | 运行 *Won Opportunities by Owner* 以计算提成。归档*失败*的交易并附上*丢单原因*。 | ## 预测汇总 @@ -96,7 +96,7 @@ description: 销售管道的日常运作方式——推进交易、自动概率 - **Opportunity Metrics** dataset(`opportunity_metrics`)—— 度量 **Total Amount**(金额之和),维度 **Stage** - **Sales Performance** 仪表盘(`sales_dashboard`)—— 小部件包括按阶段的销售管道、赢单率、按负责人的在飞管道 -- **Pipeline Coverage** 报告(`pipeline_coverage_by_quarter`,报表自身的 label 写作 **Pipeline Coverage by Forecast × Quarter**)—— 未结管道的矩阵:行是预测类别,列是预计成交季度 +- **Pipeline Coverage by Quarter** 报告(`pipeline_coverage_by_quarter`)—— 未结管道的矩阵:行是预测类别,列是预计成交季度 它们共同回答:*"我们是否有足够的销售管道来完成本季度,以及风险在哪里?"* @@ -110,7 +110,7 @@ description: 销售管道的日常运作方式——推进交易、自动概率 ## 给销售经理的提示 -- ✅ 每月运行 *Opportunity Funnel by Owner → Stage*,以发现漏斗漏水的销售代表。 +- ✅ 每月运行 *Opportunity Funnel by Owner*,以发现漏斗漏水的销售代表。 - ✅ 在 QBR 中使用 *Pipeline Coverage* 来设定下一季度的目标。 - ✅ 辅导销售代表在每笔成交时都填写结束原因——赢单填*赢单原因*,丢单填*丢单原因*,两种情况都可以填自由文本*赢/丢单详情*——这些是应用里仅有的竞争数据。 diff --git a/content/docs/sales/pipeline-management.zh-Hant.mdx b/content/docs/sales/pipeline-management.zh-Hant.mdx index 0658c68f4..ad4319b17 100644 --- a/content/docs/sales/pipeline-management.zh-Hant.mdx +++ b/content/docs/sales/pipeline-management.zh-Hant.mdx @@ -71,12 +71,12 @@ description: 銷售管道的日常運作方式——推進交易、自動機率 ## 值得收藏的銷售管道報告 -這五張都在側邊欄的 **Reports** 群組裡:應用自帶的每一張報表都在這個群組下,它在應用載入時是摺疊的。這裡用的是報表庫列出的 label,它未必等於導航項上那個更短的名字。 +這五張都在側邊欄的 **Reports** 群組裡:應用自帶的每一張報表都在這個群組下,它在應用載入時是摺疊的。這裡用的是報表標題,每個導航項上寫的也是同一個名字。 | 報告 | 它顯示什麼 | | --- | --- | -| **Pipeline Coverage by Forecast × Quarter** | 我們對即將到來的季度是否覆蓋了 3 倍配額?未結管道的矩陣:列是預測類別,欄是預計成交季度。它在側邊欄上的導航項寫作 **Pipeline Coverage**。 | -| **Opportunity Funnel by Owner → Stage** | 每位銷售代表的漏斗健康——發現漏水的階段。 | +| **Pipeline Coverage by Quarter** | 我們對即將到來的季度是否覆蓋了 3 倍配額?未結管道的矩陣:列是預測類別,欄是預計成交季度。 | +| **Opportunity Funnel by Owner** | 每位銷售代表的漏斗健康——發現漏水的階段。 | | **Opportunities by Stage** | 按階段的銷售管道柱狀圖。 | | **Won Opportunities by Owner** | 用於季末表彰的排行榜。 | | **Customer Churn Signals** | 面向主動挽留、上下疊放的四塊面板:**CSM-Flagged Accounts**(CSM 手動標記的客戶)、**At-Risk Accounts**(60 天以上無活動)、**Silent High-Value Accounts**(策略級與企業級客戶沉默 90 天以上)與 **Recently Lost Opportunities**(最近 30 天丟單)。 | @@ -89,7 +89,7 @@ description: 銷售管道的日常運作方式——推進交易、自動機率 | --- | --- | | **每日** | 打開 **Opportunities**,切到**銷售流水線**標籤頁。找出停滯的卡片(階段內 > 14 天)。推進或關閉。 | | **每週** | 執行 *Pipeline Coverage*。向 AI 助手提問:*"what's at risk?"* | -| **每月** | 執行 *Opportunity Funnel by Owner → Stage*。對漏斗漏水的銷售代表進行一對一輔導。 | +| **每月** | 執行 *Opportunity Funnel by Owner*。對漏斗漏水的銷售代表進行一對一輔導。 | | **季末** | 執行 *Won Opportunities by Owner* 以計算提成。歸檔*失敗*的交易並附上*丟單原因*。 | ## 預測彙總 @@ -98,7 +98,7 @@ description: 銷售管道的日常運作方式——推進交易、自動機率 - **Opportunity Metrics** dataset(`opportunity_metrics`)—— 度量 **Total Amount**(金額之和),維度 **Stage** - **Sales Performance** 儀表板(`sales_dashboard`)—— 小工具包括按階段的銷售管道、贏單率、按負責人的在飛管道 -- **Pipeline Coverage** 報告(`pipeline_coverage_by_quarter`,報表自身的 label 寫作 **Pipeline Coverage by Forecast × Quarter**)—— 未結管道的矩陣:列是預測類別,欄是預計成交季度 +- **Pipeline Coverage by Quarter** 報告(`pipeline_coverage_by_quarter`)—— 未結管道的矩陣:列是預測類別,欄是預計成交季度 它們共同回答:*"我們是否有足夠的銷售管道來完成本季度,以及風險在哪裡?"* @@ -112,7 +112,7 @@ description: 銷售管道的日常運作方式——推進交易、自動機率 ## 給銷售經理的提示 -- ✅ 每月執行 *Opportunity Funnel by Owner → Stage*,以發現漏斗漏水的銷售代表。 +- ✅ 每月執行 *Opportunity Funnel by Owner*,以發現漏斗漏水的銷售代表。 - ✅ 在 QBR 中使用 *Pipeline Coverage* 來設定下一季度的目標。 - ✅ 輔導銷售代表在每筆成交時都填寫結束原因——贏單填*贏單原因*,丟單填*丟單原因*,兩種情況都可以填自由文字*贏/丟單詳情*——這些是應用裡僅有的競爭資料。 diff --git a/content/docs/service/cases.mdx b/content/docs/service/cases.mdx index 1ead099ce..f4ce6e9c4 100644 --- a/content/docs/service/cases.mdx +++ b/content/docs/service/cases.mdx @@ -185,7 +185,7 @@ Nothing else claims a case, and each exclusion is deliberate: Of the **seven** names this section used to list, only **My Open Cases** was real — it survives in the table above. The other six are not views at all: -- **Critical Cases** is a metric tile, not a list. *Breached SLA* is not a name this app carries at all: the tile that counts breaches is **SLA Violations**. [Service Overview](/docs/analytics/dashboards) — the dashboard's own title is **Customer Service** — carries **Critical Cases** (open and Critical) and **SLA Violations** (flagged **SLA Violated**, or still open past its **SLA Due Date**, counted when the tile is read); no list view in this app filters on a breach. For a list you can actually work through, use **Escalated Cases** — the SLA sweep escalates every case it flags, so each breach it flags lands there. +- **Critical Cases** is a metric tile, not a list. *Breached SLA* is not a name this app carries at all: the tile that counts breaches is **Open Cases Past SLA**. [Service Overview](/docs/analytics/dashboards) — the dashboard's own title is **Customer Service** — carries **Critical Cases** (open and Critical) and **Open Cases Past SLA** (open, and flagged **SLA Violated** or past its **SLA Due Date**, counted when the tile is read); no list view in this app filters on a breach. For a list you can actually work through, use **Escalated Cases** — the SLA sweep escalates every case it flags, so each breach it flags lands there. - **Service Board** is not this app's name for anything. The kanban is **Service Workflow**, and that is what its tab reads. - **Cases Due Today**, **Recently Closed** and **By Account** do not exist in any form: no view filters on today's due date or on `closed_date`, and `crm_case` has no grouped view. @@ -237,7 +237,7 @@ If the Copilot decides the priority is *Critical*, it will also recommend immedi ## Tips for service managers -- ✅ There is no *Breached SLA* view to watch, and no red badge either — but a breach does show on *All Cases*: a case flagged **SLA Violated** is a red row, and it stays red once resolved or closed, because a late close records the breach. Work **Escalated Cases** daily, and read the **SLA Violations** tile on Service Overview for the count. +- ✅ There is no *Breached SLA* view to watch, and no red badge either — but a breach does show on *All Cases*: a case flagged **SLA Violated** is a red row, and it stays red once resolved or closed, because a late close records the breach. Work **Escalated Cases** daily, and read the **Open Cases Past SLA** tile on Service Overview for the count. - ✅ Use the kanban for daily standups — it is named **Service Workflow** and reaches the case list as the **Service Workflow** tab. - ✅ Coach agents whose **Resolution Time** trends up — but you will be reading it off the cases themselves. There is no agent leaderboard: `case_metrics`, the dataset behind every service report and dashboard widget, declares Status, Priority, Origin, Type, Created and Resolving Article as its only dimensions, so nothing in analytics can group cases by agent. diff --git a/content/docs/service/cases.zh-Hans.mdx b/content/docs/service/cases.zh-Hans.mdx index 0ec9af1e8..6bd2c70ef 100644 --- a/content/docs/service/cases.zh-Hans.mdx +++ b/content/docs/service/cases.zh-Hans.mdx @@ -185,7 +185,7 @@ description: 客户支持工单——主题、优先级、状态、解决方案 本节原先列的七个名字里,有六个根本不是视图: -- **Critical Cases** 是指标磁贴,不是列表。*Breached SLA* 这个名字本应用根本没有:按违约计数的是 **SLA Violations** 磁贴。[服务概览](/zh-Hans/docs/analytics/dashboards)(Service Overview;该仪表盘自身的标题是 **Customer Service**)上有 **Critical Cases**(未结且 Critical)和 **SLA Violations**(已标记 **SLA Violated**,或仍未结且 **SLA 截止日期**已过,读取磁贴时计数)两个磁贴;本应用里没有任何一个列表视图按违约过滤。真要有一份能逐条处理的清单,用 **已升级工单**——SLA 扫描会把它标记的每一条都升级,所以它标记的每次违约都会落到那里。 +- **Critical Cases** 是指标磁贴,不是列表。*Breached SLA* 这个名字本应用根本没有:按违约计数的是 **Open Cases Past SLA** 磁贴。[服务概览](/zh-Hans/docs/analytics/dashboards)(Service Overview;该仪表盘自身的标题是 **Customer Service**)上有 **Critical Cases**(未结且 Critical)和 **Open Cases Past SLA**(未结,且已标记 **SLA Violated** 或 **SLA 截止日期**已过,读取磁贴时计数)两个磁贴;本应用里没有任何一个列表视图按违约过滤。真要有一份能逐条处理的清单,用 **已升级工单**——SLA 扫描会把它标记的每一条都升级,所以它标记的每次违约都会落到那里。 - **Service Board** 不是本应用里任何东西的名字。看板叫 **服务流转**,页签上写的也是这个名字。 - **Cases Due Today**、**Recently Closed** 与 **By Account** 以任何形式都不存在:没有任何视图按「截止日期 = 今天」或 `closed_date` 过滤,`crm_case` 上也没有任何分组视图。 @@ -237,7 +237,7 @@ Activity 标签页是工单上发生过什么的唯一可信来源。它将三 ## 给服务经理的提示 -- ✅ 没有 *Breached SLA* 这个视图可看,也没有什么红色徽章——但违约在 *全部工单* 上看得见:已标记 **SLA Violated** 的工单是一行红色,解决或关闭之后依然是红的,因为逾期关闭会记下这次违约。每天看 **已升级工单**,数量则看服务概览上的 **SLA Violations** 磁贴。 +- ✅ 没有 *Breached SLA* 这个视图可看,也没有什么红色徽章——但违约在 *全部工单* 上看得见:已标记 **SLA Violated** 的工单是一行红色,解决或关闭之后依然是红的,因为逾期关闭会记下这次违约。每天看 **已升级工单**,数量则看服务概览上的 **Open Cases Past SLA** 磁贴。 - ✅ 用看板开每日站会——它叫 **服务流转**,在工单列表的 **服务流转** 页签上。 - ✅ 辅导那些 **Resolution Time** 趋势上升的客服——但这个趋势只能从工单本身读出来。不存在什么客服排行榜:`case_metrics` 这个数据集(服务侧每一张报表和每一个仪表盘部件都建在它上面)只声明了 Status、Priority、Origin、Type、Created、Resolving Article 六个维度,所以分析层里没有任何东西能按客服分组。 diff --git a/content/docs/service/cases.zh-Hant.mdx b/content/docs/service/cases.zh-Hant.mdx index ae0d4ad64..32b7989e2 100644 --- a/content/docs/service/cases.zh-Hant.mdx +++ b/content/docs/service/cases.zh-Hant.mdx @@ -187,7 +187,7 @@ description: 客戶支援工單——主題、優先順序、狀態、解決方 本節原先列的七個名字裡,有六個根本不是檢視: -- **Critical Cases** 是指標磁貼,不是列表。*Breached SLA* 這個名字本應用根本沒有:按違約計數的是 **SLA Violations** 磁貼。[服務概覽](/zh-Hant/docs/analytics/dashboards)(Service Overview;該儀表板自身的標題是 **Customer Service**)上有 **Critical Cases**(未結且 Critical)和 **SLA Violations**(已標記 **SLA Violated**,或仍未結且 **SLA 截止日期**已過,讀取磁貼時計數)兩個磁貼;本應用裡沒有任何一個列表檢視按違約過濾。真要有一份能逐條處理的清單,用 **已升級工單**——SLA 掃描會把它標記的每一條都升級,所以它標記的每次違約都會落到那裡。 +- **Critical Cases** 是指標磁貼,不是列表。*Breached SLA* 這個名字本應用根本沒有:按違約計數的是 **Open Cases Past SLA** 磁貼。[服務概覽](/zh-Hant/docs/analytics/dashboards)(Service Overview;該儀表板自身的標題是 **Customer Service**)上有 **Critical Cases**(未結且 Critical)和 **Open Cases Past SLA**(未結,且已標記 **SLA Violated** 或 **SLA 截止日期**已過,讀取磁貼時計數)兩個磁貼;本應用裡沒有任何一個列表檢視按違約過濾。真要有一份能逐條處理的清單,用 **已升級工單**——SLA 掃描會把它標記的每一條都升級,所以它標記的每次違約都會落到那裡。 - **Service Board** 不是本應用裡任何東西的名字。看板叫 **服務流轉**,頁籤上寫的也是這個名字。 - **Cases Due Today**、**Recently Closed** 與 **By Account** 以任何形式都不存在:沒有任何檢視按「截止日期 = 今天」或 `closed_date` 過濾,`crm_case` 上也沒有任何分組檢視。 @@ -239,7 +239,7 @@ Activity 標籤頁是工單上發生過什麼的唯一可信來源。它將三 ## 給服務經理的提示 -- ✅ 沒有 *Breached SLA* 這個檢視可看,也沒有什麼紅色徽章——但違約在 *全部工單* 上看得見:已標記 **SLA Violated** 的工單是一列紅色,解決或關閉之後依然是紅的,因為逾期關閉會記下這次違約。每天看 **已升級工單**,數量則看服務概覽上的 **SLA Violations** 磁貼。 +- ✅ 沒有 *Breached SLA* 這個檢視可看,也沒有什麼紅色徽章——但違約在 *全部工單* 上看得見:已標記 **SLA Violated** 的工單是一列紅色,解決或關閉之後依然是紅的,因為逾期關閉會記下這次違約。每天看 **已升級工單**,數量則看服務概覽上的 **Open Cases Past SLA** 磁貼。 - ✅ 用看板開每日站會——它叫 **服務流轉**,在工單列表的 **服務流轉** 頁籤上。 - ✅ 輔導那些 **Resolution Time** 趨勢上升的客服——但這個趨勢只能從工單本身讀出來。不存在什麼客服排行榜:`case_metrics` 這個資料集(服務側每一張報表和每一個儀表板部件都建在它上面)只宣告了 Status、Priority、Origin、Type、Created、Resolving Article 六個維度,所以分析層裡沒有任何東西能按客服分組。 diff --git a/content/docs/service/index.mdx b/content/docs/service/index.mdx index b0483bbf0..0026b873b 100644 --- a/content/docs/service/index.mdx +++ b/content/docs/service/index.mdx @@ -51,10 +51,10 @@ The [service skills](/docs/ai-copilot/service-copilot) are built into every case ## Standard dashboards & reports -- **Service Overview** (the dashboard's own title is **Customer Service**) — the KPIs **Open Cases**, **Critical Cases**, **Avg Resolution Time** and **SLA Violations**; the charts **Cases by Status**, **Cases by Priority** and **Cases by Origin**; **Daily Case Volume**; an **SLA Compliance** gauge; a **My Open Cases by Priority** table — the open cases the signed-in user owns; and the knowledge-deflection row — a **KB Deflection Rate** tile with its two halves, **Resolved by KB** and **Closed Cases**, printed beside it so the percentage can be checked rather than trusted, plus a **Top Resolving Articles** table ranking articles by the closed cases they resolved. What it does not carry is a *top agents* tile or an *oldest open cases* tile, and neither is merely a widget nobody has built yet: `case_metrics`, the dataset behind every service widget, declares no owner dimension, so nothing in analytics can rank agents — the [Cases](/docs/service/cases) page says the same — and a dashboard table aggregates its dataset instead of listing records, so the oldest individual cases cannot be shown there either. -- **Cases Opened by Priority × Day** — daily inflow, as a matrix of priority in the rows against day in the columns, in that order. +- **Service Overview** (the dashboard's own title is **Customer Service**) — the KPIs **Open Cases**, **Critical Cases**, **Avg Resolution Time** and **Open Cases Past SLA**; the charts **Cases by Status**, **Cases by Priority** and **Cases by Origin**; **Daily Case Volume**; an **SLA Met (Closed Cases)** gauge; a **My Open Cases by Priority** table — the open cases the signed-in user owns; and the knowledge-deflection row — a **KB Deflection Rate** tile with its two halves, **Resolved by KB** and **Closed Cases**, printed beside it so the percentage can be checked rather than trusted, plus a **Top Resolving Articles** table ranking articles by the closed cases they resolved. What it does not carry is a *top agents* tile or an *oldest open cases* tile, and neither is merely a widget nobody has built yet: `case_metrics`, the dataset behind every service widget, declares no owner dimension, so nothing in analytics can rank agents — the [Cases](/docs/service/cases) page says the same — and a dashboard table aggregates its dataset instead of listing records, so the oldest individual cases cannot be shown there either. +- **Cases Opened by Day** — daily inflow, as a matrix of priority in the rows against day in the columns, in that order. - **Cases by Status and Priority** — current snapshot: case count and average resolution time, by status and priority. -- **SLA Performance Report** — case count, **SLA Violation Rate** and average resolution time, by priority, over closed cases only. It does not report a compliance percentage: that is the service dashboard's **SLA Compliance** gauge, which plots `sla_compliance_rate` from the same dataset. See [SLA & Escalation](/docs/service/sla-and-escalation). +- **SLA on Closed Cases** — case count, **SLA Violation Rate** and average resolution time, by priority, over closed cases only. It does not report a compliance percentage: that is the service dashboard's **SLA Met (Closed Cases)** gauge, which plots `sla_compliance_rate` from the same dataset. See [SLA & Escalation](/docs/service/sla-and-escalation). See [Analytics › Dashboards](/docs/analytics/dashboards) and [Reports](/docs/analytics/reports). diff --git a/content/docs/service/index.zh-Hans.mdx b/content/docs/service/index.zh-Hans.mdx index 5090e49f9..f65e2c3e7 100644 --- a/content/docs/service/index.zh-Hans.mdx +++ b/content/docs/service/index.zh-Hans.mdx @@ -51,10 +51,10 @@ New ──► In Progress ──► Waiting on Customer ──► Resolved ─ ## 标准仪表盘与报表 -- **服务概览**(Service Overview;仪表盘自身的标题是 **Customer Service**)——KPI 有 **Open Cases**、**Critical Cases**、**Avg Resolution Time**、**SLA Violations**;分布图有 **Cases by Status**、**Cases by Priority**、**Cases by Origin**;再加 **Daily Case Volume**、一个 **SLA Compliance** 仪表图,一张 **My Open Cases by Priority** 表格(当前登录用户自己名下的未结工单);以及知识转移那一行——**KB Deflection Rate**(知识库转移率)配上它的分子分母 **Resolved by KB** 与 **Closed Cases**,两个数字就印在百分比旁边,让人能核对而不是只能相信,再加一张 **Top Resolving Articles** 表格,按解决的已关闭工单数给文章排名。它没有 *顶尖客服* 部件,也没有 *最久未结工单* 部件,而且这两个都不是"只是还没做"那么简单:`case_metrics`——服务侧每个部件都建在它上面的那个数据集——没有负责人维度,所以分析层里没有任何东西能给客服排名([工单](/zh-Hans/docs/service/cases)页写的是同一件事);而仪表盘表格聚合的是数据集,不列出单条记录,所以最久未结的那几张单也放不进去。 -- **Cases Opened by Priority × Day**(按优先级 × 天的建单量)——每日流入量:一张优先级作行、天作列的矩阵,顺序就是这个。 +- **服务概览**(Service Overview;仪表盘自身的标题是 **Customer Service**)——KPI 有 **Open Cases**、**Critical Cases**、**Avg Resolution Time**、**Open Cases Past SLA**;分布图有 **Cases by Status**、**Cases by Priority**、**Cases by Origin**;再加 **Daily Case Volume**、一个 **SLA Met (Closed Cases)** 仪表图,一张 **My Open Cases by Priority** 表格(当前登录用户自己名下的未结工单);以及知识转移那一行——**KB Deflection Rate**(知识库转移率)配上它的分子分母 **Resolved by KB** 与 **Closed Cases**,两个数字就印在百分比旁边,让人能核对而不是只能相信,再加一张 **Top Resolving Articles** 表格,按解决的已关闭工单数给文章排名。它没有 *顶尖客服* 部件,也没有 *最久未结工单* 部件,而且这两个都不是"只是还没做"那么简单:`case_metrics`——服务侧每个部件都建在它上面的那个数据集——没有负责人维度,所以分析层里没有任何东西能给客服排名([工单](/zh-Hans/docs/service/cases)页写的是同一件事);而仪表盘表格聚合的是数据集,不列出单条记录,所以最久未结的那几张单也放不进去。 +- **Cases Opened by Day**(按优先级 × 天的建单量)——每日流入量:一张优先级作行、天作列的矩阵,顺序就是这个。 - **Cases by Status and Priority**(按状态与优先级划分的工单)——当前快照:按状态与优先级给出工单数与平均解决时长。 -- **SLA Performance Report**(SLA 表现报表)——按优先级给出工单数、**SLA Violation Rate**(SLA 违约率)与平均解决时长,并且只统计已关闭的工单。它报的不是达成率百分比:达成率在服务仪表盘的 **SLA Compliance** 仪表图上,绘制的是同一个数据集里的 `sla_compliance_rate`。参见 [SLA 与升级](/zh-Hans/docs/service/sla-and-escalation)。 +- **SLA on Closed Cases**(已关闭工单的 SLA 报表)——按优先级给出工单数、**SLA Violation Rate**(SLA 违约率)与平均解决时长,并且只统计已关闭的工单。它报的不是达成率百分比:达成率在服务仪表盘的 **SLA Met (Closed Cases)** 仪表图上,绘制的是同一个数据集里的 `sla_compliance_rate`。参见 [SLA 与升级](/zh-Hans/docs/service/sla-and-escalation)。 参见 [分析 › 仪表盘](/zh-Hans/docs/analytics/dashboards) 与 [报表](/zh-Hans/docs/analytics/reports)。 diff --git a/content/docs/service/index.zh-Hant.mdx b/content/docs/service/index.zh-Hant.mdx index 74df7f756..a401b32f5 100644 --- a/content/docs/service/index.zh-Hant.mdx +++ b/content/docs/service/index.zh-Hant.mdx @@ -53,10 +53,10 @@ New ──► In Progress ──► Waiting on Customer ──► Resolved ─ ## 標準儀表板與報表 -- **服務概覽**(Service Overview;儀表板自身的標題是 **Customer Service**)——KPI 有 **Open Cases**、**Critical Cases**、**Avg Resolution Time**、**SLA Violations**;分布圖有 **Cases by Status**、**Cases by Priority**、**Cases by Origin**;再加 **Daily Case Volume**、一個 **SLA Compliance** 量表圖,一張 **My Open Cases by Priority** 表格(目前登入使用者自己名下的未結工單);以及知識轉移那一列——**KB Deflection Rate**(知識庫轉移率)配上它的分子分母 **Resolved by KB** 與 **Closed Cases**,兩個數字就印在百分比旁邊,讓人能核對而不是只能相信,再加一張 **Top Resolving Articles** 表格,按解決的已關閉工單數給文章排名。它沒有 *頂尖客服* 部件,也沒有 *最久未結工單* 部件,而且這兩個都不是"只是還沒做"那麼簡單:`case_metrics`——服務側每個部件都建在它上面的那個資料集——沒有負責人維度,所以分析層裡沒有任何東西能給客服排名([工單](/zh-Hant/docs/service/cases)頁寫的是同一件事);而儀表板表格聚合的是資料集,不列出單筆記錄,所以最久未結的那幾張單也放不進去。 -- **Cases Opened by Priority × Day**(按優先順序 × 天的建單量)——每日流入量:一張優先順序作列、天作欄的矩陣,順序就是這個。 +- **服務概覽**(Service Overview;儀表板自身的標題是 **Customer Service**)——KPI 有 **Open Cases**、**Critical Cases**、**Avg Resolution Time**、**Open Cases Past SLA**;分布圖有 **Cases by Status**、**Cases by Priority**、**Cases by Origin**;再加 **Daily Case Volume**、一個 **SLA Met (Closed Cases)** 量表圖,一張 **My Open Cases by Priority** 表格(目前登入使用者自己名下的未結工單);以及知識轉移那一列——**KB Deflection Rate**(知識庫轉移率)配上它的分子分母 **Resolved by KB** 與 **Closed Cases**,兩個數字就印在百分比旁邊,讓人能核對而不是只能相信,再加一張 **Top Resolving Articles** 表格,按解決的已關閉工單數給文章排名。它沒有 *頂尖客服* 部件,也沒有 *最久未結工單* 部件,而且這兩個都不是"只是還沒做"那麼簡單:`case_metrics`——服務側每個部件都建在它上面的那個資料集——沒有負責人維度,所以分析層裡沒有任何東西能給客服排名([工單](/zh-Hant/docs/service/cases)頁寫的是同一件事);而儀表板表格聚合的是資料集,不列出單筆記錄,所以最久未結的那幾張單也放不進去。 +- **Cases Opened by Day**(按優先順序 × 天的建單量)——每日流入量:一張優先順序作列、天作欄的矩陣,順序就是這個。 - **Cases by Status and Priority**(按狀態與優先順序劃分的工單)——當前快照:按狀態與優先順序給出工單數與平均解決時長。 -- **SLA Performance Report**(SLA 表現報表)——按優先順序給出工單數、**SLA Violation Rate**(SLA 違約率)與平均解決時長,並且只統計已關閉的工單。它報的不是達成率百分比:達成率在服務儀表板的 **SLA Compliance** 量表圖上,繪製的是同一個資料集裡的 `sla_compliance_rate`。參見 [SLA 與升級](/zh-Hant/docs/service/sla-and-escalation)。 +- **SLA on Closed Cases**(已關閉工單的 SLA 報表)——按優先順序給出工單數、**SLA Violation Rate**(SLA 違約率)與平均解決時長,並且只統計已關閉的工單。它報的不是達成率百分比:達成率在服務儀表板的 **SLA Met (Closed Cases)** 量表圖上,繪製的是同一個資料集裡的 `sla_compliance_rate`。參見 [SLA 與升級](/zh-Hant/docs/service/sla-and-escalation)。 參見 [分析 › 儀表板](/zh-Hant/docs/analytics/dashboards) 與 [報表](/zh-Hant/docs/analytics/reports)。 diff --git a/content/docs/service/sla-and-escalation.mdx b/content/docs/service/sla-and-escalation.mdx index d2d4c5aa7..ead52e09a 100644 --- a/content/docs/service/sla-and-escalation.mdx +++ b/content/docs/service/sla-and-escalation.mdx @@ -39,7 +39,7 @@ The matrix lives in the hook rather than on a Setup screen — see the *Tips for Two fields on every case: - **SLA Due Date** (`sla_due_date`) — when the case must be resolved by. Stamped on every case with a recognised priority, from the matrix above. **Raising the priority re-derives it**: the case gets *now + the new priority's hours*, so a Medium case escalated to Critical is due four hours after the escalation, not 36 hours after it was opened. The new window runs from the change, not from the case's creation — counted from creation, a case escalated late would be past due the moment it was escalated — and it only ever brings the deadline **in**: when the date the case already has is sooner (a case raised in the last hours of its old window, or one already past due), that date stays, breach and all. **Lowering** the priority does not move the deadline — the app never extends one by itself — and neither does any other edit. A due date somebody set by hand (a manager can renegotiate one) is kept by every other edit, and replaced only by a priority raise that brings a sooner one. -- **SLA Violated** (`is_sla_violated`) — there is no field called *SLA Breached?*; the checkbox agents see is labelled **SLA Violated**. It is set to `true` in two places, and both apply one rule: the **SLA Due Date** passed before the case stopped being open work. While a case is still open — status is neither *Resolved* nor *Closed* — the hourly `case_sla_monitor` sweep sets it once the due date is in the past. And the save that resolves or closes a case sets it when that moment is after the due date, so a case finished late is flagged even if no sweep ever looked at it. A case with no due date is never flagged, a case resolved on time is not flagged when it is closed later, and nothing clears the flag once it is set. Flagging a finished case escalates nothing and alerts nobody — the work is done; the flag is what the **SLA Performance** report and the **SLA Compliance** gauge count. +- **SLA Violated** (`is_sla_violated`) — there is no field called *SLA Breached?*; the checkbox agents see is labelled **SLA Violated**. It is set to `true` in two places, and both apply one rule: the **SLA Due Date** passed before the case stopped being open work. While a case is still open — status is neither *Resolved* nor *Closed* — the hourly `case_sla_monitor` sweep sets it once the due date is in the past. And the save that resolves or closes a case sets it when that moment is after the due date, so a case finished late is flagged even if no sweep ever looked at it. A case with no due date is never flagged, a case resolved on time is not flagged when it is closed later, and nothing clears the flag once it is set. Flagging a finished case escalates nothing and alerts nobody — the work is done; the flag is what the **SLA on Closed Cases** report and the **SLA Met (Closed Cases)** gauge count. The breach flag is **read-only for agents** — they can't fudge it. Nobody types the value: `case_sla_monitor` writes it as the system, and the `case_sla_defaults` hook writes it onto the agent's own resolving or closing save as a value the hook derives, not one the agent sent — so the mask below does not refuse that save. The lock is field-level security rather than a read-only field: the Service Agent profile masks `crm_case.is_sla_violated` as readable but not editable, which is why an agent sees the checkbox on the case — on the form's **SLA** tab and in the detail page's **Status & SLA** section — but cannot change it. The shipped masks are listed under [Profiles › Field-level security](/docs/administration/profiles#field-level-security). @@ -52,13 +52,13 @@ Two views help with that, neither of them a timer: ## SLA performance reporting -The **SLA Performance** report breaks its numbers down by **one** dimension: **priority**. Its `rows` are `['priority']` and nothing else (`src/service/reports/case.report.ts`). What it reports is not a compliance percentage either, but the **SLA Violation Rate** — the average of the **SLA Violated** flag — shown next to the case count and the average resolution time, and computed over **closed cases only** (`runtimeFilter: { is_closed: true }`). (The on-time percentage itself is declared: **SLA Compliance Rate**, which the Service Overview dashboard's **SLA Compliance** gauge plots. This report simply does not carry it.) Of the four questions this page used to promise, it answers one: *which priority bucket is leaking?* +The **SLA on Closed Cases** report breaks its numbers down by **one** dimension: **priority**. Its `rows` are `['priority']` and nothing else (`src/service/reports/case.report.ts`). What it reports is not a compliance percentage either, but the **SLA Violation Rate** — the average of the **SLA Violated** flag — shown next to the case count and the average resolution time, and computed over **closed cases only** (`runtimeFilter: { is_closed: true }`). (The on-time percentage itself is declared: **SLA Compliance Rate**, which the Service Overview dashboard's **SLA Met (Closed Cases)** gauge plots. This report simply does not carry it.) Of the four questions this page used to promise, it answers one: *which priority bucket is leaking?* The other three are not merely missing from the report — they are unreachable in the semantic layer underneath it. The `case_metrics` dataset (`src/service/datasets/case.dataset.ts`) declares six dimensions — **Status**, **Priority**, **Origin**, **Type**, **Created** and **Resolving Article** — so: - **Agent** — there is no owner or agent dimension. `owner_id` is a field on the case, but the dataset does not expose it, so no report and no dashboard widget can group by it. - **Account tier** — `crm_account.tier` exists on the account object, but `case_metrics` reads `crm_case` alone and never crosses over to the account, so tier is not selectable anywhere in analytics. -- **Month** — **Created** buckets by **day** (`dateGranularity: 'day'`), not by month, and the SLA Performance report does not put it in `rows` at all. The report that does use it is **Cases Opened by Priority × Day**, a matrix of priority against day. +- **Month** — **Created** buckets by **day** (`dateGranularity: 'day'`), not by month, and the SLA on Closed Cases report does not put it in `rows` at all. The report that does use it is **Cases Opened by Day**, a matrix of priority against day. Any of the three would mean changing `case_metrics` first; no setting on the report itself can bring them back. @@ -141,11 +141,11 @@ If priority resolves to **Critical**, the Copilot immediately recommends escalat | Role | What to check | When | | --- | --- | --- | | **Agent** | My Open Cases — a priority queue, not a deadline queue | Hourly during your shift | -| **Service Manager** | Escalated Cases and SLA at Risk, plus Service Overview's **SLA Violations** tile — that dashboard's own title is **Customer Service** | First thing every morning | -| **Service Director** | SLA Performance report | Weekly | +| **Service Manager** | Escalated Cases and SLA at Risk, plus Service Overview's **Open Cases Past SLA** tile — that dashboard's own title is **Customer Service** | First thing every morning | +| **Service Director** | SLA on Closed Cases report | Weekly | | **Executive** | Service Overview | Weekly | -Two names this table used to carry are not views at all. **Breached SLA** and **Critical Cases** do not exist as list views: `crm_case` ships eight — *All Cases*, *Service Workflow*, *SLA Calendar*, *Case Timeline*, *My Open Cases*, *Unassigned — triage*, *Escalated Cases* and *⏰ SLA at Risk* (`src/service/views/case.view.ts`) — and not one of them filters on **SLA Violated**. The only two surfaces that do filter on a breach are metric tiles on [Service Overview](/docs/analytics/dashboards): **SLA Violations** (flagged **SLA Violated**, or still open with its **SLA Due Date** passed — counted when the tile is read, so it is right with or without the sweep, which is still what escalates them) and **Critical Cases** (open and Critical). For a list you can actually work through, **Escalated Cases** is the closest thing: `case_sla_monitor` escalates every case it flags as breached, so each breach found on an open case lands in it — mixed in with the cases escalated on priority alone. (A case flagged on the save that resolved or closed it late is finished work: the **SLA Violations** tile counts it, and there is nothing left in it to work.) +Two names this table used to carry are not views at all. **Breached SLA** and **Critical Cases** do not exist as list views: `crm_case` ships eight — *All Cases*, *Service Workflow*, *SLA Calendar*, *Case Timeline*, *My Open Cases*, *Unassigned — triage*, *Escalated Cases* and *⏰ SLA at Risk* (`src/service/views/case.view.ts`) — and not one of them filters on **SLA Violated**. The only two surfaces that do filter on a breach are metric tiles on [Service Overview](/docs/analytics/dashboards): **Open Cases Past SLA** (open, and flagged **SLA Violated** or past its **SLA Due Date** — counted when the tile is read, so it is right with or without the sweep, which is still what escalates them) and **Critical Cases** (open and Critical). For a list you can actually work through, **Escalated Cases** is the closest thing: `case_sla_monitor` escalates every case it flags as breached, so each breach found on an open case lands in it — mixed in with the cases escalated on priority alone. (A case flagged on the save that resolved or closed it late is finished work: it leaves the **Open Cases Past SLA** tile and is counted where finished work is, in the **SLA Met (Closed Cases)** gauge and the **SLA on Closed Cases** report.) **My Open Cases is sorted by priority, not by SLA Due Date.** Its keys are `priority_rank` descending first, and `sla_due_date` ascending only as a tie-breaker (`src/service/views/case.view.ts`) — so a Low case due within the hour still sits below every Critical one. The tie-breaker does now have values to work with at every priority, so within a band the soonest deadline comes first. Keep **SLA at Risk** or **SLA Calendar** open next to it if you are working to deadlines. @@ -157,7 +157,7 @@ Two names this table used to carry are not views at all. **Breached SLA** and ** ## Tips for service managers -- ✅ There is no **Breached SLA** list view to run. Work the **Escalated Cases** view instead — every case the SLA sweep flags gets escalated into it — and read the **SLA Violations** tile on Service Overview for the count. +- ✅ There is no **Breached SLA** list view to run. Work the **Escalated Cases** view instead — every case the SLA sweep flags gets escalated into it — and read the **Open Cases Past SLA** tile on Service Overview for the count. - ✅ Use the kanban board for daily standups — it is named **Service Workflow** (`case_workflow`) and reaches the case list as the **Service Workflow** tab. Nothing in this app is called *Service Board*. - ✅ Coach agents whose breach rate trends up — surface it 1:1 before it becomes a pattern. diff --git a/content/docs/service/sla-and-escalation.zh-Hans.mdx b/content/docs/service/sla-and-escalation.zh-Hans.mdx index 7513a837d..3329d76b7 100644 --- a/content/docs/service/sla-and-escalation.zh-Hans.mdx +++ b/content/docs/service/sla-and-escalation.zh-Hans.mdx @@ -39,7 +39,7 @@ SLA(服务级别协议)系统为**每一张工单**打上一个**截止时 每个工单上有两个字段: - **SLA 截止日期**(`sla_due_date`)——工单必须在何时之前解决。凡是优先级能在矩阵里查到的工单都会被打戳。**调高优先级会重新推导它**:工单得到*当前时间 + 新优先级的小时数*,所以一张升级为 Critical 的 Medium 工单,截止时间是升级之后四小时,而不是建单之后 36 小时。新的时限从改动那一刻起算,而不是从建单起算——若从建单起算,一张升级得晚的工单在升级的那一刻就已经逾期——而且它只会把期限往**前**拉:如果工单现有的日期更早(在旧时限的最后几小时里才调高的工单,或者已经逾期的工单),就保留那个日期,违约也随之保留。**调低**优先级不会移动截止时间——应用从不自行把期限往后推——任何其他编辑也不会。有人手工设定的截止日期(经理可以重新商定一个)在其他任何编辑下都保持不变,只有带来更早日期的优先级调高才会替换它。 -- **SLA 已违约**(`is_sla_violated`)——并不存在一个叫 *SLA 已违约?* 的字段;客服看到的那个勾选框,标签就是 **SLA 已违约**。它会在两处被置为 `true`,两处用的是同一条规则:**SLA 截止日期**在工单结束之前就已经过去。工单仍然打开(状态既不是 *Resolved* 也不是 *Closed*)时,每小时一轮的 `case_sla_monitor` 扫描会在截止日期过去之后把它置位;把工单解决或关闭的那一次保存,则在那一刻已晚于截止日期时把它置位——所以超时才结束的工单,即使从没被哪一轮扫描看到过,也会被标记。没有截止日期的工单永远不会被标记;按时解决、之后才关闭的工单,关闭时也不会被标记;标记一旦置上,就不会再被清除。给已经结束的工单置位既不会升级它,也不会提醒任何人——工作已经做完,这个标记是给 **SLA Performance Report** 与 **SLA Compliance** 磁贴计数用的。 +- **SLA 已违约**(`is_sla_violated`)——并不存在一个叫 *SLA 已违约?* 的字段;客服看到的那个勾选框,标签就是 **SLA 已违约**。它会在两处被置为 `true`,两处用的是同一条规则:**SLA 截止日期**在工单结束之前就已经过去。工单仍然打开(状态既不是 *Resolved* 也不是 *Closed*)时,每小时一轮的 `case_sla_monitor` 扫描会在截止日期过去之后把它置位;把工单解决或关闭的那一次保存,则在那一刻已晚于截止日期时把它置位——所以超时才结束的工单,即使从没被哪一轮扫描看到过,也会被标记。没有截止日期的工单永远不会被标记;按时解决、之后才关闭的工单,关闭时也不会被标记;标记一旦置上,就不会再被清除。给已经结束的工单置位既不会升级它,也不会提醒任何人——工作已经做完,这个标记是给 **SLA on Closed Cases** 与 **SLA Met (Closed Cases)** 磁贴计数用的。 违约标记对客服是**只读的**——他们无法篡改。这个值从来不由人填写:`case_sla_monitor` 以系统身份写入它;`case_sla_defaults` 钩子则把它写进客服自己那次解决或关闭的保存里——那是钩子推导出的值,不是客服提交的值,所以下面这道遮罩不会因此拒绝那次保存。不过这道门是字段级安全,不是字段上的 readonly:服务专员权限档案把 `crm_case.is_sla_violated` 遮罩成可读不可编辑,所以客服能在工单上看到这个勾选框(表单的 **SLA** 页签、详情页的 **Status & SLA** 分区),却改不动它。随产品提供的遮罩清单见[权限档案](/zh-Hans/docs/administration/profiles)。 @@ -50,15 +50,15 @@ SLA(服务级别协议)系统为**每一张工单**打上一个**截止时 - **SLA 日历** 按 **SLA 截止日期**把未结工单铺在日历上:每张工单都带截止日期,已解决和已关闭的工单不显示,所以它显示的是未结积压的截止日。 - **SLA 风险预警** 列出打开的 **High 与 Critical** 工单,按截止日期升序排列。它筛的是优先级,不是"离截止还有多近"——所以它是一个恰好按期限排序的优先级队列,Medium 与 Low 工单无论离违约多近都不会出现在里面。 -## SLA 表现报表 +## SLA 达标报表 -**SLA 表现**报表只按**一个**维度细分:**优先级**。它的 `rows` 就是 `['priority']`,没有别的(`src/service/reports/case.report.ts`)。而且它报的也不是"在目标内解决的百分比",而是 **SLA 违约率**——**SLA 已违约**标记的平均值——与工单数、平均解决时长并列,并且只统计**已关闭的工单**(`runtimeFilter: { is_closed: true }`)。(准时率本身是有声明的:**SLA Compliance Rate**,服务概览上的 **SLA Compliance** 仪表盘图画的就是它。只是这份报表没有带上它。)所以本页过去许诺的四个问题,它只回答其中一个:*哪个优先级桶在漏单?* +**已关闭工单的 SLA**(SLA on Closed Cases)报表只按**一个**维度细分:**优先级**。它的 `rows` 就是 `['priority']`,没有别的(`src/service/reports/case.report.ts`)。而且它报的也不是"在目标内解决的百分比",而是 **SLA 违约率**——**SLA 已违约**标记的平均值——与工单数、平均解决时长并列,并且只统计**已关闭的工单**(`runtimeFilter: { is_closed: true }`)。(准时率本身是有声明的:**SLA Compliance Rate**,服务概览上的 **SLA Met (Closed Cases)** 仪表盘图画的就是它。只是这份报表没有带上它。)所以本页过去许诺的四个问题,它只回答其中一个:*哪个优先级桶在漏单?* 另外三个不只是"报表里没放",而是在它底下的语义层里根本够不着。`case_metrics` 数据集(`src/service/datasets/case.dataset.ts`)总共声明了六个维度——**状态**、**优先级**、**来源**、**类型**、**创建时间**、**解决所用文章**——因此: - **客服**——不存在负责人或客服维度。`owner_id` 是工单上的字段,但数据集没有把它暴露成维度,所以没有任何报表、也没有任何仪表盘部件能按它分组。 - **账户等级**——`crm_account.tier` 确实存在于账户对象上,但 `case_metrics` 只读 `crm_case`,从不跨到账户上去取它,所以分析侧任何地方都选不到账户等级。 -- **月份**——**创建时间**这个维度按**天**分桶(`dateGranularity: 'day'`),不是按月;而且 SLA 表现报表压根没把它放进 `rows`。真正用到它的是**按优先级 × 天的建单量**报表,那是一张优先级对天的矩阵。 +- **月份**——**创建时间**这个维度按**天**分桶(`dateGranularity: 'day'`),不是按月;而且这份报表压根没把它放进 `rows`。真正用到它的是**按优先级 × 天的建单量**报表,那是一张优先级对天的矩阵。 想要这三个里的任何一个,都得先改 `case_metrics`;报表本身没有任何设置能把它们变出来。 @@ -141,11 +141,11 @@ SLA(服务级别协议)系统为**每一张工单**打上一个**截止时 | 角色 | 查看什么 | 何时 | | --- | --- | --- | | **客服** | 我的待处理工单——一个按优先级排的队列,不是按截止时间排的队列 | 当班期间每小时 | -| **服务经理** | 已升级工单与 SLA 风险预警 两个视图,外加服务概览(Service Overview)上的 **SLA Violations** 磁贴——该仪表盘自身的标题是 **Customer Service** | 每天一早 | -| **服务总监** | SLA 表现报表 | 每周 | +| **服务经理** | 已升级工单与 SLA 风险预警 两个视图,外加服务概览(Service Overview)上的 **Open Cases Past SLA** 磁贴——该仪表盘自身的标题是 **Customer Service** | 每天一早 | +| **服务总监** | SLA on Closed Cases 报表 | 每周 | | **高管** | 服务概览 | 每周 | -这张表过去写的两个名字根本不是视图。**Breached SLA** 与 **Critical Cases** 都不存在于列表视图里:`crm_case` 随产品提供的视图一共八个——*全部工单*、*服务流转*、*SLA 日历*、*工单时间线*、*我的待处理工单*、*未分派 — 待分诊*、*已升级工单* 与 *⏰ SLA 风险预警*(`src/service/views/case.view.ts`)——其中没有任何一个以 **SLA 已违约**为过滤条件。真正按违约过滤的只有[服务概览](/zh-Hans/docs/analytics/dashboards)上的两个指标磁贴:**SLA Violations**(已标记 **SLA 已违约**,或仍未结且 **SLA 截止日期**已过——读取磁贴时才计数,所以有没有扫描它都是准的;升级这些工单的仍然是扫描)与 **Critical Cases**(未结 + Critical)。如果你要的是一份能逐条处理的列表,**已升级工单** 是最接近的那个:`case_sla_monitor` 会把它标记为违约的每一个工单都升级掉,所以工单仍然打开时发现的每一次违约都会落进这个视图——只是会和那些仅因优先级而升级的工单混在一起。(在解决或关闭的那次保存里因超时而被标记的工单已经结束:**SLA Violations** 磁贴会把它计入,但它已经没有什么可再处理的了。) +这张表过去写的两个名字根本不是视图。**Breached SLA** 与 **Critical Cases** 都不存在于列表视图里:`crm_case` 随产品提供的视图一共八个——*全部工单*、*服务流转*、*SLA 日历*、*工单时间线*、*我的待处理工单*、*未分派 — 待分诊*、*已升级工单* 与 *⏰ SLA 风险预警*(`src/service/views/case.view.ts`)——其中没有任何一个以 **SLA 已违约**为过滤条件。真正按违约过滤的只有[服务概览](/zh-Hans/docs/analytics/dashboards)上的两个指标磁贴:**Open Cases Past SLA**(未结,且已标记 **SLA 已违约** 或 **SLA 截止日期**已过——读取磁贴时才计数,所以有没有扫描它都是准的;升级这些工单的仍然是扫描)与 **Critical Cases**(未结 + Critical)。如果你要的是一份能逐条处理的列表,**已升级工单** 是最接近的那个:`case_sla_monitor` 会把它标记为违约的每一个工单都升级掉,所以工单仍然打开时发现的每一次违约都会落进这个视图——只是会和那些仅因优先级而升级的工单混在一起。(在解决或关闭的那次保存里因超时而被标记的工单已经结束:它离开 **Open Cases Past SLA** 磁贴,计入已结束工作所在的地方——**SLA Met (Closed Cases)** 仪表图和 **SLA on Closed Cases** 报表。) **我的待处理工单是按优先级排序的,不是按 SLA 截止日期。** 它的排序键是 `priority_rank` 降序在先、`sla_due_date` 升序仅作次级键(`src/service/views/case.view.ts`)——所以一个一小时后就到期的 Low 工单,仍然排在每一个 Critical 工单下面。不过次级键现在在每一档里都有值可排了,同一档内最早到期的排在最前面。如果你是按截止时间在干活,请在它旁边同时开着 **SLA 风险预警** 或 **SLA 日历**。 @@ -157,7 +157,7 @@ SLA(服务级别协议)系统为**每一张工单**打上一个**截止时 ## 给服务经理的提示 -- ✅ 没有 **Breached SLA** 这个列表视图可以运行。改去处理 **已升级工单** 视图——SLA 扫描标记的每一个工单都会被升级进这个视图——数量则看服务概览上的 **SLA Violations** 磁贴。 +- ✅ 没有 **Breached SLA** 这个列表视图可以运行。改去处理 **已升级工单** 视图——SLA 扫描标记的每一个工单都会被升级进这个视图——数量则看服务概览上的 **Open Cases Past SLA** 磁贴。 - ✅ 每日站会用那块看板,但它叫 **服务流转**(`case_workflow`),在工单列表上以 **服务流转** 页签出现。本应用里不存在叫 *Service Board* 的东西。 - ✅ 辅导那些违约率趋升的客服——在它形成模式之前于 1:1 中提出。 diff --git a/content/docs/service/sla-and-escalation.zh-Hant.mdx b/content/docs/service/sla-and-escalation.zh-Hant.mdx index ed7415d9f..41b92ce4a 100644 --- a/content/docs/service/sla-and-escalation.zh-Hant.mdx +++ b/content/docs/service/sla-and-escalation.zh-Hant.mdx @@ -41,7 +41,7 @@ SLA(服務級別協議)系統為**每一張工單**打上一個**截止時 每個工單上有兩個欄位: - **SLA 截止日期**(`sla_due_date`)——工單必須在何時之前解決。凡是優先順序能在矩陣裡查到的工單都會被打戳。**調高優先順序會重新推導它**:工單得到*目前時間 + 新優先順序的小時數*,所以一張升級為 Critical 的 Medium 工單,截止時間是升級之後四小時,而不是建單之後 36 小時。新的時限從改動那一刻起算,而不是從建單起算——若從建單起算,一張升級得晚的工單在升級的那一刻就已經逾期——而且它只會把期限往**前**拉:如果工單現有的日期更早(在舊時限的最後幾小時裡才調高的工單,或者已經逾期的工單),就保留那個日期,違約也隨之保留。**調低**優先順序不會移動截止時間——應用從不自行把期限往後推——任何其他編輯也不會。有人手工設定的截止日期(經理可以重新商定一個)在其他任何編輯下都保持不變,只有帶來更早日期的優先順序調高才會替換它。 -- **SLA 已違約**(`is_sla_violated`)——並不存在一個叫 *SLA 已違約?* 的欄位;客服看到的那個勾選框,標籤就是 **SLA 已違約**。它會在兩處被置為 `true`,兩處用的是同一條規則:**SLA 截止日期**在工單結束之前就已經過去。工單仍然打開(狀態既不是 *Resolved* 也不是 *Closed*)時,每小時一輪的 `case_sla_monitor` 掃描會在截止日期過去之後把它置位;把工單解決或關閉的那一次儲存,則在那一刻已晚於截止日期時把它置位——所以超時才結束的工單,即使從沒被哪一輪掃描看到過,也會被標記。沒有截止日期的工單永遠不會被標記;按時解決、之後才關閉的工單,關閉時也不會被標記;標記一旦置上,就不會再被清除。給已經結束的工單置位既不會升級它,也不會提醒任何人——工作已經做完,這個標記是給 **SLA Performance Report** 與 **SLA Compliance** 磁貼計數用的。 +- **SLA 已違約**(`is_sla_violated`)——並不存在一個叫 *SLA 已違約?* 的欄位;客服看到的那個勾選框,標籤就是 **SLA 已違約**。它會在兩處被置為 `true`,兩處用的是同一條規則:**SLA 截止日期**在工單結束之前就已經過去。工單仍然打開(狀態既不是 *Resolved* 也不是 *Closed*)時,每小時一輪的 `case_sla_monitor` 掃描會在截止日期過去之後把它置位;把工單解決或關閉的那一次儲存,則在那一刻已晚於截止日期時把它置位——所以超時才結束的工單,即使從沒被哪一輪掃描看到過,也會被標記。沒有截止日期的工單永遠不會被標記;按時解決、之後才關閉的工單,關閉時也不會被標記;標記一旦置上,就不會再被清除。給已經結束的工單置位既不會升級它,也不會提醒任何人——工作已經做完,這個標記是給 **SLA on Closed Cases** 與 **SLA Met (Closed Cases)** 磁貼計數用的。 違約標記對客服是**唯讀的**——他們無法篡改。這個值從來不由人填寫:`case_sla_monitor` 以系統身分寫入它;`case_sla_defaults` 鉤子則把它寫進客服自己那次解決或關閉的儲存裡——那是鉤子推導出的值,不是客服提交的值,所以下面這道遮罩不會因此拒絕那次儲存。不過這道門是欄位層級安全,不是欄位上的 readonly:服務專員權限設定檔把 `crm_case.is_sla_violated` 遮罩成可讀不可編輯,所以客服能在工單上看到這個勾選框(表單的 **SLA** 頁籤、詳情頁的 **Status & SLA** 分區),卻改不動它。隨產品提供的遮罩清單見[權限設定檔](/zh-Hant/docs/administration/profiles)。 @@ -52,15 +52,15 @@ SLA(服務級別協議)系統為**每一張工單**打上一個**截止時 - **SLA Calendar** 按 **SLA 截止日期**把未結工單鋪在日曆上:每張工單都帶截止日期,已解決和已關閉的工單不顯示,所以它顯示的是未結積壓的截止日。 - **SLA at Risk** 列出打開的 **High 與 Critical** 工單,按截止日期升序排列。它篩的是優先順序,不是"離截止還有多近"——所以它是一個恰好按期限排序的優先順序佇列,Medium 與 Low 工單無論離違約多近都不會出現在裡面。 -## SLA 表現報表 +## SLA 達標報表 -**SLA 表現**報表只按**一個**維度細分:**優先順序**。它的 `rows` 就是 `['priority']`,沒有別的(`src/service/reports/case.report.ts`)。而且它報的也不是"在目標內解決的百分比",而是 **SLA 違約率**——**SLA 已違約**標記的平均值——與工單數、平均解決時長並列,並且只統計**已關閉的工單**(`runtimeFilter: { is_closed: true }`)。(準時率本身是有宣告的:**SLA Compliance Rate**,服務概覽上的 **SLA Compliance** 儀表圖畫的就是它。只是這份報表沒有帶上它。)所以本頁過去許諾的四個問題,它只回答其中一個:*哪個優先順序桶在漏單?* +**已關閉工單的 SLA**(SLA on Closed Cases)報表只按**一個**維度細分:**優先順序**。它的 `rows` 就是 `['priority']`,沒有別的(`src/service/reports/case.report.ts`)。而且它報的也不是"在目標內解決的百分比",而是 **SLA 違約率**——**SLA 已違約**標記的平均值——與工單數、平均解決時長並列,並且只統計**已關閉的工單**(`runtimeFilter: { is_closed: true }`)。(準時率本身是有宣告的:**SLA Compliance Rate**,服務概覽上的 **SLA Met (Closed Cases)** 儀表圖畫的就是它。只是這份報表沒有帶上它。)所以本頁過去許諾的四個問題,它只回答其中一個:*哪個優先順序桶在漏單?* 另外三個不只是"報表裡沒放",而是在它底下的語意層裡根本搆不著。`case_metrics` 資料集(`src/service/datasets/case.dataset.ts`)總共宣告了六個維度——**狀態**、**優先順序**、**來源**、**類型**、**建立時間**、**解決所用文章**——因此: - **客服**——不存在負責人或客服維度。`owner_id` 是工單上的欄位,但資料集沒有把它暴露成維度,所以沒有任何報表、也沒有任何儀表板部件能按它分組。 - **帳戶等級**——`crm_account.tier` 確實存在於帳戶物件上,但 `case_metrics` 只讀 `crm_case`,從不跨到帳戶上去取它,所以分析側任何地方都選不到帳戶等級。 -- **月份**——**建立時間**這個維度按**天**分桶(`dateGranularity: 'day'`),不是按月;而且 SLA 表現報表壓根沒把它放進 `rows`。真正用到它的是**按優先順序 × 天的建單量**報表,那是一張優先順序對天的矩陣。 +- **月份**——**建立時間**這個維度按**天**分桶(`dateGranularity: 'day'`),不是按月;而且這份報表壓根沒把它放進 `rows`。真正用到它的是**按優先順序 × 天的建單量**報表,那是一張優先順序對天的矩陣。 想要這三個裡的任何一個,都得先改 `case_metrics`;報表本身沒有任何設定能把它們變出來。 @@ -143,11 +143,11 @@ SLA(服務級別協議)系統為**每一張工單**打上一個**截止時 | 角色 | 查看什麼 | 何時 | | --- | --- | --- | | **客服** | My Open Cases——一個按優先順序排的佇列,不是按截止時間排的佇列 | 當班期間每小時 | -| **服務經理** | Escalated Cases 與 SLA at Risk 兩個檢視,外加服務概覽(Service Overview)上的 **SLA Violations** 磁貼——該儀表板自身的標題是 **Customer Service** | 每天一早 | -| **服務總監** | SLA 表現報表 | 每週 | +| **服務經理** | Escalated Cases 與 SLA at Risk 兩個檢視,外加服務概覽(Service Overview)上的 **Open Cases Past SLA** 磁貼——該儀表板自身的標題是 **Customer Service** | 每天一早 | +| **服務總監** | SLA on Closed Cases 報表 | 每週 | | **高管** | 服務概覽 | 每週 | -這張表過去寫的兩個名字根本不是檢視。**Breached SLA** 與 **Critical Cases** 都不存在於列表檢視裡:`crm_case` 隨產品提供的檢視一共八個——*All Cases*、*Service Workflow*、*SLA Calendar*、*Case Timeline*、*My Open Cases*、*Unassigned — triage*、*Escalated Cases* 與 *⏰ SLA at Risk*(`src/service/views/case.view.ts`)——其中沒有任何一個以 **SLA 已違約**為過濾條件。真正按違約過濾的只有[服務概覽](/zh-Hant/docs/analytics/dashboards)上的兩個指標磁貼:**SLA Violations**(已標記 **SLA 已違約**,或仍未結且 **SLA 截止日期**已過——讀取磁貼時才計數,所以有沒有掃描它都是準的;升級這些工單的仍然是掃描)與 **Critical Cases**(未結 + Critical)。如果你要的是一份能逐條處理的列表,**Escalated Cases** 是最接近的那個:`case_sla_monitor` 會把它標記為違約的每一個工單都升級掉,所以工單仍然打開時發現的每一次違約都會落進這個檢視——只是會和那些僅因優先順序而升級的工單混在一起。(在解決或關閉的那次儲存裡因超時而被標記的工單已經結束:**SLA Violations** 磁貼會把它計入,但它已經沒有什麼可再處理的了。) +這張表過去寫的兩個名字根本不是檢視。**Breached SLA** 與 **Critical Cases** 都不存在於列表檢視裡:`crm_case` 隨產品提供的檢視一共八個——*All Cases*、*Service Workflow*、*SLA Calendar*、*Case Timeline*、*My Open Cases*、*Unassigned — triage*、*Escalated Cases* 與 *⏰ SLA at Risk*(`src/service/views/case.view.ts`)——其中沒有任何一個以 **SLA 已違約**為過濾條件。真正按違約過濾的只有[服務概覽](/zh-Hant/docs/analytics/dashboards)上的兩個指標磁貼:**Open Cases Past SLA**(未結,且已標記 **SLA 已違約** 或 **SLA 截止日期**已過——讀取磁貼時才計數,所以有沒有掃描它都是準的;升級這些工單的仍然是掃描)與 **Critical Cases**(未結 + Critical)。如果你要的是一份能逐條處理的列表,**Escalated Cases** 是最接近的那個:`case_sla_monitor` 會把它標記為違約的每一個工單都升級掉,所以工單仍然打開時發現的每一次違約都會落進這個檢視——只是會和那些僅因優先順序而升級的工單混在一起。(在解決或關閉的那次儲存裡因超時而被標記的工單已經結束:它離開 **Open Cases Past SLA** 磁貼,計入已結束工作所在的地方——**SLA Met (Closed Cases)** 量表圖和 **SLA on Closed Cases** 報表。) **My Open Cases 是按優先順序排序的,不是按 SLA 截止日期。** 它的排序鍵是 `priority_rank` 降序在先、`sla_due_date` 升序僅作次級鍵(`src/service/views/case.view.ts`)——所以一個一小時後就到期的 Low 工單,仍然排在每一個 Critical 工單下面。不過次級鍵現在在每一檔裡都有值可排了,同一檔內最早到期的排在最前面。如果你是按截止時間在幹活,請在它旁邊同時開著 **SLA at Risk** 或 **SLA Calendar**。 @@ -159,7 +159,7 @@ SLA(服務級別協議)系統為**每一張工單**打上一個**截止時 ## 給服務經理的提示 -- ✅ 沒有 **Breached SLA** 這個列表檢視可以執行。改去處理 **Escalated Cases** 檢視——SLA 掃描標記的每一個工單都會被升級進這個檢視——數量則看服務概覽上的 **SLA Violations** 磁貼。 +- ✅ 沒有 **Breached SLA** 這個列表檢視可以執行。改去處理 **Escalated Cases** 檢視——SLA 掃描標記的每一個工單都會被升級進這個檢視——數量則看服務概覽上的 **Open Cases Past SLA** 磁貼。 - ✅ 每日站會用那塊看板,但它叫 **Service Workflow**(`case_workflow`),在工單列表上以 **Service Workflow** 頁籤出現。本應用裡不存在叫 *Service Board* 的東西。 - ✅ 輔導那些違約率趨升的客服——在它形成模式之前於 1:1 中提出。 diff --git a/docs/architecture/module-split-plan.md b/docs/architecture/module-split-plan.md index 4990fe73b..6ed9547ef 100644 --- a/docs/architecture/module-split-plan.md +++ b/docs/architecture/module-split-plan.md @@ -577,7 +577,7 @@ decisions left to the compile path and this ruling settles. `objectstack.composition.ts`, the roster in `test/package-ownership.test.ts`) until a platform release carried #25514. **✅ Moved by #2276** on the 18.2.0 pin: each registers with its module, the app stack resolves the three action targets through - `artifactFlows`, and the Top Products tile binds revenue's `product_metrics` with no + `artifactFlows`, and the List Price by Category tile binds revenue's `product_metrics` with no option at all; both lists are gone, and the tile's rows and the three actions' launches measured the same before and after. The page related lists, the `crm_product` picker and the app → module lookup were measured accepted; diff --git a/src/docs/crm_analytics_metrics.md b/src/docs/crm_analytics_metrics.md index a8e8d148f..3d379e74b 100644 --- a/src/docs/crm_analytics_metrics.md +++ b/src/docs/crm_analytics_metrics.md @@ -72,18 +72,18 @@ in the pipeline. | **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. | +| **Quota Attainment by Rep** | **Quota**, **Closed** and **Attainment** per owner, from the **Quarter** forecasts of the current quarter only. It stays empty until the nightly forecast snapshot has opened the current quarter and a manager has set each quota. | 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 +Under **Reports**, **Pipeline Coverage by Quarter** 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**. | +| **Pipeline Coverage by Quarter** | **Total Amount** and **Opportunities** of the deals not at **Closed Won** or **Closed Lost**, by **Forecast Category** and **Close Quarter**. | ## Activity diff --git a/src/docs/crm_analytics_metrics.zh.md b/src/docs/crm_analytics_metrics.zh.md index 41a99dd09..7f95c04ff 100644 --- a/src/docs/crm_analytics_metrics.zh.md +++ b/src/docs/crm_analytics_metrics.zh.md @@ -51,15 +51,15 @@ description: 赢率、管道、配额达成、覆盖倍数和客户流失信号 | **Attainment** | **Closed** ÷ **Quota**。先把金额加总再相除,所以配额大的人权重更大。 | | **达成率(%)** | 单条销售预测上的 **已成交金额** ÷ **配额**。设置配额之前显示 0%。 | | **覆盖倍数** | 单条销售预测上的 **管道金额** ÷(**配额** − **已成交金额**),即剩下的管道够不够填补还差的部分。配额已完成后显示 0。 | -| **Quota Attainment by Rep** | 每位负责人的 **Quota**、**Closed** 和 **Attainment**,只取本季度的**季度**预测。 | +| **Quota Attainment by Rep** | 每位负责人的 **Quota**、**Closed** 和 **Attainment**,只取本季度的**季度**预测。要等夜间预测快照开出本季度、并由经理设好每个配额之后才有数据。 | 周期始终是固定的。把一个季度的配额和一个月的配额加在一起,得到的数字没有意义,所以每个配额数字要么只显示一个周期,要么把各个周期并排列出。 -在**报表**下,**管道覆盖率**用整个团队回答同一个问题: +在**报表**下,**季度管道覆盖率**用整个团队回答同一个问题: | 报表 | 统计内容 | |---|---| -| **Pipeline Coverage by Forecast × Quarter** | 不处于**成交**或**失败**的商机的 **Total Amount** 和 **Opportunities**,按 **Forecast Category** 和 **Close Quarter** 分列。 | +| **Pipeline Coverage by Quarter** | 不处于**成交**或**失败**的商机的 **Total Amount** 和 **Opportunities**,按 **Forecast Category** 和 **Close Quarter** 分列。 | ## 互动 diff --git a/src/revenue/index.ts b/src/revenue/index.ts index cfe53f4b1..d4b1efafe 100644 --- a/src/revenue/index.ts +++ b/src/revenue/index.ts @@ -66,7 +66,7 @@ export type HotCrmRevenueCollections = Pick; * * Two of them are reached from the app package: `quote_generation` (see * {@link revenueFlows}) and `product_metrics`, which the app's CRM Overview - * Top Products tile binds — a widget reference `os validate` resolves against + * List Price by Category tile binds — a widget reference `os validate` resolves against * the artifact's packages since objectstack#25514. * * It is packaged exactly as `src/service/index.ts` is, and that file carries diff --git a/src/sales/index.ts b/src/sales/index.ts index 7c581a579..8d183578f 100644 --- a/src/sales/index.ts +++ b/src/sales/index.ts @@ -69,7 +69,7 @@ export type HotCrmAppCollections = Omit< * validation failed (3 issues)"); with it, none. Verified like * `artifactObjects`: a target that resolved only through the list and that no * package defines is refused by `composeStacks`. The dashboard half of the - * same fix needs no option — the CRM Overview Top Products tile binds + * same fix needs no option — the CRM Overview List Price by Category tile binds * revenue's `product_metrics`, which `os validate` now resolves against the * artifact's packages. * diff --git a/test/case-creation-date.test.ts b/test/case-creation-date.test.ts index 3c45ad432..e3471047b 100644 --- a/test/case-creation-date.test.ts +++ b/test/case-creation-date.test.ts @@ -98,7 +98,7 @@ describe('crm_case has no duplicate creation field', () => { /** * The property the card is about, EXECUTED rather than read: a case written * with no date at all — the shape a UI or REST create has — is counted by - * "Cases Opened by Priority × Day" and inside the service dashboard's range, + * "Cases Opened by Day" and inside the service dashboard's range, * and a seeded case keeps the creation day it was authored with. * * The shipped stack runs on a real kernel so the platform's own @@ -216,7 +216,7 @@ describe('a case created without any date is counted', () => { ).toBe(authoredSeedInstant); }); - it('counts both cases in "Cases Opened by Priority × Day", each on its own day', async () => { + it('counts both cases in "Cases Opened by Day", each on its own day', async () => { const rows = await runReport(); const column = String((CasesOpenedByDayPriorityReport.columns ?? [])[0]); const cell = (priority: string): AnyRec[] => rows.filter((r) => r.priority === priority); diff --git a/test/case-sla-breach-on-close.test.ts b/test/case-sla-breach-on-close.test.ts index 3ef3e5ae1..29c3bcf24 100644 --- a/test/case-sla-breach-on-close.test.ts +++ b/test/case-sla-breach-on-close.test.ts @@ -232,7 +232,7 @@ describe('the SLA surfaces count the late finishes, through the dataset query pa return (res.rows ?? []) as Rec[]; }; - it('the SLA Performance report reads the late closes as violations', async () => { + it('the SLA on Closed Cases report reads the late closes as violations', async () => { const report = item('report', 'sla_performance'); const rows = await query({ dimensions: report.rows, measures: report.values, filter: report.runtimeFilter }); // Three CLOSED cases (two late, one on time); the late resolve is not diff --git a/test/docs-analytics-vocabulary.test.ts b/test/docs-analytics-vocabulary.test.ts index 1350c98db..eee9bf644 100644 --- a/test/docs-analytics-vocabulary.test.ts +++ b/test/docs-analytics-vocabulary.test.ts @@ -93,7 +93,7 @@ const REPORT_LABELS: string[] = Object.values(reports as Record) const DATASET_LIST = Object.values(datasets as Record); // The SERVED navigation: the service module contributes *Service Overview* and -// *SLA Performance* into the app's groups (`servedApp()` folds them in the +// *SLA on Closed Cases* into the app's groups (`servedApp()` folds them in the // way the console does), so they are navigation labels like any other. const NAV = (servedApp((CrmApp as AnyRec).name).navigation ?? []) as AnyRec[]; const GROUPS = NAV.filter((n) => n.type === 'group'); diff --git a/test/docs-quick-tour-navigation.test.ts b/test/docs-quick-tour-navigation.test.ts index 789db02cf..45d8f718f 100644 --- a/test/docs-quick-tour-navigation.test.ts +++ b/test/docs-quick-tour-navigation.test.ts @@ -635,9 +635,10 @@ describe('the source facts the quick-tour landing section now rests on (#971)', 'industry', 'stage', ]); + // No `close_date`: CRM Overview's revenue trend moved to Executive + // Overview, its one home (#2370 row 2). expect(dims(CrmOverviewDashboard as AnyRec)).toEqual([ 'category', - 'close_date', 'lead_source', 'owner', 'stage', diff --git a/test/docs-service-index-analytics.test.ts b/test/docs-service-index-analytics.test.ts index c44891c34..fa8a9e32b 100644 --- a/test/docs-service-index-analytics.test.ts +++ b/test/docs-service-index-analytics.test.ts @@ -194,9 +194,9 @@ describe('service/index names the dashboard and reports that exist (#948)', () = it('does not resurrect the claims #948 removed', () => { const text = section(); expect(retired.filter((phrase) => text.includes(phrase))).toEqual([]); - // `SLA Performance` alone is the retired short name; the report is - // `SLA Performance Report`. - expect(text).not.toMatch(/SLA Performance(?!\s+Report)/); + // `SLA Performance` is a retired name; the report is `SLA on Closed + // Cases`, which states its base (#2370). + expect(text).not.toMatch(/SLA Performance/); }); it('states why an agent ranking cannot be built, not merely that it is absent', () => { @@ -259,6 +259,6 @@ describe('the source facts those four bullets now rest on (#948)', () => { it('the daily-inflow report puts priority in the rows and the day in the columns', () => { expect(CasesOpenedByDayPriorityReport.rows).toEqual(['priority']); expect(CasesOpenedByDayPriorityReport.columns).toEqual(['created_at']); - expect(CasesOpenedByDayPriorityReport.label).toBe('Cases Opened by Priority × Day'); + expect(CasesOpenedByDayPriorityReport.label).toBe('Cases Opened by Day'); }); }); diff --git a/test/line-item-conventions.test.ts b/test/line-item-conventions.test.ts index 759212bc4..c5c180fbe 100644 --- a/test/line-item-conventions.test.ts +++ b/test/line-item-conventions.test.ts @@ -274,6 +274,19 @@ describe('the two line-item price-fill hooks share one implementation', () => { }); }); + it.each(KINDS)('%s line item counts whole units: a fractional quantity is refused, a whole one stored as written', async (kind) => { + // `quantity` is `Field.number({ scale: 0 })` (#2370 row 8): a line counts + // units, so `2.5` is refused at the write door instead of being stored and + // shown as `2.50`. + const product = (await productAt(250)).id; + await expect(repLine(kind, { ...(await parentOf(kind)), crm_product: product, quantity: 2.5 })).rejects.toMatchObject({ + code: 'VALIDATION_FAILED', + fields: expect.arrayContaining([expect.objectContaining({ field: 'quantity' })]), + }); + const line = await repLine(kind, { ...(await parentOf(kind)), crm_product: product, quantity: 4 }); + expect(line.quantity).toBe(4); + }); + it('a product with no catalog price cannot exist — the no-price branch has no row either', async () => { // `crm_product.list_price` is required, for the system's writes too. await expect(verify.seed('crm_product', [{ name: `Unpriced ${++k}`, product_code: `UP-${k}`, is_active: true }])) diff --git a/test/seed-sla-share.test.ts b/test/seed-sla-share.test.ts index a89c9509c..b13837d1f 100644 --- a/test/seed-sla-share.test.ts +++ b/test/seed-sla-share.test.ts @@ -84,7 +84,7 @@ describe('a fresh demo seed after its first SLA sweep (#2288)', () => { /** * Open work is young, so the timing puts the generated inflow closer to * today than one case per day. The daily volume chart and the Cases Opened - * by Priority × Day matrix bucket `created_at` by day. Whatever hour of the + * by Day matrix bucket `created_at` by day. Whatever hour of the * UTC day the seed loads, no day holds more than 5 of the 30 generated * cases. The first cut of this change put up to 12 on one day. */ From ffcec3d81b55083fe15c4c086971bda76662b626 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 05:45:50 +0000 Subject: [PATCH 3/3] docs(#2370): dashboards page states which KPI tiles carry a comparison Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01J8VVAoAy1McNK4cq14Q2vC --- content/docs/analytics/dashboards.mdx | 4 ++-- content/docs/analytics/dashboards.zh-Hans.mdx | 4 ++-- content/docs/analytics/dashboards.zh-Hant.mdx | 4 ++-- src/service/dashboards/service.dashboard.ts | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/content/docs/analytics/dashboards.mdx b/content/docs/analytics/dashboards.mdx index 3f189570f..0857ad7e6 100644 --- a/content/docs/analytics/dashboards.mdx +++ b/content/docs/analytics/dashboards.mdx @@ -23,9 +23,9 @@ All five respect [sharing rules](/docs/administration/sharing-and-security) — **A hidden dashboard is not a locked one.** Executive Overview is also left out of the sidebar for anyone holding the `sales_rep`, `service_agent` or `service_manager` position, so their app opens on a page built for their day — **Sales Home** for a rep — instead of the company-level view. That is a shorter menu for focus, not an access restriction: a rep who opens the dashboard's link still sees it. What any dashboard shows is decided by record visibility, which [sharing rules](/docs/administration/sharing-and-security#how-the-layers-combine) set: every number adds up only the records the viewer may read, so a rep sees the executive layout over their own share of the data, never a deal they could not open themselves. -### No tile shows a trend arrow +### A trend arrow is a real comparison -None of the KPI tiles displays a "vs last period" delta, on any dashboard. A period-over-period comparison is a measurement: it has to come from a real comparison query, and until the renderer can run one for dataset-bound metrics, a percentage on the tile would be a number typed by hand that no data can change — it would read the same on a full database and an empty one. So the tiles show the figure that was actually queried, and nothing else. +A KPI tile shows a "vs previous period" or "vs previous year" delta only when it declares a comparison, and the console then runs a second query over the earlier window. On CRM Overview that is Total Revenue, Deals Won and Avg Deal Size; on Sales Performance, Open Pipeline, Closed Won and Avg Deal Size; on Customer Service, Open Cases, Avg Resolution Time and Open Cases Past SLA; on Executive Overview, Total Revenue (YTD). A *(Selected Period)* tile compares with the window of the same length just before the picker's, so with the picker at All time there is no earlier window and the tile shows its bare number. No percentage is typed by hand: every one changes with the data. ## 🏠 CRM Overview diff --git a/content/docs/analytics/dashboards.zh-Hans.mdx b/content/docs/analytics/dashboards.zh-Hans.mdx index 57f5a3f4d..593a1bfe9 100644 --- a/content/docs/analytics/dashboards.zh-Hans.mdx +++ b/content/docs/analytics/dashboards.zh-Hans.mdx @@ -23,9 +23,9 @@ HotCRM 内置五个仪表盘。每个都为特定角色而构建,回答该角 **隐藏的仪表盘不等于上锁的仪表盘。** Executive Overview 还对持有 `sales_rep`、`service_agent` 或 `service_manager` 岗位的人不出现在侧边栏里,这样他们打开应用时落在为自己日常工作准备的页面上——销售代表落在 **Sales Home**——而不是公司层面的总览。这只是为了聚焦而缩短菜单,不是访问限制:销售代表直接打开这个仪表盘的链接,仍然能看到它。任何仪表盘显示什么,由记录可见性决定,而记录可见性由 [共享规则](/zh-Hans/docs/administration/sharing-and-security#how-the-layers-combine) 设定:每个数字只汇总查看者有权读取的记录,所以销售代表看到的是高管版式下自己那一份数据,看不到任何他自己打不开的交易。 -### 没有任何磁贴显示趋势箭头 +### 趋势箭头来自真实的对比查询 -在任何一个仪表盘上,KPI 磁贴都不显示"对比上期"的涨跌幅。跨期对比是一次测量:它必须来自一次真实的对比查询。在渲染器能为数据集绑定的指标跑出这样一次查询之前,磁贴上的百分比只会是手工敲进去的数字,没有任何数据能改变它——数据库塞得满满的时候和一条记录都没有的时候,它读起来一模一样。所以磁贴只呈现真正查询出来的那个数,此外什么都不加。 +KPI 磁贴只有在声明了对比时,才显示「对比上期」或「对比去年」的涨跌幅,console 会再对更早的那个时间窗跑一次查询。CRM Overview 上是 Total Revenue、Deals Won 和 Avg Deal Size;Sales Performance 上是 Open Pipeline、Closed Won 和 Avg Deal Size;Customer Service 上是 Open Cases、Avg Resolution Time 和 Open Cases Past SLA;Executive Overview 上是 Total Revenue (YTD)。*(Selected Period)* 磁贴对比的是日期选择器所选时间窗之前、同样长度的那个时间窗,所以选择器在 All time 时没有更早的时间窗,磁贴只显示数字本身。没有任何百分比是手工敲进去的:每一个都随数据变化。 ## 🏠 CRM Overview diff --git a/content/docs/analytics/dashboards.zh-Hant.mdx b/content/docs/analytics/dashboards.zh-Hant.mdx index 76af9ed20..8ef309443 100644 --- a/content/docs/analytics/dashboards.zh-Hant.mdx +++ b/content/docs/analytics/dashboards.zh-Hant.mdx @@ -25,9 +25,9 @@ HotCRM 內建五個儀表板。每個都為特定角色而建構,回答該角 **隱藏的儀表板不等於上鎖的儀表板。** Executive Overview 還對持有 `sales_rep`、`service_agent` 或 `service_manager` 職位的人不出現在側邊欄裡,這樣他們打開應用時落在為自己日常工作準備的頁面上——銷售代表落在 **Sales Home**——而不是公司層面的總覽。這只是為了聚焦而縮短選單,不是存取限制:銷售代表直接打開這個儀表板的連結,仍然能看到它。任何儀表板顯示什麼,由記錄可見性決定,而記錄可見性由 [共享規則](/zh-Hant/docs/administration/sharing-and-security#how-the-layers-combine) 設定:每個數字只彙總檢視者有權讀取的記錄,所以銷售代表看到的是高管版面下自己那一份資料,看不到任何他自己打不開的交易。 -### 沒有任何磁貼顯示趨勢箭頭 +### 趨勢箭頭來自真實的對比查詢 -在任何一個儀表板上,KPI 磁貼都不顯示"對比上期"的漲跌幅。跨期對比是一次測量:它必須來自一次真實的對比查詢。在渲染器能為綁定資料集的指標跑出這樣一次查詢之前,磁貼上的百分比只會是手動敲進去的數字,沒有任何資料能改變它——資料庫塞得滿滿的時候和一筆記錄都沒有的時候,它讀起來一模一樣。所以磁貼只呈現真正查詢出來的那個數,此外什麼都不加。 +KPI 磁貼只有在宣告了對比時,才顯示「對比上期」或「對比去年」的漲跌幅,console 會再對更早的那個時間窗跑一次查詢。CRM Overview 上是 Total Revenue、Deals Won 和 Avg Deal Size;Sales Performance 上是 Open Pipeline、Closed Won 和 Avg Deal Size;Customer Service 上是 Open Cases、Avg Resolution Time 和 Open Cases Past SLA;Executive Overview 上是 Total Revenue (YTD)。*(Selected Period)* 磁貼對比的是日期選擇器所選時間窗之前、同樣長度的那個時間窗,所以選擇器在 All time 時沒有更早的時間窗,磁貼只顯示數字本身。沒有任何百分比是手動敲進去的:每一個都隨資料變化。 ## 🏠 CRM Overview diff --git a/src/service/dashboards/service.dashboard.ts b/src/service/dashboards/service.dashboard.ts index 444d263e8..238eb4004 100644 --- a/src/service/dashboards/service.dashboard.ts +++ b/src/service/dashboards/service.dashboard.ts @@ -18,7 +18,7 @@ const dataset = 'case_metrics'; * * No KPI tile declares a `trend`. A period-over-period delta is a measurement, * so it comes from a real comparison query: Open Cases, Avg Resolution Time - * and SLA Violations declare `compareTo: { kind: 'previousPeriod' }` (#2296) + * and Open Cases Past SLA declare `compareTo: { kind: 'previousPeriod' }` (#2296) * over the date picker's `created_at` window (bare number at All time, * objectstack#25970). The percentages this file used to carry were typed by * hand and recomputed by nothing, so they kept asserting the same "-6.2% vs