Reports

The standard report library — the ten reports HotCRM publishes across deals, leads, cases and accounts, and what the missing ones would need.

Reports

Reports are filterable, exportable lists with grouping and summary totals. Where dashboards show the what, reports answer the why and which.

Standard reports

HotCRM ships ten reports out of the box — the barrel in src/reports/index.ts is the whole library. Each section below lists the ones it publishes, then names what this page used to promise and says whether the semantic layer could answer it:

📈 Sales reports

src/reports/opportunity.report.ts publishes four opportunity reports:

ReportWhat it shows
Opportunities by StagePipeline 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 OwnerClosed-won amount per rep, charted as Revenue by Sales Rep
Pipeline Coverage by Forecast × QuarterA 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 → StageA two-level funnel — each rep's book of business broken down stage by stage, with amount, deal count and average probability

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

The eight names this section used to list are published nowhere. Two of them are real names that belong to something other than a report:

  • Pipeline by Stage is a dashboard tile and a chart title, not a report. The tile is the open-pipeline funnel that CRM Overview, Sales Performance and Executive Overview all share (src/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, the Stale tab (src/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.

Of the five that are simply absent, three are a custom report away and two are not:

  • Forecast vs Actual — buildable, but not from the pipeline dataset: quota lives in forecast_metrics (src/datasets/forecast.dataset.ts), one row per owner per period, with Quota, Closed, Pipeline, Commit and a derived Attainment. Any report over it has to pin the period — group by Period, or filter to a single Period Type + Period Start — because the measures are plain sums and the table holds several periods at once.
  • Win/Loss Analysis — buildable: Win Reason and Loss Reason are dimensions, and Won Deals, Lost Deals, Settled Deals, Won Revenue, Lost Revenue and Win Rate are declared measures on opportunity_metrics.
  • Big Deals Won — buildable: closed-won amount is Won Revenue, and a threshold on the deal amount is an ordinary report filter.
  • Sales Cycle Length — not buildable. Nothing stores a cycle duration: the opportunity carries Stage Entry Date and Close Date but no elapsed-days column, and the one duration it does carry, Days in Current Stage, is the post-query formula above. By product has no landing place either — no dataset reads crm_opportunity_line_item.
  • Discount Approval Activity — not buildable. The approvals themselves are real (src/flows/opportunity-approval.flow.ts, and Approval Status on the opportunity), but no dimension exposes that field, and the discount is a percent on the line item (crm_opportunity_line_item.discount) that no dataset reads.

🎯 Lead reports

src/reports/lead.report.ts publishes one lead report:

ReportWhat it shows
Lead Engagement by Month × SourceA 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:

  • Lead Conversion Funnel and Aged Leads do not exist in any form. Both leaned on a Working status, and crm_lead.status has no such value — the route is New → Contacted → Qualified → Unqualified → Converted (src/objects/lead.object.ts), with Unqualified reachable from any open status. Working is a row in the import alias table (src/mappings/lead_import.mapping.ts), which maps that legacy spelling onto Contacted as records come in; the product never displays the word. State machines documents the declared route.
  • Lead Source ROI does not exist either, and the semantic layer could not answer it as written: lead_metrics (src/datasets/lead.dataset.ts) declares a single measure — the lead count — over four dimensions (status, source, created date, last-contacted month). Volume by source comes straight off that, a conversion rate is arithmetic you do on top of a count grouped by status, and revenue is not in this dataset at all — it never crosses over to opportunities.

The lead count is the one lead number anything else reads: the Open Leads tile on the Executive Overview dashboard counts unconverted leads off that same dataset. For the rest, build the report you need — see Custom reports below.

🎧 Service reports

src/reports/case.report.ts publishes three case reports:

ReportWhat it shows
Cases by Status and PriorityCase count and average resolution time, grouped by status and then priority, with a bar chart of cases by status
SLA Performance ReportCase count, SLA Violation Rate and average resolution time, by priority — over closed cases only
Cases Opened by Priority × DayA 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/datasets/case.dataset.ts) reads crm_case alone, through five dimensions — Status, Priority, Origin, Type, Created (bucketed by day) — and three measures: case count, average resolution time, SLA Violation Rate.

  • Case Volume by Origin is the one of the five you could build yourself: Origin is a dimension, so a count of cases by Email / Phone / Web / Chat / Portal is a report away. It just is not one of the shipped ones.
  • Case Resolution Time by category and agent — the measure is real and rides in both summary reports above, but agent is not reachable at all: owner_id is a field on the case and no dimension exposes it. The nearest thing to a category is Type (Question / Problem / Feature Request / Bug).
  • Top Accounts by Case Volume cannot be built either — there is no account dimension, and the dataset never crosses over to crm_account.
  • 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 fails on both halves: Customer Satisfaction (customer_rating) is a real 1–5 field on the case, yet case_metrics declares no measure over it, and there is no agent 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. 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.

💰 Customer and revenue reports

src/reports/account.report.ts and src/reports/churn.report.ts publish two reports, and this is the section they belong in — neither has ever appeared on this page:

ReportWhat it shows
Accounts by Industry and TypeA matrix of active accounts — industry down the rows, account type across the columns — with the account count and the Annual Revenue sum in each cell
Customer Churn SignalsThree stacked panels for proactive retention: At-Risk Accounts (active, no activity in 60+ days, by industry), Silent High-Value Accounts (strategic and enterprise tiers quiet for 90+ days, by type) and Recently Lost Opportunities (closed-lost in the last 30 days, by owner)

The three contract reports this section used to list are published nowhere, and none of them can be built as written, for one shared reason: there is no contract dataset. src/datasets/ covers nine objects — account, case, contact, event, forecast, lead, opportunity, product and task — and crm_contract is not among them, so nothing in analytics counts, sums or groups contracts.

  • Contracts Expiring — the question is real and it is answered by a view, not a report: Renewal Calendar (the Renewals tab on Contracts) plots every contract on its End Date, and Contract Terms shows the same terms as a Gantt (src/views/contract.view.ts). Both list records; neither totals them, because there is no dataset behind them to total.
  • Active Contracts by Product — unreachable twice over: no contract dataset, and product_metrics is a catalog rollup — Category, the product count and the list-price sum — that never crosses to contracts or to line items.
  • Renewal Pipeline — as written, opportunities tied to renewing contracts, it cannot be built: the link runs the other way (crm_contract.crm_opportunity) and no dimension exposes it. The renewal pipeline itself is buildable, though: Deal Type is a dimension on opportunity_metrics, and Existing Customer - Renewal is one of its values.

📣 Marketing reports

src/reports/ publishes no marketing report, and no custom one can be built — this is the one section of the page where the gap is not a missing report but a missing data source. src/datasets/ has no campaign dataset, so no report and no dashboard tile can aggregate campaign numbers at all.

The records do carry the numbers. crm_campaign has Budgeted Cost, Actual Cost, Expected Revenue, Actual Revenue, the Num Sent / Responses / Leads / Converted Leads / Opportunities / Won Opportunities counters and two formula fields — Response Rate % and ROI % — all of which the campaign record shows in its Performance section (src/views/campaign.view.ts). crm_campaign_member stamps First Opened, First Clicked, Response Date and Responded per member.

  • Campaign ROI — the per-campaign figure exists on the record, as the ROI % formula above. What does not exist is any way to aggregate or rank it: no campaign dataset means no measure, no dimension and no grouping. Attribution from the deal side is missing too — crm_opportunity.crm_campaign is a real lookup, but opportunity_metrics declares no campaign dimension, so sourced and influenced revenue cannot be attributed in analytics either.
  • Campaign Engagement — same reason, one layer down: the opens, clicks and responses are stamped on each member record, and nothing rolls them up. Conversion by campaign has the extra problem that lead_metrics never crosses to campaigns.

Closing the gap means adding src/datasets/campaign.dataset.ts — a source change and a redeploy, not something a report builder can reach. See Campaigns for what the campaign record does show today.

Working with reports

Every report has the same basic controls:

ControlWhat it does
FilterAdd criteria (date range, owner, stage, etc.)
Group byRoll up by any field — owner, stage, product, region
SortClick any column header
SummaryShow totals, averages, min, max at group and overall level
Drill inClick any row to open the source record
ExportDownload as CSV or Excel
Save asSave your filtered/grouped version with a new name
ScheduleEmail a PDF or CSV on a recurring schedule
SubscribeGet notified when the report changes

Permissions

  • A user only sees records they have access to — a rep running Opportunities by Stage sees their own deals; a manager sees the team's.
  • Admins can lock down which fields appear in reports via Field-Level Security.
  • Sensitive reports can be restricted to specific profiles.

Custom reports

Anyone with the Report Builder permission can:

  1. Pick a source object (lead, opportunity, case, etc.).
  2. Pick columns to display.
  3. Add filters.
  4. Pick grouping and summary fields.
  5. Choose a chart to visualise.
  6. Save and share.

For more advanced analysis — multi-object joins, time-series, cohort analysis — use cubes.

Subscriptions and scheduled delivery

The most common way to consume reports is by email subscription:

  • Frequency: daily, weekly, monthly.
  • Day & time of delivery.
  • Format: PDF (formatted) or CSV (raw data).
  • Recipients: yourself, a team alias, or specific users (subject to their own permissions).

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.

Tips for users

  • ✅ Save filtered versions for the views you run weekly — don't re-filter from scratch.
  • ✅ Use Group by Owner on team reports to spotlight outperformers and stragglers.
  • ✅ Export to Excel only when you need to combine with external data — otherwise stay in-app for live data.

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 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

  • ✅ When users keep building the same report, promote it to the standard library.
  • ✅ Retire reports no one runs — keep the library curated.
  • ✅ Set up default report folders by team (Sales, Service, Marketing, Exec) for easy discovery.

On this page