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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/2369-forms-record-pages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"hotcrm": patch
---

Clause-②: no
Metadata-protocol: none

**Forms and record pages are laid out for the work done on them (#2369).** **New Contact** asks for Email and Phone on its first tab, beside the required names, so a bad email is flagged where you are typing. **New Lead** puts Salutation, First Name and Last Name on one row, and **Lead Score** says that a blank score is filled in for you. The **Send Email** dialog says where the email goes. The record headers of accounts, quotes, products, campaigns and knowledge articles show what the title does not: tier, health and revenue on an account; account, deal, status, total and expiry on a quote; type, status, start date and budget on a campaign; and List Price on a product, which the product picker on a line item now shows too. The related lists that used to borrow those headers keep their columns. A staff-created case can now set **Case Type**. A case on track no longer shows a red "× No" for SLA Violated; a breached case shows a red **SLA breached** banner (「已违反 SLA」) instead. A knowledge article opens on its body and shows its Helpful and Not Helpful counts in the header, and after a vote the message says whether it was recorded, changed or already your verdict, with the current counts. **Log a Call**, **Log a Meeting** and **Schedule a Meeting** say that the contact or lead you start from is added for you. The Related tabs of opportunities and cases open their Quotes, Products and Open Tasks panels. New and changed text is in English, Chinese, Spanish and Japanese, and the Sales, Service and Knowledge manual pages describe the new behaviour.
3 changes: 2 additions & 1 deletion src/docs/crm_sales_activities.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ Leads, contacts, accounts, opportunities and cases each carry three buttons:

- **Log a Call** and **Log a Meeting** record something that already happened.
The event is saved as **held**, and the people you name are added as its
attendees.
attendees. You are added as the organizer, and the contact or lead you start
from is added for you, so **Contact Attendees** is only for anyone else.
- **Schedule a Meeting** books one in the future. It is saved as **planned**
and appears in **My Calendar**.

Expand Down
2 changes: 1 addition & 1 deletion src/docs/crm_sales_activities.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ description: 任务、电话和会议记录、安排跟进,以及它们背后

线索、联系人、客户、商机和工单上都有三个按钮:

- **记录通话**和**记录会议**用来记录已经发生的事。活动以**已举行**状态保存,你填写的人会成为它的参与者。
- **记录通话**和**记录会议**用来记录已经发生的事。活动以**已举行**状态保存,你填写的人会成为它的参与者。你自己作为组织者加入,你发起操作的那位联系人或线索也会自动加入,所以**联系人参与者**里只需填其他人。
- **安排会议**用来预约将来的会议。它以**已计划**状态保存,并出现在**我的日历**里。

活动会关联到你发起操作的那条记录,因此会出现在那条记录的相关列表里,以及**互动 › 活动**下的日历视图中。
Expand Down
2 changes: 2 additions & 0 deletions src/docs/crm_sales_leads.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ automatically:

**Lead Score** is the 1–5 star field on each lead (shown as *Lead Score* in the
UI). A score of 4★ or more marks the lead "hot" and triggers the 1-day SLA.
Leave it blank on a new lead and HotCRM fills it from the email domain, phone,
job title, industry and company size; a score you type stands.

A lead submitted through a web-to-lead form arrives with nobody behind it, so
it is **auto-assigned to the sales rep with the fewest open leads** — a
Expand Down
2 changes: 1 addition & 1 deletion src/docs/crm_sales_leads.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ description: 新线索如何获得跟进期限和负责人,以及转化一条
| **热线索**——4★ 或以上 | **1 天** | 提醒负责人在 *24 小时内跟进*。 |
| 其余线索 | **3 天** | 负责人获得一个标准的跟进期限。 |

**线索评分**是每条线索上的 1–5 星字段(界面上显示为*线索评分*)。4★ 或以上即视为"热线索",触发 1 天的 SLA。
**线索评分**是每条线索上的 1–5 星字段(界面上显示为*线索评分*)。4★ 或以上即视为"热线索",触发 1 天的 SLA。新建线索时留空,HotCRM 会根据邮箱域名、电话、职位、行业和公司规模替你打分;你自己填写的评分会保留。

通过网页表单提交的线索(网页转线索)背后没有具体的人,所以会**自动分配给名下未结线索最少的销售代表**——在所有持有*销售代表*岗位的人之间自我均衡地轮询。只有在还没有任何销售代表时,这类线索才会暂时无人负责,等待经理认领。

Expand Down
8 changes: 7 additions & 1 deletion src/marketing/objects/campaign.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@ export const Campaign = ObjectSchema.create({
// title was a formula with no column for `$search` to default to. The
// title is stored now; these stay because they name what is searched.
searchableFields: ['name', 'campaign_code'],
highlightFields: ['campaign_code', 'name', 'type', 'status', 'start_date'],
// The record header's strip (#2369 row 5): the title already reads
// "CPG-0001 - Name". The Add to Campaign picker shows the same fields under
// each campaign, where the code used to repeat the title. `end_date` and
// `owner_id` stay in the body: each is the last visible member of its group
// ("schedule", "assignment"), which would then never render on the detail
// page (lint `field-group-shadowed`).
highlightFields: ['type', 'status', 'start_date', 'budgeted_cost'],

// Every other business object with a detail page groups its fields; campaign
// was one of the two that did not, so its detail page fell back to one flat
Expand Down
5 changes: 4 additions & 1 deletion src/revenue/objects/product.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ export const Product = ObjectSchema.create({
// title was a formula with no column for `$search` to default to. The
// title is stored now; these stay because they name what is searched.
searchableFields: ['name', 'product_code', 'sku'],
highlightFields: ['product_code', 'name', 'category', 'is_active'],
// The record header's strip (#2369 row 5): the title already reads
// "PRD-0001 - Name". The lookup picker shows the same fields under each
// product, so a line item's product picker now shows the list price.
highlightFields: ['category', 'list_price', 'is_active'],

fieldGroups: [
{ key: 'basic', label: 'Product Information', icon: 'info' },
Expand Down
9 changes: 8 additions & 1 deletion src/revenue/objects/quote.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ export const Quote = ObjectSchema.create({
// title was a formula with no column for `$search` to default to. The
// title is stored now; these stay because they name what is searched.
searchableFields: ['name', 'quote_number'],
highlightFields: ['quote_number', 'name', 'crm_account', 'status', 'total_price'],
// The record header's strip (#2369 row 5): the title already reads
// "QTE-0003 - Name", so the strip carries what it does not. The quote panels
// on an account, a contact and an opportunity keep their old columns through
// `relatedListColumns` on the three lookups below.
highlightFields: ['crm_account', 'crm_opportunity', 'status', 'total_price', 'expiration_date'],

fieldGroups: [
{ key: 'basic', label: 'Quote Information', icon: 'info' },
Expand Down Expand Up @@ -86,11 +90,13 @@ export const Quote = ObjectSchema.create({
required: true,
storage: { notNull: true },
deleteBehavior: 'cascade',
relatedListColumns: ['quote_number', 'name', 'status', 'total_price'],
}),

crm_contact: Field.lookup('crm_contact', {
label: 'Contact',
group: 'basic',
relatedListColumns: ['quote_number', 'name', 'crm_account', 'status', 'total_price'],
// A TRANSITION GATE, not an invariant (#1017, narrowed by #2161; AGENTS.md
// Metadata semantics rule 7). Optional at draft time. Required on the
// write that takes the quote INTO `presented` or `accepted`, and on every
Expand Down Expand Up @@ -176,6 +182,7 @@ export const Quote = ObjectSchema.create({
group: 'basic',
// Scope opportunities to the quote's account (was dead referenceFilters).
dependsOn: ['crm_account'],
relatedListColumns: ['quote_number', 'name', 'crm_account', 'status', 'total_price'],
}),


Expand Down
9 changes: 9 additions & 0 deletions src/sales/actions/activity-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export const ACTIVITY_TARGETS: Record<string, string> = {
crm_case: 'related_to_case',
};


/** JS string literal, safely quoted for splicing into a body source. */
const lit = (value: string): string => JSON.stringify(value);

Expand Down Expand Up @@ -447,6 +448,14 @@ function activityAction(spec: ActivitySpec, objectName: string, nameField: strin
reference: 'crm_contact',
multiple: true,
required: false,
// Says what the body does with the record itself (#2369 row 8): on a
// contact or a lead that person is added as an attendee (the attendees
// step above) while this picker stays empty. One sentence true on all
// five objects, because the locale packs share one params block.
// ⛔ Not a prefill: on 18.2.0 the dialog gets no row from the record
// header or the list row menu, so `defaultFromRow` fills nothing and a
// `{{ record.x }}` hole prints raw (measured; platform finding).
helpText: 'The contact or lead you log this on is added automatically. Add anyone else here.',
},
{
name: 'attendee_users',
Expand Down
7 changes: 7 additions & 0 deletions src/sales/actions/contact.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,13 @@ export const AddContactToCampaignAction: Action = {
export const SendEmailAction: Action = {
name: 'send_email',
label: 'Send Email',
// The line under the dialog's title (#2369 row 3), which read the platform's
// generic "Please provide the required information to continue.": where the
// email goes and what happens to it. It names no address: on 18.2.0 the
// dialog gets no row from the record header or the list row menu, so a
// `{{ record.email }}` hole prints raw and a `defaultFromRow` param stays
// empty (measured; platform finding).
description: "Goes to this contact's email address. It is queued, then logged on the contact's timeline.",
objectName: 'crm_contact',
icon: 'mail',
// script, not modal: modal submits die on GET /api/v1/meta/object/<target>
Expand Down
10 changes: 9 additions & 1 deletion src/sales/objects/account.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ export const Account = ObjectSchema.create({
// title was a formula with no column for `$search` to default to. The
// title is stored now; these stay because they name what is searched.
searchableFields: ['name', 'account_number', 'registration_number'],
highlightFields: ['account_number', 'name', 'type', 'owner_id'],
// The record header's strip (#2369 row 5): what the title does not say. The
// page header shows the name and `{industry} · {type}` (account_detail.page.ts),
// so the strip carries the number, the tier, the health, the revenue and the
// owner. The child-account panel keeps its old columns through
// `relatedListColumns` on `parent_account`.
highlightFields: ['account_number', 'tier', 'health_score', 'annual_revenue', 'owner_id'],
// The record's picture (#1199): the record chrome draws the logo beside the
// title in every account page header. No logo, no picture: the platform
// draws no placeholder or initials in its place.
Expand Down Expand Up @@ -546,6 +551,9 @@ export const Account = ObjectSchema.create({
label: 'Parent Account',
description: 'Parent company in hierarchy',
group: 'ownership',
// The child-account panel's columns. Without them it falls back to
// `highlightFields` minus this lookup, which is the header strip.
relatedListColumns: ['account_number', 'name', 'type', 'owner_id'],
}),

// Boolean field
Expand Down
4 changes: 3 additions & 1 deletion src/sales/objects/lead.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,9 @@ export const Lead = ObjectSchema.create({

rating: Field.rating(5, {
label: 'Lead Score',
description: 'Lead quality score (1-5 stars)',
// Says what a blank does (#2369 row 2): `lead_automation` scores a lead
// created without one (`computeRating` in `lead.hook.ts`).
description: 'Leave blank and a new lead is scored for you (1-5 stars) from its email domain, phone, job title, industry and company size.',
group: 'qualification',
}),

Expand Down
34 changes: 15 additions & 19 deletions src/sales/pages/opportunity_detail.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ export const OpportunityDetailPage: Page = {
type: 'page:accordion',
id: 'opp_related_accordion',
properties: {
allowMultiple: true,
items: [
{
// Accordion item `key` is DELETED, not renamed to
Expand All @@ -304,26 +305,19 @@ export const OpportunityDetailPage: Page = {
// and the component's own designer text says the same
// ("collapsed: false opens a panel by default").
//
// ⚠️ ONLY the first panel, and that is the renderer's
// choice rather than a preference: with `allowMultiple`
// absent (it defaults false) the accordion mounts as
// `type="single"` and takes `defaultValue={open[0]}` —
// one panel, the first in declaration order. Marking
// Products or Open Tasks too would change nothing on
// screen. Opening more than one means declaring
// `allowMultiple: true`, which changes the interaction
// model (panels stop closing each other) and is not
// this card's ask.
// EVERY panel opens (#2369 row 9), the lead page's
// shape: `allowMultiple: true` above mounts the
// accordion as `type="multiple"`, so the renderer
// opens each item marked `collapsed: false` instead of
// only the first. Before, Products and Open Tasks
// started shut and closed each other, and a deal's
// line items (and their Add) sat behind a click.
//
// ⛔ NOT "default-open the sections that have rows",
// which the card asked for first: that is not
// expressible here. `collapsed` is a static boolean on
// a strict item shape (`label` / `icon` / `collapsed` /
// `children` — nothing else parses), the page is
// authored metadata evaluated before any row is read,
// and the related lists below fetch their own rows
// after mount. Reported rather than worked around; ⛔
// the component is not this repo's to change.
// ⛔ Still NOT "open the sections that have rows":
// `collapsed` is a static boolean on a strict item
// shape (`label` / `icon` / `collapsed` / `children`),
// the page is evaluated before any row is read, and
// the related lists fetch their own rows after mount.
collapsed: false,
children: [
{
Expand All @@ -340,6 +334,7 @@ export const OpportunityDetailPage: Page = {
},
{
label: 'Products',
collapsed: false,
children: [
{
type: 'record:related_list',
Expand Down Expand Up @@ -435,6 +430,7 @@ export const OpportunityDetailPage: Page = {
},
{
label: 'Open Tasks',
collapsed: false,
children: [
{
type: 'record:related_list',
Expand Down
15 changes: 12 additions & 3 deletions src/sales/translations/en/_shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ export const activityActions = {
params: {
subject: { label: 'Call Subject' },
duration: { label: 'Duration (minutes)' },
attendee_contacts: { label: 'Contact Attendees' },
attendee_contacts: {
label: 'Contact Attendees',
helpText: 'The contact or lead you log this on is added automatically. Add anyone else here.',
},
attendee_users: { label: 'Internal Attendees' },
notes: { label: 'Call Notes' },
},
Expand All @@ -44,7 +47,10 @@ export const activityActions = {
params: {
subject: { label: 'Meeting Subject' },
duration: { label: 'Duration (minutes)' },
attendee_contacts: { label: 'Contact Attendees' },
attendee_contacts: {
label: 'Contact Attendees',
helpText: 'The contact or lead you log this on is added automatically. Add anyone else here.',
},
attendee_users: { label: 'Internal Attendees' },
notes: { label: 'Meeting Notes' },
},
Expand All @@ -58,7 +64,10 @@ export const activityActions = {
start_time: { label: 'Start Time (UTC)' },
location: { label: 'Location' },
duration: { label: 'Duration (minutes)' },
attendee_contacts: { label: 'Contact Attendees' },
attendee_contacts: {
label: 'Contact Attendees',
helpText: 'The contact or lead you log this on is added automatically. Add anyone else here.',
},
attendee_users: { label: 'Internal Attendees' },
notes: { label: 'Agenda' },
},
Expand Down
1 change: 1 addition & 0 deletions src/sales/translations/en/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ export const appSurface: Omit<TranslationData, 'objects'> = {
subtitle: '{crm_account}',
components: {
case_highlights: { label: 'Key Information' },
case_sla_breached: { label: 'SLA Breached' },
case_status_path: { label: 'Case Status Path' },
},
},
Expand Down
1 change: 1 addition & 0 deletions src/sales/translations/en/objects.customer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ export const customer: Record<string, ObjectTranslationData> = {
},
send_email: {
label: 'Send Email',
description: 'Goes to this contact\'s email address. It is queued, then logged on the contact\'s timeline.',
params: {
subject: { label: 'Subject' },
body: { label: 'Body' },
Expand Down
2 changes: 1 addition & 1 deletion src/sales/translations/en/objects.pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const pipeline: Record<string, ObjectTranslationData> = {
industry: { label: 'Industry' },
mobile: { label: 'Mobile' },
website: { label: 'Website' },
rating: { label: 'Lead Score', help: 'Lead quality score (1-5 stars)' },
rating: { label: 'Lead Score', help: 'Leave blank and a new lead is scored for you (1-5 stars) from its email domain, phone, job title, industry and company size.' },
converted_account: { label: 'Converted Account' },
converted_contact: { label: 'Converted Contact' },
converted_opportunity: { label: 'Converted Opportunity' },
Expand Down
10 changes: 10 additions & 0 deletions src/sales/translations/en/objects.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,10 +187,20 @@ export const service: Record<string, ObjectTranslationData> = {
mark_article_helpful: {
label: 'Helpful',
successMessage: 'Thanks — recorded as helpful.',
outcomeMessages: {
recorded: 'Thanks — recorded as helpful. Helpful: ${result.helpful} · Not helpful: ${result.notHelpful}.',
changed: 'Your vote is now Helpful. Helpful: ${result.helpful} · Not helpful: ${result.notHelpful}.',
already: 'You already marked this article helpful. Helpful: ${result.helpful} · Not helpful: ${result.notHelpful}.',
},
},
mark_article_not_helpful: {
label: 'Not Helpful',
successMessage: 'Thanks — recorded as not helpful.',
outcomeMessages: {
recorded: 'Thanks — recorded as not helpful. Helpful: ${result.helpful} · Not helpful: ${result.notHelpful}.',
changed: 'Your vote is now Not Helpful. Helpful: ${result.helpful} · Not helpful: ${result.notHelpful}.',
already: 'You already marked this article not helpful. Helpful: ${result.helpful} · Not helpful: ${result.notHelpful}.',
},
},
},
},
Expand Down
Loading
Loading