From 0ec067ce8b0a31e079b3039f454131d87493dabd Mon Sep 17 00:00:00 2001 From: tintinthong Date: Thu, 14 Mar 2024 09:04:18 +0800 Subject: [PATCH 01/47] Add account, company cards --- packages/base/email.gts | 16 +++ packages/base/web-url.gts | 7 ++ .../89ebf54f-4593-4f1e-a62d-8e0111db7796.json | 27 ++++ .../3200e097-b5cd-4099-a9b4-902b06de7a31.json | 19 +++ .../7298c9c6-2964-41a0-87f7-42127a16e12b.json | 72 +++++++++++ .../992531db-9912-49eb-9b72-f710cbcdb085.json | 72 +++++++++++ .../4802eeed-bec6-4d7a-8f05-6370866edd40.json | 67 ++++++++++ .../ca9dbfad-633a-4911-bc09-7fe3ebcb60c1.json | 19 +++ packages/drafts-realm/contact-card.gts | 15 +-- packages/drafts-realm/crm/account.gts | 65 ++++++++++ .../drafts-realm/crm/accounts-dashboard.gts | 118 ++++++++++++++++++ packages/drafts-realm/crm/contact.gts | 38 ++++++ packages/drafts-realm/crm/lead.gts | 14 +++ packages/drafts-realm/crm/opportunity.gts | 58 +++++++++ packages/drafts-realm/matrix-user.gts | 31 +++++ packages/drafts-realm/phone-number.gts | 20 +++ 16 files changed, 644 insertions(+), 14 deletions(-) create mode 100644 packages/base/email.gts create mode 100644 packages/base/web-url.gts create mode 100644 packages/drafts-realm/AccountsDashboard/89ebf54f-4593-4f1e-a62d-8e0111db7796.json create mode 100644 packages/drafts-realm/CardDef/3200e097-b5cd-4099-a9b4-902b06de7a31.json create mode 100644 packages/drafts-realm/CardDef/7298c9c6-2964-41a0-87f7-42127a16e12b.json create mode 100644 packages/drafts-realm/CardDef/992531db-9912-49eb-9b72-f710cbcdb085.json create mode 100644 packages/drafts-realm/CrmAccount/4802eeed-bec6-4d7a-8f05-6370866edd40.json create mode 100644 packages/drafts-realm/MatrixUser/ca9dbfad-633a-4911-bc09-7fe3ebcb60c1.json create mode 100644 packages/drafts-realm/crm/account.gts create mode 100644 packages/drafts-realm/crm/accounts-dashboard.gts create mode 100644 packages/drafts-realm/crm/contact.gts create mode 100644 packages/drafts-realm/crm/lead.gts create mode 100644 packages/drafts-realm/crm/opportunity.gts create mode 100644 packages/drafts-realm/matrix-user.gts create mode 100644 packages/drafts-realm/phone-number.gts diff --git a/packages/base/email.gts b/packages/base/email.gts new file mode 100644 index 00000000000..e8fc9fcf92b --- /dev/null +++ b/packages/base/email.gts @@ -0,0 +1,16 @@ +import { FieldDef, contains, field, Component } from './card-api'; +import StringField from './string'; + +export class EmailAddress extends FieldDef { + static displayName = 'EmailAddress'; + @field value = contains(StringField); + // put some validation logic here + + static embedded = class Embedded extends Component { + + }; +} diff --git a/packages/base/web-url.gts b/packages/base/web-url.gts new file mode 100644 index 00000000000..0febeb49edd --- /dev/null +++ b/packages/base/web-url.gts @@ -0,0 +1,7 @@ +import { FieldDef, contains, field } from './card-api'; +import StringField from './string'; + +export class WebUrl extends FieldDef { + static displayName = 'Web URL'; + @field value = contains(StringField); +} diff --git a/packages/drafts-realm/AccountsDashboard/89ebf54f-4593-4f1e-a62d-8e0111db7796.json b/packages/drafts-realm/AccountsDashboard/89ebf54f-4593-4f1e-a62d-8e0111db7796.json new file mode 100644 index 00000000000..f5b8f52f0f6 --- /dev/null +++ b/packages/drafts-realm/AccountsDashboard/89ebf54f-4593-4f1e-a62d-8e0111db7796.json @@ -0,0 +1,27 @@ +{ + "data": { + "type": "card", + "attributes": { + "description": null, + "thumbnailURL": null + }, + "relationships": { + "accounts.0": { + "links": { + "self": "../CardDef/7298c9c6-2964-41a0-87f7-42127a16e12b" + } + }, + "accounts.1": { + "links": { + "self": "../CardDef/992531db-9912-49eb-9b72-f710cbcdb085" + } + } + }, + "meta": { + "adoptsFrom": { + "module": "../crm/accounts-dashboard", + "name": "AccountsDashboard" + } + } + } +} \ No newline at end of file diff --git a/packages/drafts-realm/CardDef/3200e097-b5cd-4099-a9b4-902b06de7a31.json b/packages/drafts-realm/CardDef/3200e097-b5cd-4099-a9b4-902b06de7a31.json new file mode 100644 index 00000000000..f5f5a948343 --- /dev/null +++ b/packages/drafts-realm/CardDef/3200e097-b5cd-4099-a9b4-902b06de7a31.json @@ -0,0 +1,19 @@ +{ + "data": { + "type": "card", + "attributes": { + "username": "jane_doe88", + "email": { + "value": "jane.doe88@example.com" + }, + "description": null, + "thumbnailURL": null + }, + "meta": { + "adoptsFrom": { + "module": "../matrix-user", + "name": "MatrixUser" + } + } + } +} \ No newline at end of file diff --git a/packages/drafts-realm/CardDef/7298c9c6-2964-41a0-87f7-42127a16e12b.json b/packages/drafts-realm/CardDef/7298c9c6-2964-41a0-87f7-42127a16e12b.json new file mode 100644 index 00000000000..edcea4c0b3d --- /dev/null +++ b/packages/drafts-realm/CardDef/7298c9c6-2964-41a0-87f7-42127a16e12b.json @@ -0,0 +1,72 @@ +{ + "data": { + "type": "card", + "attributes": { + "accountName": "Acme Corporation", + "accountAlias": "AcmeCorp", + "description": "A leading provider of innovative solutions in the technology sector.", + "contactInformation": { + "salutation": "Mr.", + "firstName": "John", + "lastName": "Doe", + "address": { + "streetAddress": "123 Tech Road", + "city": "Innovate City", + "region": "Silicon Valley", + "postalCode": "94016", + "poBoxNumber": "456", + "country": "USA" + }, + "phoneNumber": null, + "email": { + "value": "contact@acmecorp.com" + }, + "website": { + "value": "www.acmecorp.com" + }, + "company": "Acme Corporation" + }, + "billingAddress": { + "streetAddress": "123 Tech Road", + "city": "Innovate City", + "region": "Silicon Valley", + "postalCode": "94016", + "poBoxNumber": "456", + "country": "USA" + }, + "shippingAddress": { + "streetAddress": "123 Tech Road", + "city": "Innovate City", + "region": "Silicon Valley", + "postalCode": "94016", + "poBoxNumber": "789", + "country": "USA" + }, + "numberOfEmployees": 500, + "thumbnailURL": null + }, + "relationships": { + "owner": { + "links": { + "self": null + } + }, + "parentAccount": { + "links": { + "self": "./992531db-9912-49eb-9b72-f710cbcdb085" + } + }, + "company": { + "links": { + "self": null + } + } + }, + "meta": { + "adoptsFrom": { + "module": "../crm/account", + "name": "CrmAccount" + } + } + } +} \ No newline at end of file diff --git a/packages/drafts-realm/CardDef/992531db-9912-49eb-9b72-f710cbcdb085.json b/packages/drafts-realm/CardDef/992531db-9912-49eb-9b72-f710cbcdb085.json new file mode 100644 index 00000000000..bc43a08c5c0 --- /dev/null +++ b/packages/drafts-realm/CardDef/992531db-9912-49eb-9b72-f710cbcdb085.json @@ -0,0 +1,72 @@ +{ + "data": { + "type": "card", + "attributes": { + "accountName": "Globex Corporation", + "accountAlias": "GlobexCorp", + "description": "A global leader in the field of international commerce and innovation.", + "contactInformation": { + "salutation": "Ms.", + "firstName": "Jane", + "lastName": "Smith", + "address": { + "streetAddress": "456 Global Plaza", + "city": "World City", + "region": "Global District", + "postalCode": "55555", + "poBoxNumber": "789", + "country": "Globaland" + }, + "phoneNumber": null, + "email": { + "value": "info@globexcorp.com" + }, + "website": { + "value": "www.globexcorp.com" + }, + "company": "Globex Corporation" + }, + "billingAddress": { + "streetAddress": "456 Global Plaza", + "city": "World City", + "region": "Global District", + "postalCode": "55555", + "poBoxNumber": "789", + "country": "Globaland" + }, + "shippingAddress": { + "streetAddress": "789 Trade Route", + "city": "Commerce Town", + "region": "Trade Zone", + "postalCode": "101010", + "poBoxNumber": "101", + "country": "Marketland" + }, + "numberOfEmployees": 10000, + "thumbnailURL": null + }, + "relationships": { + "owner": { + "links": { + "self": null + } + }, + "parentAccount": { + "links": { + "self": null + } + }, + "company": { + "links": { + "self": null + } + } + }, + "meta": { + "adoptsFrom": { + "module": "../crm/account", + "name": "CrmAccount" + } + } + } +} \ No newline at end of file diff --git a/packages/drafts-realm/CrmAccount/4802eeed-bec6-4d7a-8f05-6370866edd40.json b/packages/drafts-realm/CrmAccount/4802eeed-bec6-4d7a-8f05-6370866edd40.json new file mode 100644 index 00000000000..40fadb70272 --- /dev/null +++ b/packages/drafts-realm/CrmAccount/4802eeed-bec6-4d7a-8f05-6370866edd40.json @@ -0,0 +1,67 @@ +{ + "data": { + "type": "card", + "attributes": { + "accountName": "Cardstack Malaysia", + "accountAlias": "Cardstack Malaysia", + "description": "Cardstack Malaysia wants to sell x1000 card templates to be used in their card catalog", + "contactInformation": { + "salutation": "Mrs", + "firstName": "Justin", + "lastName": "Thong", + "address": { + "streetAddress": "Jalan Permatang Gedong, Taman Sejati Indah,", + "city": "Kuala Lumpur", + "region": "Kuala Lumpur", + "postalCode": "55200", + "poBoxNumber": null, + "country": "Malaysia" + }, + "phoneNumber": { + "country": 1, + "area": 212, + "number": 5551212 + }, + "email": { + "value": null + }, + "website": { + "value": null + } + }, + "billingAddress": { + "streetAddress": "Jalan Permatang Gedong, Taman Sejati Indah,", + "city": "Kuala Lumpur", + "region": "Kuala Lumpur", + "postalCode": "55200", + "poBoxNumber": null, + "country": "Malaysia" + }, + "numberOfEmployees": 4, + "thumbnailURL": null + }, + "relationships": { + "owner": { + "links": { + "self": "../MatrixUser/ca9dbfad-633a-4911-bc09-7fe3ebcb60c1" + } + }, + "parentAccount": { + "links": { + "self": null + } + }, + "company": { + "links": { + "self": null + } + } + }, + "meta": { + "adoptsFrom": { + "module": "../crm/account", + "name": "CrmAccount" + } + } + } +} \ No newline at end of file diff --git a/packages/drafts-realm/MatrixUser/ca9dbfad-633a-4911-bc09-7fe3ebcb60c1.json b/packages/drafts-realm/MatrixUser/ca9dbfad-633a-4911-bc09-7fe3ebcb60c1.json new file mode 100644 index 00000000000..86e6fb30069 --- /dev/null +++ b/packages/drafts-realm/MatrixUser/ca9dbfad-633a-4911-bc09-7fe3ebcb60c1.json @@ -0,0 +1,19 @@ +{ + "data": { + "type": "card", + "attributes": { + "username": "tintinthong", + "email": { + "value": "tintinthong@cardstack.com" + }, + "description": null, + "thumbnailURL": null + }, + "meta": { + "adoptsFrom": { + "module": "../matrix-user", + "name": "MatrixUser" + } + } + } +} \ No newline at end of file diff --git a/packages/drafts-realm/contact-card.gts b/packages/drafts-realm/contact-card.gts index 6d9db14d3d9..9ce562cfbaf 100644 --- a/packages/drafts-realm/contact-card.gts +++ b/packages/drafts-realm/contact-card.gts @@ -1,5 +1,4 @@ import StringField from 'https://cardstack.com/base/string'; -import NumberField from 'https://cardstack.com/base/number'; import { Component, CardDef, @@ -9,6 +8,7 @@ import { containsMany, linksToMany, } from 'https://cardstack.com/base/card-api'; +import { PhoneField } from './phone-number'; class Alias extends CardDef { static displayName = 'Alias'; @@ -26,19 +26,6 @@ class Alias extends CardDef { }; } -export class PhoneField extends FieldDef { - static displayName = 'Phone'; - @field country = contains(NumberField); - @field area = contains(NumberField); - @field number = contains(NumberField); - - static embedded = class Embedded extends Component { - - }; -} - export class EmergencyContactField extends FieldDef { static displayName = 'Emergency Contact'; @field name = contains(StringField); diff --git a/packages/drafts-realm/crm/account.gts b/packages/drafts-realm/crm/account.gts new file mode 100644 index 00000000000..e3d3ca93c21 --- /dev/null +++ b/packages/drafts-realm/crm/account.gts @@ -0,0 +1,65 @@ +import { + contains, + field, + CardDef, + linksTo, + Component, +} from 'https://cardstack.com/base/card-api'; + +import NumberField from 'https://cardstack.com/base/number'; +import StringField from 'https://cardstack.com/base/string'; +import { Address } from '../address'; +import { Contact } from './contact'; +import { MatrixUser } from '../matrix-user'; +import { GridContainer } from '@cardstack/boxel-ui/components'; + +export class Company extends CardDef { + static displayName = 'Company'; + @field name = contains(StringField); + @field regstrationNumber = contains(StringField); + @field address = contains(Address); + @field contactPerson = contains(Contact); + + @field title = contains(StringField, { + computeVia: function (this: Company) { + return `${this.name} Company`; + }, + }); + + static embedded = class Embedded extends Component { + + }; +} + +export class CrmAccount extends CardDef { + static displayName = 'Crm Account'; + @field owner = linksTo(MatrixUser); + @field accountName = contains(StringField); + @field accountAlias = contains(StringField); + @field description = contains(StringField); + @field contactInformation = contains(Contact); + @field billingAddress = contains(Address); + @field shippingAddress = contains(Address, { + computeVia: function (this: CrmAccount) { + return this.billingAddress; + }, + }); + @field numberOfEmployees = contains(NumberField); + @field parentAccount = linksTo(() => CrmAccount); + @field company = linksTo(Company); + @field title = contains(StringField, { + computeVia: function (this: CrmAccount) { + return this.accountName; + }, + }); + + static embedded = class Embedded extends Component { + + }; +} diff --git a/packages/drafts-realm/crm/accounts-dashboard.gts b/packages/drafts-realm/crm/accounts-dashboard.gts new file mode 100644 index 00000000000..a7749d47d64 --- /dev/null +++ b/packages/drafts-realm/crm/accounts-dashboard.gts @@ -0,0 +1,118 @@ +import StringCard from 'https://cardstack.com/base/string'; +import { + Component, + CardDef, + field, + contains, + linksToMany, +} from 'https://cardstack.com/base/card-api'; +import { GridContainer } from '@cardstack/boxel-ui/components'; +import { CrmAccount } from './account'; + +class AccountsDashboardIsolated extends Component { + get getAccounts() { + let allAccounts = this.args.model.accounts || []; + return allAccounts; + } + + get getAccountsNames() { + let allAccounts = this.args.model.accounts || []; + return allAccounts.map((o) => o.accountName); + } + +} + +export class AccountsDashboard extends CardDef { + static displayName = 'Accounts Dashboard'; + @field title = contains(StringCard, { + computeVia: function (this: AccountsDashboard) { + return 'Crm Accounts Dashboard'; + }, + }); + + @field accounts = linksToMany(() => CrmAccount); + + static isolated = AccountsDashboardIsolated; + + static embedded = class Embedded extends Component { + + }; +} diff --git a/packages/drafts-realm/crm/contact.gts b/packages/drafts-realm/crm/contact.gts new file mode 100644 index 00000000000..8137223ef81 --- /dev/null +++ b/packages/drafts-realm/crm/contact.gts @@ -0,0 +1,38 @@ +import { + contains, + field, + FieldDef, + Component, +} from 'https://cardstack.com/base/card-api'; +import StringField from 'https://cardstack.com/base/string'; +import { WebUrl } from 'https://cardstack.com/base/web-url'; +import { Address } from '../address'; +import { EmailAddress } from 'https://cardstack.com/base/email'; +import { PhoneField } from '../phone-number'; + +export class Contact extends FieldDef { + // This is not a registered user, rather just a contact + @field salutation = contains(StringField); + @field firstName = contains(StringField); + @field lastName = contains(StringField); + @field address = contains(Address); + @field phoneNumber = contains(PhoneField); + @field email = contains(EmailAddress); + @field website = contains(WebUrl); + + @field name = contains(StringField, { + computeVia: function (this: Contact) { + return [this.salutation, this.firstName, this.lastName] + .filter(Boolean) + .join(' '); + }, + }); + + static embedded = class Embedded extends Component { + + }; +} diff --git a/packages/drafts-realm/crm/lead.gts b/packages/drafts-realm/crm/lead.gts new file mode 100644 index 00000000000..77b3670e8d4 --- /dev/null +++ b/packages/drafts-realm/crm/lead.gts @@ -0,0 +1,14 @@ +import { contains, field, FieldDef } from 'https://cardstack.com/base/card-api'; + +import { Contact } from './contact'; +// import { OptionList } from 'https://cardstack.com/base/select-list'; + +export class HotnessField extends FieldDef { + static displayName = 'Crm Lead'; + // @field hotness = contains(OptionList); +} + +export class Lead extends Contact { + static displayName = 'Crm Lead'; + @field hotness = contains(HotnessField); +} diff --git a/packages/drafts-realm/crm/opportunity.gts b/packages/drafts-realm/crm/opportunity.gts new file mode 100644 index 00000000000..a73ea99c3a1 --- /dev/null +++ b/packages/drafts-realm/crm/opportunity.gts @@ -0,0 +1,58 @@ +import { + contains, + field, + CardDef, + FieldDef, + linksTo, +} from 'https://cardstack.com/base/card-api'; + +import NumberField from 'https://cardstack.com/base/number'; +import StringField from 'https://cardstack.com/base/string'; +import { WebUrl } from 'https://cardstack.com/base/web-url'; +import DatetimeField from 'https://cardstack.com/base/datetime'; +import { CrmAccount } from './account'; +import { MatrixUser } from '../matrix-user'; + +export class Opportunity extends CardDef { + static displayName = 'Crm Opportunity'; + @field name = contains(StringField); + @field owner = linksTo(MatrixUser); + @field account = linksTo(CrmAccount); + @field closeDate = contains(DatetimeField); + @field status = contains(OpportunityStatus); + @field leadSource = linksTo(Lead); + @field amount = linksTo(Currency); +} + +class OpportunityStage extends FieldDef { + // Qualify: Determine if the potential customer has a need for your product or service, and if they are ready to buy. + // Negotiate: Discuss terms, prices, and conditions after identifying a mutual interest. + // Meet & Present: Engage directly with the potential customer to present your solution. + // Closed Won: The stage where the deal is successfully completed, and the sale is made. + // Closed Lost: The opportunity did not result in a sale and is considered lost. +} + +class OpportunityStatus extends FieldDef { + @field stage = contains(OpportunityStage); + @field probability = contains(PercentField); + @field forecastCategory = contains(ForecastCategory); +} +class ForecastCategory extends FieldDef { + // Omitted: Excludes the opportunity from forecasts, often used for lost or inactive deals. + // Pipeline: Opportunities that are in the early stages, indicating potential future sales. + // Best Case: Opportunities that are more likely to close, beyond just being in the pipeline. + // Commit: Highly likely to close, where the sales team commits to the forecast. + // Closed: The opportunity has either been won or lost. +} + +class PercentField extends NumberField { + //validation for percentage +} + +class Lead extends CardDef {} + +class Currency extends CardDef { + @field tickSymbol = contains(StringField); + @field amount = contains(NumberField); + @field logoURL = contains(WebUrl); +} diff --git a/packages/drafts-realm/matrix-user.gts b/packages/drafts-realm/matrix-user.gts new file mode 100644 index 00000000000..a34e6515e6a --- /dev/null +++ b/packages/drafts-realm/matrix-user.gts @@ -0,0 +1,31 @@ +import { + field, + contains, + Component, +} from 'https://cardstack.com/base/card-api'; +import { CardDef } from 'https://cardstack.com/base/card-api'; +import { GridContainer } from '@cardstack/boxel-ui/components'; + +import StringField from 'https://cardstack.com/base/string'; +import { EmailAddress } from 'https://cardstack.com/base/email'; + +export class MatrixUser extends CardDef { + static displayName = 'Matrix User'; + @field username = contains(StringField); + @field email = contains(EmailAddress); + //threePids? + + @field title = contains(StringField, { + computeVia: function (this: MatrixUser) { + return this.username; + }, + }); + + static embedded = class Embedded extends Component { + + }; +} diff --git a/packages/drafts-realm/phone-number.gts b/packages/drafts-realm/phone-number.gts new file mode 100644 index 00000000000..1a244b668ac --- /dev/null +++ b/packages/drafts-realm/phone-number.gts @@ -0,0 +1,20 @@ +import NumberField from 'https://cardstack.com/base/number'; +import { + Component, + FieldDef, + field, + contains, +} from 'https://cardstack.com/base/card-api'; + +export class PhoneField extends FieldDef { + static displayName = 'Phone'; + @field country = contains(NumberField); + @field area = contains(NumberField); + @field number = contains(NumberField); + + static embedded = class Embedded extends Component { + + }; +} From a9025c2e94a29d280428039e128249fc561695a1 Mon Sep 17 00:00:00 2001 From: lucas Date: Wed, 29 May 2024 10:56:11 +0800 Subject: [PATCH 02/47] create: contact form --- .../4530dcff-062d-4a69-911a-db32da501eab.json | 34 ++ packages/drafts-realm/address-info.gts | 495 ++++++++++++++++++ packages/drafts-realm/contact-form.gts | 273 ++++++++++ packages/drafts-realm/user-email.gts | 127 +++++ packages/drafts-realm/user-name.gts | 130 +++++ 5 files changed, 1059 insertions(+) create mode 100644 packages/drafts-realm/ContactForm/4530dcff-062d-4a69-911a-db32da501eab.json create mode 100644 packages/drafts-realm/address-info.gts create mode 100644 packages/drafts-realm/contact-form.gts create mode 100644 packages/drafts-realm/user-email.gts create mode 100644 packages/drafts-realm/user-name.gts diff --git a/packages/drafts-realm/ContactForm/4530dcff-062d-4a69-911a-db32da501eab.json b/packages/drafts-realm/ContactForm/4530dcff-062d-4a69-911a-db32da501eab.json new file mode 100644 index 00000000000..6a664ca1b45 --- /dev/null +++ b/packages/drafts-realm/ContactForm/4530dcff-062d-4a69-911a-db32da501eab.json @@ -0,0 +1,34 @@ +{ + "data": { + "type": "card", + "attributes": { + "title": "Application of Job", + "name": { + "salutation": "Mr.", + "firstName": "Law", + "lastName": "Yung xxx" + }, + "email": "lorem@gmail.com", + "phone": "0103819999", + "fax": "+609092312", + "department": "IT", + "addressInfo": { + "address": "1 Chome-3-8 Hashiba Higashinocho", + "zip": "570-0031", + "city": "Moriguchi", + "state": "Ōsaka Prefecture", + "country": "Japan", + "countryCode": "JP", + "stateCode": "27" + }, + "description": null, + "thumbnailURL": null + }, + "meta": { + "adoptsFrom": { + "module": "../contact-form", + "name": "ContactForm" + } + } + } +} \ No newline at end of file diff --git a/packages/drafts-realm/address-info.gts b/packages/drafts-realm/address-info.gts new file mode 100644 index 00000000000..90013d11619 --- /dev/null +++ b/packages/drafts-realm/address-info.gts @@ -0,0 +1,495 @@ +import { FieldDef, field, contains } from 'https://cardstack.com/base/card-api'; +import { Component } from 'https://cardstack.com/base/card-api'; +import StringField from 'https://cardstack.com/base/string'; +import { not } from '@cardstack/boxel-ui/helpers'; +import { + BoxelSelect, + FieldContainer, + CardContainer, +} from '@cardstack/boxel-ui/components'; +import { task } from 'ember-concurrency'; +import { action } from '@ember/object'; +import type Owner from '@ember/owner'; +import { tracked } from '@glimmer/tracking'; + +interface Timezone { + zoneName: string; + gmtOffset: number; + gmtOffsetName: string; + abbreviation: string; + tzName: string; +} + +interface Translations { + kr: string; + 'pt-BR': string; + pt: string; + nl: string; + hr: string; + fa: string; + de: string; + es: string; + fr: string; + ja: string; + it: string; + cn: string; + tr: string; +} + +interface CountrySignature { + id: number; + name: string; + iso3: string; + iso2: string; + numeric_code: string; + phone_code: string; + capital: string; + currency: string; + currency_name: string; + currency_symbol: string; + tld: string; + native: string; + region: string; + region_id: string; + subregion: string; + subregion_id: string; + nationality: string; + timezones: Timezone[]; + translations: Translations; + latitude: string; + longitude: string; + emoji: string; + emojiU: string; +} + +interface StateSignature { + id: number; + name: string; + country_id: number; + country_code: string; + country_name: string; + state_code: string; + type?: string | null; + latitude: string; + longitude: string; +} + +interface CitySignature { + id: number; + name: string; + state_id: number; + state_code: string; + state_name: string; + country_id: number; + country_code: string; + country_name: string; + latitude: string; + longitude: string; + wikiDataId: string; +} + +class View extends Component { + get addressInfo() { + let { address, zip, state, city, country } = this.args.model; + let arr = [address, zip, state, city, country]; + + return arr + .map((str: string | undefined) => str?.trim()) + .filter((str) => str && str.length > 0) + .join(', '); + } + + +} + +class Edit extends Component { + @tracked selectedCountryType = { + name: this.args.model.country || 'Select', + }; + @tracked selectedStateType = { + name: this.args.model.state || 'Select', + }; + @tracked selectedCityType = { + name: this.args.model.city || 'Select', + }; + + @tracked private allCountries = []; + @tracked private allStatesOfCountry = []; + @tracked private allCitiesOfState = []; + + get hasStates() { + return this.allStatesOfCountry.length > 0; + } + + get hasCities() { + return this.allCitiesOfState.length > 0; + } + + @action + async updateCountry(type: CountrySignature) { + this.args.model.countryCode = type.iso2; + this.selectedCountryType = type; + this.args.model.country = type.name; + + // reset state while country is changed + const states = await this.loadStates.perform(type.iso2); + + if (states.length > 0) { + this.updateState(states[0]); + } else { + this.selectedStateType = { name: 'Select' }; + this.args.model.stateCode = ''; + this.args.model.state = ''; + this.selectedCityType = { name: 'Select' }; + this.args.model.city = ''; + } + } + + @action + async updateState(type: StateSignature) { + this.args.model.stateCode = type.state_code; + this.selectedStateType = type; + this.args.model.state = type.name; + + // reset city while state is changed + const cities = await this.loadCities.perform( + this.args.model.countryCode, + this.args.model.stateCode, + ); + + if (cities.length > 0) { + this.updateCity(cities[0]); + } else { + this.selectedCityType = { name: 'Select' }; + this.args.model.city = ''; + } + } + + @action + updateCity(type: CitySignature) { + console.log(type); + this.selectedCityType = type; + this.args.model.city = type.name; + } + + //query fetch + private loadCountry = task(async () => { + try { + let response = await fetch( + 'https://raw.githubusercontent.com/dr5hn/countries-states-cities-database/master/countries.json', + ); + if (!response.ok) { + throw new Error('Network response was not ok ' + response.statusText); + } + let data = await response.json(); + + this.allCountries = data; + + return data; + } catch (error) { + console.error('loadCountry', error); + } + }); + + private loadStates = task(async (countryCode: string | null) => { + try { + let response = await fetch( + 'https://raw.githubusercontent.com/dr5hn/countries-states-cities-database/master/states.json', + ); + if (!response.ok) { + throw new Error('Network response was not ok ' + response.statusText); + } + let data = await response.json(); + + if (!countryCode) { + return data; + } + + let filterData = data.filter( + (state: StateSignature) => state.country_code === countryCode, + ); + + this.allStatesOfCountry = filterData; + + return filterData; + } catch (error) { + console.error('loadStates', error); + } + }); + + private loadCities = task( + async (countryCode: string | null, stateCode: string | null) => { + try { + let response = await fetch( + 'https://raw.githubusercontent.com/dr5hn/countries-states-cities-database/master/cities.json', + ); + if (!response.ok) { + throw new Error('Network response was not ok ' + response.statusText); + } + + let data = await response.json(); + + if (!countryCode || !stateCode) { + return data; + } + + let filterData = data.filter( + (item: CitySignature) => + item.country_code === countryCode && item.state_code === stateCode, + ); + + this.allCitiesOfState = filterData; + + return filterData; + } catch (error) { + console.error('loadCities', error); + } + }, + ); + + constructor(owner: Owner, args: any) { + super(owner, args); + this.loadCountry.perform(); + this.loadStates.perform(this.args.model.countryCode); + this.loadCities.perform( + this.args.model.countryCode, + this.args.model.stateCode, + ); + } + + +} + +export class AddressInfo extends FieldDef { + static displayName = 'Mailing Address'; + @field address = contains(StringField, { + description: `Mailing Address`, + }); + @field zip = contains(StringField, { + description: `Mailing Zip/Postal Code`, + }); + @field city = contains(StringField, { + description: `Mailing City`, + }); + @field state = contains(StringField, { + description: `Mailing State/Province`, + }); + @field country = contains(StringField, { + description: `Mailing Country`, + }); + @field countryCode = contains(StringField, { + description: `Mailing Country Code`, + }); + @field stateCode = contains(StringField, { + description: `Mailing State Code`, + }); + + @field mapUrl = contains(StringField, { + computeVia: function (this: AddressInfo) { + let searchCountry = + this.address + + ' ' + + this.zip + + ' ' + + this.city + + ' ' + + this.state + + ' ' + + this.country; + + return `https://maps.google.com/maps?q=${searchCountry}&t=&z=13&ie=UTF8&iwloc=&output=embed`; + }, + }); + + static embedded = View; + static edit = Edit; +} diff --git a/packages/drafts-realm/contact-form.gts b/packages/drafts-realm/contact-form.gts new file mode 100644 index 00000000000..1609ffd5169 --- /dev/null +++ b/packages/drafts-realm/contact-form.gts @@ -0,0 +1,273 @@ +import { UserName } from './user-name'; +import { UserEmail } from './user-email'; +import { AddressInfo } from './address-info'; +import { CardDef, field, contains } from 'https://cardstack.com/base/card-api'; +import { Component } from 'https://cardstack.com/base/card-api'; +import StringField from 'https://cardstack.com/base/string'; +import { + FieldContainer, + CardContainer, + IconButton, +} from '@cardstack/boxel-ui/components'; +import { Sparkle } from '@cardstack/boxel-ui/icons'; + +class Isolated extends Component { + get hasTitleField() { + return this.args.model.title && this.args.model.title.length > 0; + } + + +} + +class View extends Component { + +} + +class Edit extends Component { + +} + +export class ContactForm extends CardDef { + @field title = contains(StringField, { + description: `Contact Form Title`, + }); + + @field name = contains(UserName, { + description: `User's Full Name`, + }); + + @field email = contains(UserEmail, { + description: `User's Email`, + }); + + @field phone = contains(StringField, { + description: `User's phone number`, + }); + + @field fax = contains(StringField, { + description: `User's Fax Number`, + }); + @field department = contains(StringField, { + description: `User's Department`, + }); + + @field addressInfo = contains(AddressInfo, { + description: `User's AddressInfo`, + }); + + static displayName = 'Contact Form'; + static isolated = Isolated; + static embedded = View; + static atom = View; + static edit = Edit; +} diff --git a/packages/drafts-realm/user-email.gts b/packages/drafts-realm/user-email.gts new file mode 100644 index 00000000000..06d2894014b --- /dev/null +++ b/packages/drafts-realm/user-email.gts @@ -0,0 +1,127 @@ +import { + FieldDef, + primitive, + useIndexBasedKey, + deserialize, + BaseInstanceType, + BaseDefConstructor, + queryableValue, + field, + contains, + StringField, +} from 'https://cardstack.com/base/card-api'; +import { Component } from 'https://cardstack.com/base/card-api'; +import { BoxelInput, FieldContainer } from '@cardstack/boxel-ui/components'; +import { TextInputValidator } from '../base/text-input-validator'; + +function isValidEmail(email: string): boolean { + let emailRegex = new RegExp( + "^[-!#$%&'*+\\/0-9=?A-Z^_a-z{|}~](\\.?[-!#$%&'*+\\/0-9=?A-Z^_a-z`{|}~])*@[a-zA-Z0-9](-*\\.?[a-zA-Z0-9])*\\.[a-zA-Z](-?[a-zA-Z0-9])+", + ); + + if (!email) return false; + if (email.length > 254) return false; + if (!emailRegex.test(email)) return false; + + let e = email.split('@'); + if (e[0].length > 64) return false; + if ( + e[1].split('.').some(function (part) { + return part.length > 63; + }) + ) + return false; + return true; +} + +function validate(val: string | null): string | null { + if (!val) return null; + + if (!isValidEmail(val)) { + return 'Invalid email format. Please use a valid email address format (e.g., example@example.com'; + } + + return null; +} + +function serialize(val: string | null): string | undefined { + return val ? val : undefined; +} + +function _deserialize(string: string | null): string | null { + let errorMessage = validate(string); + + if (errorMessage) { + return null; + } else { + return string; + } +} + +class View extends Component { + + + textInputValidator: TextInputValidator = new TextInputValidator( + () => this.args.model, + (inputVal) => this.args.set(inputVal), + _deserialize, + serialize, + validate, + ); +} + +class Edit extends Component { + + + textInputValidator: TextInputValidator = new TextInputValidator( + () => this.args.model, + (inputVal) => this.args.set(inputVal), + _deserialize, + serialize, + validate, + ); +} + +export class UserEmail extends FieldDef { + static displayName = 'User Email'; + @field email = contains(StringField, { + description: `Email`, + }); + + static [primitive]: string; + static [useIndexBasedKey]: never; + static async [deserialize]( + this: T, + string: any, + ): Promise> { + return _deserialize(string) as BaseInstanceType; + } + static [queryableValue](val: string | undefined): string | undefined { + return serialize(val ?? null); + } + + static embedded = View; + static atom = View; + static edit = Edit; +} diff --git a/packages/drafts-realm/user-name.gts b/packages/drafts-realm/user-name.gts new file mode 100644 index 00000000000..1607170f928 --- /dev/null +++ b/packages/drafts-realm/user-name.gts @@ -0,0 +1,130 @@ +import { FieldDef, field, contains } from 'https://cardstack.com/base/card-api'; +import { Component } from 'https://cardstack.com/base/card-api'; +import StringField from 'https://cardstack.com/base/string'; +import { + BoxelSelect, + FieldContainer, + CardContainer, +} from '@cardstack/boxel-ui/components'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; + +interface Salutation { + name: string; +} + +class View extends Component { + get isValidVal() { + let { salutation, firstName, lastName } = this.args.model; + + if (!salutation || salutation === 'Select' || salutation === 'None') + return false; + if (!firstName) return false; + if (!lastName) return false; + return true; + } + + +} + +class Edit extends Component { + @tracked selectedSalutationType = { + name: this.args.model.salutation || 'Select', + }; + + @tracked placeholder = this.args.model.salutation || 'Select'; + + @tracked salutationType = [ + { name: 'None' }, + { name: 'Mr.' }, + { name: 'Ms.' }, + { name: 'Mrs.' }, + { name: 'Dr.' }, + { name: 'Prof.' }, + { name: 'Mx.' }, + ] as Array; + + @action updateSalutationType(type: { name: string }) { + this.selectedSalutationType = type; + this.args.model.salutation = type.name; + } + + +} + +export class UserName extends FieldDef { + static displayName = 'User Name'; + @field salutation = contains(StringField, { + description: `User's Salutation`, + }); + @field firstName = contains(StringField, { + description: `User's First Name`, + }); + @field lastName = contains(StringField, { + description: `User's Last Name`, + }); + + @field fullName = contains(StringField, { + description: `User's Full Name`, + computeVia: function (this: UserName) { + return [this.salutation, this.firstName, this.lastName] + .filter(Boolean) + .join(' '); + }, + }); + + static atom = View; + static embedded = View; + static edit = Edit; +} From 5cdd345c1a4310bc7e861dd8ab439839ac378b55 Mon Sep 17 00:00:00 2001 From: lucas Date: Fri, 31 May 2024 18:27:47 +0800 Subject: [PATCH 03/47] create: task-form --- .../c06a01cc-0a93-4b99-8575-d1bd455da520.json | 36 ++ packages/drafts-realm/task-form.gts | 315 ++++++++++++++++++ 2 files changed, 351 insertions(+) create mode 100644 packages/drafts-realm/TaskForm/c06a01cc-0a93-4b99-8575-d1bd455da520.json create mode 100644 packages/drafts-realm/task-form.gts diff --git a/packages/drafts-realm/TaskForm/c06a01cc-0a93-4b99-8575-d1bd455da520.json b/packages/drafts-realm/TaskForm/c06a01cc-0a93-4b99-8575-d1bd455da520.json new file mode 100644 index 00000000000..fd413545823 --- /dev/null +++ b/packages/drafts-realm/TaskForm/c06a01cc-0a93-4b99-8575-d1bd455da520.json @@ -0,0 +1,36 @@ +{ + "data": { + "type": "card", + "attributes": { + "subject": { + "title": "Send Quote" + }, + "dueDate": "2024-05-16", + "comments": "Hi dear, please send quote to us asap.", + "relatedTo": { + "account": "Cardstack Malaysia" + }, + "title": null, + "description": null, + "thumbnailURL": null + }, + "relationships": { + "relatedTo.accounts.0": { + "links": { + "self": "http://localhost:4201/drafts/CardDef/7298c9c6-2964-41a0-87f7-42127a16e12b" + } + }, + "relatedTo.accounts.1": { + "links": { + "self": "http://localhost:4201/drafts/CrmAccount/4802eeed-bec6-4d7a-8f05-6370866edd40" + } + } + }, + "meta": { + "adoptsFrom": { + "module": "../task-form", + "name": "TaskForm" + } + } + } +} \ No newline at end of file diff --git a/packages/drafts-realm/task-form.gts b/packages/drafts-realm/task-form.gts new file mode 100644 index 00000000000..af97f63357e --- /dev/null +++ b/packages/drafts-realm/task-form.gts @@ -0,0 +1,315 @@ +import MarkdownField from 'https://cardstack.com/base/markdown'; +import { + CardDef, + FieldDef, + contains, + field, + linksToMany, +} from 'https://cardstack.com/base/card-api'; +import { Component } from 'https://cardstack.com/base/card-api'; +import DateCard from 'https://cardstack.com/base/date'; +import StringField from 'https://cardstack.com/base/string'; +import { + BoxelSelect, + CardContainer, + FieldContainer, +} from '@cardstack/boxel-ui/components'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { CrmAccount } from './crm/account'; + +interface CategorySignature { + name: string; +} + +/* Account */ +class EditSecForAccountField extends Component { + @tracked selectedAccount = { + name: this.args.model.account || 'Select', + }; + + @tracked accountPlaceholder = this.args.model.account || 'Select'; + + get getAccountsNames() { + let allAccounts = this.args.model.accounts || []; + return allAccounts.map((o) => ({ name: o.accountName })); + } + + @action updateAccount(type: { name: string }) { + this.selectedAccount = type; + this.args.model.account = type.name; + } + + +} + +class EmbeddedSecForAccountField extends Component { + +} + +class AccountField extends FieldDef { + @field accounts = linksToMany(() => CrmAccount, { + description: `CRM Accounts`, + }); + @field account = contains(StringField, { + description: `Account Name`, + }); + static edit = EditSecForAccountField; + static embedded = EmbeddedSecForAccountField; +} + +/* Subject */ +class EditSecForSubjectField extends Component { + @tracked selectedSubject = { + name: this.args.model.title || 'Select', + }; + + @tracked subjectPlaceholder = this.args.model.title || 'Select'; + + @tracked subjectOptions = [ + { name: 'None' }, + { name: 'Email' }, + { name: 'Call' }, + { name: 'Send Letter' }, + { name: 'Send Quote' }, + { name: 'Other' }, + ] as Array; + + @action updateSubject(type: { name: string }) { + this.selectedSubject = type; + this.args.model.title = type.name; + } + + +} + +class EmbeddedSecForSubjectField extends Component { + +} + +class SubjectField extends FieldDef { + @field title = contains(StringField, { + description: `Selected Subject`, + }); + + static edit = EditSecForSubjectField; + static embedded = EmbeddedSecForSubjectField; +} + +/* Task Form */ +class IsolatedSecForTaskForm extends Component { + +} + +class ViewSecForTaskForm extends Component { + +} + +class EditSecForTaskForm extends Component { + +} + +export class TaskForm extends CardDef { + static displayName = 'Task Form'; + @field subject = contains(SubjectField, { + description: `Subject`, + }); + @field dueDate = contains(DateCard, { + description: `Due Date`, + }); + @field comments = contains(MarkdownField, { + description: `Comments`, + }); + @field relatedTo = contains(AccountField, { + description: `Related to Which Account`, + }); + + static isolated = IsolatedSecForTaskForm; + static atom = ViewSecForTaskForm; + static embedded = ViewSecForTaskForm; + static edit = EditSecForTaskForm; +} From 3236e6d0e44222198025a6444951f889778c58d6 Mon Sep 17 00:00:00 2001 From: lucas Date: Fri, 31 May 2024 00:23:46 +0800 Subject: [PATCH 04/47] create: opportunity-form --- .../b5900a19-cb91-48cb-b083-e04e38eedbaf.json | 44 ++ packages/drafts-realm/opportunity-form.gts | 389 ++++++++++++++++++ 2 files changed, 433 insertions(+) create mode 100644 packages/drafts-realm/OpportunityForm/b5900a19-cb91-48cb-b083-e04e38eedbaf.json create mode 100644 packages/drafts-realm/opportunity-form.gts diff --git a/packages/drafts-realm/OpportunityForm/b5900a19-cb91-48cb-b083-e04e38eedbaf.json b/packages/drafts-realm/OpportunityForm/b5900a19-cb91-48cb-b083-e04e38eedbaf.json new file mode 100644 index 00000000000..de779f69e3f --- /dev/null +++ b/packages/drafts-realm/OpportunityForm/b5900a19-cb91-48cb-b083-e04e38eedbaf.json @@ -0,0 +1,44 @@ +{ + "data": { + "type": "card", + "attributes": { + "opportunityName": "Business Opportunity", + "accountName": { + "account": "Acme Corporation" + }, + "closeDate": "2024-08-02", + "amount": { + "currency": "RM", + "amount": 123123 + }, + "description": null, + "stage": { + "category": "Closed Won" + }, + "percentage": null, + "forecastCategory": { + "category": "Commit" + }, + "title": null, + "thumbnailURL": null + }, + "relationships": { + "accountName.accounts.0": { + "links": { + "self": "http://localhost:4201/drafts/CardDef/7298c9c6-2964-41a0-87f7-42127a16e12b" + } + }, + "accountName.accounts.1": { + "links": { + "self": "http://localhost:4201/drafts/CrmAccount/4802eeed-bec6-4d7a-8f05-6370866edd40" + } + } + }, + "meta": { + "adoptsFrom": { + "module": "../opportunity-form", + "name": "OpportunityForm" + } + } + } +} \ No newline at end of file diff --git a/packages/drafts-realm/opportunity-form.gts b/packages/drafts-realm/opportunity-form.gts new file mode 100644 index 00000000000..e274d47bd76 --- /dev/null +++ b/packages/drafts-realm/opportunity-form.gts @@ -0,0 +1,389 @@ +import MarkdownField from 'https://cardstack.com/base/markdown'; +import { + CardDef, + FieldDef, + contains, + field, + linksToMany, +} from 'https://cardstack.com/base/card-api'; +import { Component } from 'https://cardstack.com/base/card-api'; +import DateCard from 'https://cardstack.com/base/date'; +import StringField from 'https://cardstack.com/base/string'; +import { + BoxelSelect, + CardContainer, + FieldContainer, + GridContainer, +} from '@cardstack/boxel-ui/components'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { CrmAccount } from './crm/account'; +import NumberField from '../base/number'; + +interface CategorySignature { + name: string; +} + +/* Stage */ +class EmbeddedSecForForecastField extends Component { + +} + +class EditSecForForecastField extends Component { + @tracked selectedCategory = { + name: this.args.model.category || 'None', + }; + + @tracked categoryPlaceholder = this.args.model.category || 'None'; + + @tracked categoryOptions = [ + { name: 'None' }, + { name: 'Omitted' }, + { name: 'Pipeline' }, + { name: 'Best Case' }, + { name: 'Commit' }, + { name: 'Closed' }, + ] as Array; + + @action updateCategory(type: { name: string }) { + this.selectedCategory = type; + this.args.model.category = type.name; + } + + +} + +class ForecastField extends FieldDef { + @field category = contains(StringField, { + description: `Selected Category`, + }); + + static edit = EditSecForForecastField; + static embedded = EmbeddedSecForForecastField; +} + +/* Stage */ +class EmbeddedSecForStageField extends Component { + +} + +class EditSecForStageField extends Component { + @tracked selectedCategory = { + name: this.args.model.category || 'None', + }; + + @tracked categoryPlaceholder = this.args.model.category || 'None'; + + @tracked categoryOptions = [ + { name: 'None' }, + { name: 'Qualification' }, + { name: 'Needs Analysis' }, + { name: 'Proposal' }, + { name: 'Negotiation' }, + { name: 'Closed Won' }, + { name: 'Closed Lost' }, + ] as Array; + + @action updateCategory(type: { name: string }) { + this.selectedCategory = type; + this.args.model.category = type.name; + } + + +} + +class StageField extends FieldDef { + @field category = contains(StringField, { + description: `Selected Category`, + }); + + static edit = EditSecForStageField; + static embedded = EmbeddedSecForStageField; +} + +/* Account */ +class EditSecForAccountField extends Component { + @tracked selectedAccount = { + name: this.args.model.account || 'Select', + }; + + @tracked accountPlaceholder = this.args.model.account || 'Select'; + + get getAccountsNames() { + let allAccounts = this.args.model.accounts || []; + return allAccounts.map((o) => ({ name: o.accountName })); + } + + @action updateAccount(type: { name: string }) { + this.selectedAccount = type; + this.args.model.account = type.name; + } + + +} + +class EmbeddedSecForAccountField extends Component { + +} + +class AccountField extends FieldDef { + @field accounts = linksToMany(() => CrmAccount); + @field account = contains(StringField); + + static edit = EditSecForAccountField; + static embedded = EmbeddedSecForAccountField; +} + +/* Amount */ + +class EditSecForAmountField extends Component { + @tracked currencyOptions = ['USD', 'RM']; + + @tracked currencyPlaceHolder = 'RM'; + @tracked selectedCurrency = this.args.model.currency || 'RM'; + + @action selectExampleOnSelectItem(item: string) { + this.selectedCurrency = item; + } + + @action + updateCurrency(item: string) { + this.args.model.currency = item; + this.selectedCurrency = item; + } + + @action + updateAmount(val: number) { + this.args.model.amount = val; + } + + +} + +class AmountField extends FieldDef { + @field currency = contains(StringField); + @field amount = contains(NumberField); + + static edit = EditSecForAmountField; +} + +/* Opportunity Form */ +class EditSecForOpportunityForm extends Component { + +} + +export class OpportunityForm extends CardDef { + static displayName = 'Opportunity Form'; + @field opportunityName = contains(StringField, { + description: `Opportunity Name`, + }); + @field accountName = contains(AccountField, { + description: `Account Name`, + }); + @field closeDate = contains(DateCard, { + description: `Close Date`, + }); + + @field amount = contains(AmountField, { + description: `Amount`, + }); + @field description = contains(MarkdownField, { + description: `Description`, + }); + @field stage = contains(StageField, { + description: `Stage`, + }); + @field percentage = contains(NumberField, { + description: `Percentage`, + }); + @field forecastCategory = contains(ForecastField, { + description: `Forecast Category`, + }); + + static edit = EditSecForOpportunityForm; +} From 268c14444084094e95a589209f9d90c33a8eae35 Mon Sep 17 00:00:00 2001 From: lucas Date: Fri, 31 May 2024 17:27:54 +0800 Subject: [PATCH 05/47] create: view for cardDef and fieldDef, logic for percentage and amount --- .../b5900a19-cb91-48cb-b083-e04e38eedbaf.json | 14 +- packages/drafts-realm/opportunity-form.gts | 299 +++++++++++++++--- 2 files changed, 266 insertions(+), 47 deletions(-) diff --git a/packages/drafts-realm/OpportunityForm/b5900a19-cb91-48cb-b083-e04e38eedbaf.json b/packages/drafts-realm/OpportunityForm/b5900a19-cb91-48cb-b083-e04e38eedbaf.json index de779f69e3f..6ed1407c7ba 100644 --- a/packages/drafts-realm/OpportunityForm/b5900a19-cb91-48cb-b083-e04e38eedbaf.json +++ b/packages/drafts-realm/OpportunityForm/b5900a19-cb91-48cb-b083-e04e38eedbaf.json @@ -2,22 +2,24 @@ "data": { "type": "card", "attributes": { - "opportunityName": "Business Opportunity", + "opportunityName": "Business Opportunity A", "accountName": { - "account": "Acme Corporation" + "name": "Acme Corporation" }, "closeDate": "2024-08-02", "amount": { "currency": "RM", - "amount": 123123 + "total": 150000.5 }, - "description": null, + "description": "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum", "stage": { "category": "Closed Won" }, - "percentage": null, + "percentage": { + "percentage": 10.56 + }, "forecastCategory": { - "category": "Commit" + "category": "Closed" }, "title": null, "thumbnailURL": null diff --git a/packages/drafts-realm/opportunity-form.gts b/packages/drafts-realm/opportunity-form.gts index e274d47bd76..57734917336 100644 --- a/packages/drafts-realm/opportunity-form.gts +++ b/packages/drafts-realm/opportunity-form.gts @@ -10,10 +10,10 @@ import { Component } from 'https://cardstack.com/base/card-api'; import DateCard from 'https://cardstack.com/base/date'; import StringField from 'https://cardstack.com/base/string'; import { + BoxelInput, BoxelSelect, CardContainer, FieldContainer, - GridContainer, } from '@cardstack/boxel-ui/components'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; @@ -24,7 +24,75 @@ interface CategorySignature { name: string; } -/* Stage */ +export const formatNumber = (val: number) => { + let formatter = new Intl.NumberFormat('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }); + return formatter.format(val); +}; + +/* Percentage */ +class EditSecForPercentageField extends Component { + @action + updatePercentage(val: number) { + this.args.model.percentage = val; + } + + get getFormattedPercentage() { + if (!this.args.model.percentage) return null; + const formattedNumber = formatNumber( + Math.round(this.args.model.percentage), + ); + + return formattedNumber + '%'; + } + + +} + +class EmbdeddedSecForPercentageField extends Component { + get getFormattedPercentage() { + if (!this.args.model.percentage) return null; + const formattedNumber = formatNumber( + Math.round(this.args.model.percentage), + ); + + return formattedNumber + '%'; + } + + +} + +class PercentageField extends FieldDef { + @field percentage = contains(NumberField, { + description: `Percentage`, + }); + + static edit = EditSecForPercentageField; + static embedded = EmbdeddedSecForPercentageField; +} + +/* Forecast */ class EmbeddedSecForForecastField extends Component { } From 38323f0ae4479e1451348fa83716a0de5a288b32 Mon Sep 17 00:00:00 2001 From: lucas Date: Sun, 9 Jun 2024 19:00:43 +0800 Subject: [PATCH 14/47] create: lead-form --- .../ca5a2eb1-fb9e-4467-a7aa-54f3ec316ee9.json | 42 ++ packages/drafts-realm/currency-amount.gts | 121 +++++ packages/drafts-realm/lead-form.gts | 431 ++++++++++++++++++ 3 files changed, 594 insertions(+) create mode 100644 packages/drafts-realm/LeadForm/ca5a2eb1-fb9e-4467-a7aa-54f3ec316ee9.json create mode 100644 packages/drafts-realm/currency-amount.gts create mode 100644 packages/drafts-realm/lead-form.gts diff --git a/packages/drafts-realm/LeadForm/ca5a2eb1-fb9e-4467-a7aa-54f3ec316ee9.json b/packages/drafts-realm/LeadForm/ca5a2eb1-fb9e-4467-a7aa-54f3ec316ee9.json new file mode 100644 index 00000000000..e2a4177ed9a --- /dev/null +++ b/packages/drafts-realm/LeadForm/ca5a2eb1-fb9e-4467-a7aa-54f3ec316ee9.json @@ -0,0 +1,42 @@ +{ + "data": { + "type": "card", + "attributes": { + "name": { + "salutation": "Mr.", + "firstName": "Loo", + "lastName": "Mok Sun" + }, + "company": "Loo Holding", + "title": "Title Here", + "website": "www.loobholding.com", + "description": "Description Here", + "leadStatus": "New", + "phone": "0109090000", + "email": "loobholding@xxxx.com", + "addressInfo": { + "address": "14, Jalan Teknologi, Taman Sains", + "zip": "47810", + "city": "Petaling Jaya", + "state": "Selangor", + "country": "Malaysia", + "countryCode": "MY", + "stateCode": "10" + }, + "noOfEmployees": 500.5, + "annualRevenue": { + "currency": "RM", + "totalAmount": 150300 + }, + "leadSource": "Advertisement", + "industry": "Banking", + "thumbnailURL": null + }, + "meta": { + "adoptsFrom": { + "module": "../lead-form", + "name": "LeadForm" + } + } + } +} \ No newline at end of file diff --git a/packages/drafts-realm/currency-amount.gts b/packages/drafts-realm/currency-amount.gts new file mode 100644 index 00000000000..b340e42f8d9 --- /dev/null +++ b/packages/drafts-realm/currency-amount.gts @@ -0,0 +1,121 @@ +import { + Component, + FieldDef, + contains, + field, +} from 'https://cardstack.com/base/card-api'; +import StringField from 'https://cardstack.com/base/string'; +import NumberField from 'https://cardstack.com/base/number'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { + BoxelInput, + BoxelSelect, + CardContainer, + FieldContainer, +} from '@cardstack/boxel-ui/components'; + +const formatNumber = (val: number) => { + let formatter = new Intl.NumberFormat('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }); + return formatter.format(val); +}; + +class EmbeddedSecForAmount extends Component { + get getFormattedAmount() { + if (!this.args.model.totalAmount) return null; + const formattedNumber = formatNumber(this.args.model.totalAmount); + return formattedNumber; + } + + +} + +class EditSecForAmount extends Component { + @tracked currencyOptions = ['Select', 'RM', 'USD']; + get selectedCurrency() { + return this.args.model.currency || this.currencyOptions[0] || 'Select'; + } + + @action + updateCurrency(item: string) { + this.args.model.currency = item; + } + + @action + updateAmount(val: number) { + this.args.model.totalAmount = val; + } + + get getFormattedAmount() { + if (!this.args.model.totalAmount) return null; + const formattedNumber = formatNumber(this.args.model.totalAmount); + return formattedNumber; + } + + +} + +export class CurrencyAmount extends FieldDef { + static displayName = 'Currency Amount'; + @field currency = contains(StringField, { + description: `Currency`, + }); + + @field totalAmount = contains(NumberField, { + description: `Total Amount`, + }); + + static embedded = EmbeddedSecForAmount; + static edit = EditSecForAmount; +} diff --git a/packages/drafts-realm/lead-form.gts b/packages/drafts-realm/lead-form.gts new file mode 100644 index 00000000000..0037c9eb59a --- /dev/null +++ b/packages/drafts-realm/lead-form.gts @@ -0,0 +1,431 @@ +import MarkdownField from 'https://cardstack.com/base/markdown'; +import { + BaseDefComponent, + CardDef, + contains, + field, +} from 'https://cardstack.com/base/card-api'; +import StringField from 'https://cardstack.com/base/string'; +import NumberField from 'https://cardstack.com/base/number'; +import { UserName } from './user-name'; +import { UserEmail } from './user-email'; +import { AddressInfo } from './address-info'; +import { Component } from 'https://cardstack.com/base/card-api'; +import { CardContainer, FieldContainer } from '@cardstack/boxel-ui/components'; +import { BoxelSelect } from '@cardstack/boxel-ui/components'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { BoxelInput } from '@cardstack/boxel-ui/components'; +import { CurrencyAmount } from './currency-amount'; + +interface CategorySignature { + name: string; +} + +class IsolatedSecForLeadForm extends Component { + get getFormattedNoOfEmployees() { + if (!this.args.model.noOfEmployees) return null; + return Math.round(this.args.model.noOfEmployees); + } + + +} + +class ViewSecForLeadForm extends Component { + +} + +class EditSecFoLeadForm extends Component { + /* Lead Status Options */ + get selectedLeadStatus() { + return { name: this.args.model.leadStatus || 'None' }; + } + + @tracked leadStatusOptions = [ + { name: 'None' }, + { name: 'New' }, + { name: 'Working' }, + { name: 'Nurturing' }, + { name: 'Qualified' }, + { name: 'Unqualified' }, + ] as Array; + + @action updateLeadStatus(type: { name: string }) { + this.args.model.leadStatus = type.name; + } + + /* No Of Employees */ + @action updateNoOfEmployees(val: number) { + this.args.model.noOfEmployees = val; + } + + get getFormattedNoOfEmployees() { + if (!this.args.model.noOfEmployees) return null; + return Math.round(this.args.model.noOfEmployees); + } + + /* Lead Source Options */ + get selectedLeadSource() { + return { name: this.args.model.leadSource || 'None' }; + } + + @tracked leadSourceOptions = [ + { name: 'None' }, + { name: 'Advertisement' }, + { name: 'Employee Referral' }, + { name: 'External Referral' }, + { name: 'Partner' }, + { name: 'Public Relations' }, + { name: 'Seminar - Internal' }, + { name: 'Seminar - Partner' }, + { name: 'Trade Show' }, + { name: 'Web' }, + { name: 'Word of mouth' }, + { name: 'Other' }, + ] as Array; + + @action updateLeadSource(type: { name: string }) { + this.args.model.leadSource = type.name; + } + + /* Industry Options */ + get selectedIndustry() { + return { name: this.args.model.industry || 'None' }; + } + + @tracked industryOptions = [ + { name: 'None' }, + { name: 'Agriculture' }, + { name: 'Apparel' }, + { name: 'Banking' }, + { name: 'Biotechnology' }, + { name: 'Chemicals' }, + { name: 'Communications' }, + { name: 'Construction' }, + { name: 'Consulting' }, + { name: 'Education' }, + { name: 'Electronics' }, + { name: 'Energy' }, + { name: 'Engineering' }, + { name: 'Entertainment' }, + { name: 'Environmental' }, + { name: 'Finance' }, + { name: 'Food & Beverage' }, + { name: 'Government' }, + { name: 'Healthcare' }, + { name: 'Hospitality' }, + { name: 'Insurance' }, + { name: 'Machinery' }, + { name: 'Manufacturing' }, + { name: 'Media' }, + { name: 'Not For Profit' }, + { name: 'Recreation' }, + { name: 'Retail' }, + { name: 'Shipping' }, + { name: 'Technology' }, + { name: 'Telecommunications' }, + { name: 'Transportation' }, + { name: 'Utilities' }, + { name: 'Others' }, + ] as Array; + + @action updateIndustry(type: { name: string }) { + this.args.model.industry = type.name; + } + + +} + +export class LeadForm extends CardDef { + static displayName = 'Lead Form'; + + @field name = contains(UserName, { + description: `User's Full Name`, + }); + @field company = contains(StringField, { + description: `User's Company Name`, + }); + @field title = contains(StringField, { + description: `User's Title`, + }); + @field website = contains(StringField, { + description: `User's Website`, + }); + @field description = contains(MarkdownField, { + description: `User's Description`, + }); + @field leadStatus = contains(StringField, { + description: `Lead Status`, + }); + @field phone = contains(StringField, { + description: `User's phone number`, + }); + @field email = contains(UserEmail, { + description: `User's Email`, + }); + @field addressInfo = contains(AddressInfo, { + description: `User's AddressInfo`, + }); + @field noOfEmployees = contains(NumberField, { + description: `No Of Employees`, + }); + @field annualRevenue = contains(CurrencyAmount, { + description: `Annual Revenue`, + }); + @field leadSource = contains(StringField, { + description: `Lead Source`, + }); + @field industry = contains(StringField, { + description: `Industry`, + }); + + static isolated = IsolatedSecForLeadForm; + static atom = ViewSecForLeadForm; + static embedded = ViewSecForLeadForm; + static edit = EditSecFoLeadForm; +} From 883300143f7b858aea674107413453257e409a4f Mon Sep 17 00:00:00 2001 From: lucas Date: Mon, 10 Jun 2024 12:02:53 +0800 Subject: [PATCH 15/47] create: lead-form --- packages/drafts-realm/lead-form.gts | 87 +++++++++++++++++++++++++---- 1 file changed, 75 insertions(+), 12 deletions(-) diff --git a/packages/drafts-realm/lead-form.gts b/packages/drafts-realm/lead-form.gts index 0037c9eb59a..517d40eb152 100644 --- a/packages/drafts-realm/lead-form.gts +++ b/packages/drafts-realm/lead-form.gts @@ -109,7 +109,6 @@ class IsolatedSecForLeadForm extends Component { display: grid; gap: var(--boxel-sp-lg); overflow: hidden; - box-shadow: 0px !important; } section { overflow: hidden; @@ -157,25 +156,89 @@ class IsolatedSecForLeadForm extends Component { class ViewSecForLeadForm extends Component { } From ee5d95377d48c2e27c5b275d238697310d646c2e Mon Sep 17 00:00:00 2001 From: lucas Date: Mon, 10 Jun 2024 15:14:43 +0800 Subject: [PATCH 16/47] adjust css: form-row-full --- packages/drafts-realm/currency-amount.gts | 19 ++++- packages/drafts-realm/lead-form.gts | 93 ++++++++++++----------- 2 files changed, 63 insertions(+), 49 deletions(-) diff --git a/packages/drafts-realm/currency-amount.gts b/packages/drafts-realm/currency-amount.gts index b340e42f8d9..4e566a078ef 100644 --- a/packages/drafts-realm/currency-amount.gts +++ b/packages/drafts-realm/currency-amount.gts @@ -11,7 +11,6 @@ import { action } from '@ember/object'; import { BoxelInput, BoxelSelect, - CardContainer, FieldContainer, } from '@cardstack/boxel-ui/components'; @@ -58,7 +57,7 @@ class EditSecForAmount extends Component { } } diff --git a/packages/drafts-realm/lead-form.gts b/packages/drafts-realm/lead-form.gts index 517d40eb152..ab75630b58e 100644 --- a/packages/drafts-realm/lead-form.gts +++ b/packages/drafts-realm/lead-form.gts @@ -1,10 +1,5 @@ import MarkdownField from 'https://cardstack.com/base/markdown'; -import { - BaseDefComponent, - CardDef, - contains, - field, -} from 'https://cardstack.com/base/card-api'; +import { CardDef, contains, field } from 'https://cardstack.com/base/card-api'; import StringField from 'https://cardstack.com/base/string'; import NumberField from 'https://cardstack.com/base/number'; import { UserName } from './user-name'; @@ -160,54 +155,65 @@ class ViewSecForLeadForm extends Component {
About
- - <@fields.name @format='edit' /> - - - <@fields.company @format='edit' /> - - - <@fields.title @format='edit' /> - - - <@fields.website @format='edit' /> - - - <@fields.description @format='edit' /> - - - <@fields.leadStatus @format='edit' /> + + <@fields.name @format='edit' /> + + + <@fields.company @format='edit' /> + + + <@fields.title @format='edit' /> + + + <@fields.website @format='edit' /> + + + <@fields.description @format='edit' /> + + + <@fields.leadStatus @format='edit' /> +
Get In Touch
- - <@fields.phone @format='edit' /> - - - <@fields.email @format='edit' /> - - - <@fields.addressInfo @format='edit' /> + + <@fields.phone @format='edit' /> + + + <@fields.email @format='edit' /> + + + <@fields.addressInfo @format='edit' /> +
Segment
- - <@fields.noOfEmployees @format='edit' /> - - - <@fields.annualRevenue @format='edit' /> - - - <@fields.leadSource @format='edit' /> - - - <@fields.industry @format='edit' /> + + <@fields.noOfEmployees @format='edit' /> + + + <@fields.annualRevenue @format='edit' /> + + + <@fields.leadSource @format='edit' /> + + + <@fields.industry @format='edit' /> +
@@ -236,9 +242,6 @@ class ViewSecForLeadForm extends Component { border-radius: var(--boxel-border-radius); padding: var(--boxel-sp); } - label { - font-weight: 700; - } } From 99b7ae645e8c3f913e48710b7316e7e67fbef514 Mon Sep 17 00:00:00 2001 From: Richard Tan Date: Tue, 18 Jun 2024 13:03:03 +0800 Subject: [PATCH 17/47] feat: campaign form --- .../4ee4a9ff-f61b-476f-9140-27308934024c.json | 34 ++ .../b06e2425-70c2-47db-bafa-75a3ca349541.json | 34 ++ packages/drafts-realm/campaign-form.gts | 512 ++++++++++++++++++ 3 files changed, 580 insertions(+) create mode 100644 packages/drafts-realm/CampaignForm/4ee4a9ff-f61b-476f-9140-27308934024c.json create mode 100644 packages/drafts-realm/CampaignForm/b06e2425-70c2-47db-bafa-75a3ca349541.json create mode 100644 packages/drafts-realm/campaign-form.gts diff --git a/packages/drafts-realm/CampaignForm/4ee4a9ff-f61b-476f-9140-27308934024c.json b/packages/drafts-realm/CampaignForm/4ee4a9ff-f61b-476f-9140-27308934024c.json new file mode 100644 index 00000000000..a5f880038a6 --- /dev/null +++ b/packages/drafts-realm/CampaignForm/4ee4a9ff-f61b-476f-9140-27308934024c.json @@ -0,0 +1,34 @@ +{ + "data": { + "type": "card", + "attributes": { + "name": "Advertisement Campaign Form", + "status": "Planned", + "active": true, + "type": "Advertisement", + "description": "ssssss", + "start_date": "2024-06-30", + "end_date": "2024-06-30", + "number_sent": "200003212", + "expected_response_percentage": "222.53", + "expected_revenue": "2312", + "budgeted_cost": "1003039", + "actual_cost": "12034044", + "title": null, + "thumbnailURL": null + }, + "relationships": { + "parent_campaign": { + "links": { + "self": null + } + } + }, + "meta": { + "adoptsFrom": { + "module": "../campaign-form", + "name": "CampaignForm" + } + } + } +} \ No newline at end of file diff --git a/packages/drafts-realm/CampaignForm/b06e2425-70c2-47db-bafa-75a3ca349541.json b/packages/drafts-realm/CampaignForm/b06e2425-70c2-47db-bafa-75a3ca349541.json new file mode 100644 index 00000000000..70570d39235 --- /dev/null +++ b/packages/drafts-realm/CampaignForm/b06e2425-70c2-47db-bafa-75a3ca349541.json @@ -0,0 +1,34 @@ +{ + "data": { + "type": "card", + "attributes": { + "name": "Test Email Campaign 2", + "status": "Planned", + "active": false, + "type": "Advertisement", + "description": "This is a test form", + "start_date": "2024-06-22", + "end_date": "2024-06-29", + "number_sent": "1000", + "expected_response_percentage": "100", + "expected_revenue": "2000", + "budgeted_cost": "1000", + "actual_cost": "1000", + "title": null, + "thumbnailURL": null + }, + "relationships": { + "parent_campaign": { + "links": { + "self": "./4ee4a9ff-f61b-476f-9140-27308934024c" + } + } + }, + "meta": { + "adoptsFrom": { + "module": "../campaign-form", + "name": "CampaignForm" + } + } + } +} \ No newline at end of file diff --git a/packages/drafts-realm/campaign-form.gts b/packages/drafts-realm/campaign-form.gts new file mode 100644 index 00000000000..d91296d37d8 --- /dev/null +++ b/packages/drafts-realm/campaign-form.gts @@ -0,0 +1,512 @@ +import DateField from 'https://cardstack.com/base/date'; +import BooleanField from 'https://cardstack.com/base/boolean'; +import { + Component, + CardDef, + field, + contains, + StringField, + linksTo, +} from 'https://cardstack.com/base/card-api'; + +import { + FieldContainer, + BoxelSelect, + BoxelInput, +} from '@cardstack/boxel-ui/components'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { parse, format, isBefore, isAfter } from 'date-fns'; +import { fn } from '@ember/helper'; + +const dateFormat = `yyyy-MM-dd`; + +const sanitisedNumber = (inputText: string) => { + const sanitised = inputText + .replace(/ /g, '') + .replace(/,/g, '') + .replace(/%/g, '') + .replace(/RM/g, ''); + return !isNaN(parseFloat(sanitised)) ? parseFloat(sanitised) : 0; +}; + +const nearestDecimal = (num: number, decimalPlaces: number) => { + // https://stackoverflow.com/questions/11832914/how-to-round-to-at-most-2-decimal-places-if-necessary + const factorOfTen = Math.pow(10, decimalPlaces); + return Math.round(num * factorOfTen + Number.EPSILON) / factorOfTen; +}; + +const formatCurrency = ( + num: number | string | null | undefined, + locale: string = 'en-MY', + currency: string = 'MYR', +) => { + if (num === null || num === undefined) { + return ''; + } + + const formatter = new Intl.NumberFormat(locale, { + style: 'currency', + currency: currency, + minimumFractionDigits: 0, // No decimal places + maximumFractionDigits: 0, // No decimal places + }); + + let currentNumber = num; + if (typeof num === 'string') { + currentNumber = parseFloat(num); + } + + return formatter.format(currentNumber as number); +}; + +const formatNumberWithSeparator = ( + num: number | string | null | undefined, + isPercentage = false, +) => { + if (num === null || num === undefined) { + return ''; + } + + let currentNumber = num; + if (typeof num === 'string') { + currentNumber = parseFloat(num); + } + + return `${currentNumber.toLocaleString('en-US')}${isPercentage ? ' %' : ''}`; +}; + +class Isolated extends Component { + +} + +class Embedded extends Component { + +} + +class Edit extends Component { + @tracked name = this.args.model.name; + @tracked selectedStatus = { name: this.args.model.status }; + @tracked selectedActive = this.args.model.active; + @tracked selectedType = { name: this.args.model.type }; + @tracked description = this.args.model.description; + @tracked startDateString = this.args.model.start_date + ? format(this.args.model.start_date, dateFormat) + : null; + @tracked endDateString = this.args.model.end_date + ? format(this.args.model.end_date, dateFormat) + : null; + @tracked numberSentInputValue = formatNumberWithSeparator( + this.args.model.number_sent, + ); + @tracked expectedResponseInputValue = formatNumberWithSeparator( + this.args.model.expected_response_percentage, + true, + ); + @tracked expectedRevenueInputValue = formatCurrency( + this.args.model.expected_revenue, + ); + @tracked budgetedCostInputValue = formatCurrency( + this.args.model.budgeted_cost, + ); + @tracked actualCostInputValue = formatCurrency(this.args.model.actual_cost); + + @action updateName(inputText: string) { + this.name = inputText; + this.args.model.name = inputText; + } + + @action updateStatus(type: { name: string }) { + this.selectedStatus = type; + this.args.model.status = type.name; + } + + @action updateActive() { + this.args.model.active = !this.args.model.active; + } + + @action updateType(type: { name: string }) { + this.selectedType = type; + this.args.model.type = type.name; + } + + @action updateDescription(inputText: string) { + this.description = inputText; + this.args.model.description = inputText; + } + + @action updateStartDate(date: Date) { + // If the end date is set and the new start date is after the end date, update the end date + if (this.args.model.end_date && isAfter(date, this.args.model.end_date)) { + this.args.model.end_date = date; + this.endDateString = format(date, dateFormat); + } + this.args.model.start_date = date; + this.startDateString = format(date, dateFormat); + } + + @action updateEndDate(date: Date) { + // If the start date is set and the new end date is before the start date, update the start date + if ( + this.args.model.start_date && + isBefore(date, this.args.model.start_date) + ) { + this.args.model.start_date = date; + this.startDateString = format(date, dateFormat); + } + this.args.model.end_date = date; + this.endDateString = format(date, dateFormat); + } + + @action parseDateInput(field: string, date: string) { + if (field === 'start_date') { + return this.updateStartDate(parse(date, dateFormat, new Date())); + } + return this.updateEndDate(parse(date, dateFormat, new Date())); + } + + validateOnKeyPress = (event: KeyboardEvent) => { + const eventKey = event.key; + // Allow only numeric characters (0-9) and decimal point (.) + if ( + !/^\d+$/.test(eventKey) && + eventKey !== '.' && + eventKey !== 'Backspace' && + eventKey !== 'Delete' && + eventKey !== 'ArrowLeft' && + eventKey !== 'ArrowRight' && + eventKey !== 'ArrowUp' && + eventKey !== 'ArrowDown' && + eventKey !== 'Tab' + ) { + event.preventDefault(); + } + }; + + @action updateCustomNumberInput( + fieldName: + | 'numberSentInputValue' + | 'expectedResponseInputValue' + | 'expectedRevenueInputValue' + | 'budgetedCostInputValue' + | 'actualCostInputValue', + inputText: string, + ) { + this[fieldName] = inputText; + } + + @action onBlurNumberSent() { + const currentNumber = sanitisedNumber(this.numberSentInputValue); + const nearestRoundNumber = Math.round(currentNumber); + + const formatNumber = nearestRoundNumber; + this.numberSentInputValue = formatNumberWithSeparator(formatNumber); + this.args.model.number_sent = formatNumber.toString(); + } + + @action onBlurExpectedResponse() { + const currentNumber = sanitisedNumber(this.expectedResponseInputValue); + const formatNumber = nearestDecimal(currentNumber, 2); + this.expectedResponseInputValue = formatNumberWithSeparator( + formatNumber, + true, + ); + this.args.model.expected_response_percentage = formatNumber.toString(); + } + + @action onBlurCurrencyField( + inputText: string, + inputValueName: + | 'expectedRevenueInputValue' + | 'budgetedCostInputValue' + | 'actualCostInputValue', + fieldName: 'expected_revenue' | 'budgeted_cost' | 'actual_cost', + ) { + const currentNumber = sanitisedNumber(inputText); + const formatNumber = Math.round(currentNumber); + const numberWithCurrency = formatCurrency(formatNumber); + this[inputValueName] = numberWithCurrency; + this.args.model[fieldName] = formatNumber.toString(); + } + + private campaignStatuses = [ + { name: 'None' }, + { name: 'Planned' }, + { name: 'In Progress' }, + { name: 'Completed' }, + { name: 'Aborted' }, + ]; + + private campaignTypes = [ + { name: 'None' }, + { name: 'Advertisement' }, + { name: 'Email' }, + { name: 'Telemarketing' }, + { name: 'Banner Ads' }, + { name: 'Seminar/Conference' }, + { name: 'Public Relations' }, + { name: 'Partners' }, + { name: 'Referral Program' }, + { name: 'Other' }, + ]; + + +} + +export class CampaignForm extends CardDef { + @field name = contains(StringField); + @field status = contains(StringField); + @field active = contains(BooleanField); + @field type = contains(StringField); + @field parent_campaign = linksTo(() => CampaignForm); + @field description = contains(StringField); + @field start_date = contains(DateField); + @field end_date = contains(DateField); + @field number_sent = contains(StringField); + @field expected_response_percentage = contains(StringField); + @field expected_revenue = contains(StringField); + @field budgeted_cost = contains(StringField); + @field actual_cost = contains(StringField); + + static displayName = 'CampaignForm'; + + static isolated = Isolated; + static embedded = Embedded; + static atom = Embedded; + static edit = Edit; +} From 7e4efba1e1fc4da126787590c08f8b618bf800fb Mon Sep 17 00:00:00 2001 From: Richard Tan Date: Tue, 18 Jun 2024 13:09:30 +0800 Subject: [PATCH 18/47] feat: proper test field initialisation --- packages/drafts-realm/campaign-form.gts | 110 +++++++++++++----------- 1 file changed, 58 insertions(+), 52 deletions(-) diff --git a/packages/drafts-realm/campaign-form.gts b/packages/drafts-realm/campaign-form.gts index d91296d37d8..c4b8df303c4 100644 --- a/packages/drafts-realm/campaign-form.gts +++ b/packages/drafts-realm/campaign-form.gts @@ -79,75 +79,43 @@ const formatNumberWithSeparator = ( class Isolated extends Component { @@ -1304,10 +1322,9 @@ class IsolatedSecForSaleHub extends Component { export class SaleHub extends CardDef { static displayName = 'sale hub'; @field leadForm = contains(LeadFormField); - // @field leadConvertedForm = contains(LeadConvertedFormField); - // @field accountForm = linksTo(CrmAccount); + @field accountForm = contains(CrmAccountField); @field contactForm = contains(ContactFormField); - // @field opportunityForm = linksTo(OpportunityForm); + @field opportunityForm = contains(OpportunityFormField); @field scheduledTask = containsMany(ScheduledTask); static isolated = IsolatedSecForSaleHub; } From 312a9e99f973e4d5ca5d6266fcf8fe37f838bb54 Mon Sep 17 00:00:00 2001 From: lucas Date: Fri, 28 Jun 2024 10:35:44 +0800 Subject: [PATCH 23/47] add company name field at opportunity form, currently dont have this field --- .../37f6334a-02be-48db-a675-20409f8d5507.json | 1 + .../bcbf2e7d-16ad-41e0-95a4-03dc988809e5.json | 1 + packages/drafts-realm/opportunity-form.gts | 20 +++++++++++++++++++ packages/drafts-realm/sale-hub.gts | 20 +++++-------------- 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/packages/drafts-realm/OpportunityForm/37f6334a-02be-48db-a675-20409f8d5507.json b/packages/drafts-realm/OpportunityForm/37f6334a-02be-48db-a675-20409f8d5507.json index c8d7e1f9290..a630c7f8ec5 100644 --- a/packages/drafts-realm/OpportunityForm/37f6334a-02be-48db-a675-20409f8d5507.json +++ b/packages/drafts-realm/OpportunityForm/37f6334a-02be-48db-a675-20409f8d5507.json @@ -3,6 +3,7 @@ "type": "card", "attributes": { "opportunityName": "Opportunity Name", + "companyName": "", "closeDate": "2024-06-13", "amount": { "currency": "RM", diff --git a/packages/drafts-realm/SaleHub/bcbf2e7d-16ad-41e0-95a4-03dc988809e5.json b/packages/drafts-realm/SaleHub/bcbf2e7d-16ad-41e0-95a4-03dc988809e5.json index 6dacede677f..124107945a6 100644 --- a/packages/drafts-realm/SaleHub/bcbf2e7d-16ad-41e0-95a4-03dc988809e5.json +++ b/packages/drafts-realm/SaleHub/bcbf2e7d-16ad-41e0-95a4-03dc988809e5.json @@ -93,6 +93,7 @@ }, "opportunityForm": { "opportunityName": null, + "companyName": "Law lawbombom", "closeDate": null, "amount": { "currency": null, diff --git a/packages/drafts-realm/opportunity-form.gts b/packages/drafts-realm/opportunity-form.gts index 971266bb18d..a4abc9addb2 100644 --- a/packages/drafts-realm/opportunity-form.gts +++ b/packages/drafts-realm/opportunity-form.gts @@ -119,6 +119,11 @@ class AmountField extends FieldDef { /* Opportunity Form */ class IsolatedSecForOpportunityForm extends Component { + get getCompanyName() { + if (!this.args.model.companyName) return '-'; + return this.args.model.companyName; + } + get getFormattedAmount() { const amount = this.args.model.amount; const hasAmount = amount && amount.totalAmount; @@ -176,6 +181,11 @@ class IsolatedSecForOpportunityForm extends Component {
+
+ + + {{this.getCompanyName}} +
<@fields.closeDate /> @@ -338,6 +348,10 @@ class EditSecForOpportunityForm extends Component { <@fields.accountName /> + + <@fields.companyName /> + + <@fields.closeDate /> @@ -432,6 +446,9 @@ export class OpportunityFormField extends FieldDef { @field accountName = linksTo(CrmAccount, { description: `Account Name`, }); + @field companyName = contains(StringField, { + description: `Company Name`, + }); @field closeDate = contains(DateCard, { description: `Close Date`, }); @@ -465,6 +482,9 @@ export class OpportunityForm extends CardDef { @field accountName = linksTo(CrmAccount, { description: `Account Name`, }); + @field companyName = contains(StringField, { + description: `Company Name`, + }); @field closeDate = contains(DateCard, { description: `Close Date`, }); diff --git a/packages/drafts-realm/sale-hub.gts b/packages/drafts-realm/sale-hub.gts index c0e1fd2460a..5316a7925da 100644 --- a/packages/drafts-realm/sale-hub.gts +++ b/packages/drafts-realm/sale-hub.gts @@ -788,6 +788,9 @@ class IsolatedSecForSaleHub extends Component { @action handleConvert() { this.openModal(); + this.updateAccountFormAccountName(); + this.updateContactFormName(); + this.updateOpportunityAccountName(); } //task-form modal @@ -796,10 +799,6 @@ class IsolatedSecForSaleHub extends Component { @action openTaskFormModal() { this.isTaskFormModalVisible = true; - - this.updateAccountFormAccountName(); - this.updateContactFormName(); - this.updateOpportunityAccountName(); } @action @@ -848,43 +847,34 @@ class IsolatedSecForSaleHub extends Component { const firstName = this.args.model.leadForm.name.firstName; const company = this.args.model.leadForm.company; - this.args.model.opportunityForm.opportunityName = `${firstName} ${company}`; + this.args.model.opportunityForm.companyName = `${firstName} ${company}`; } } get accountFormAccountName() { const { leadForm } = this.args.model; - if (!leadForm || !leadForm.name) return ''; - const { salutation, firstName, lastName } = leadForm.name; if (!salutation || !firstName || !lastName) return ''; - return `${salutation} ${firstName} ${lastName}`; } get contactFormAccountName() { const { leadForm } = this.args.model; - if (!leadForm || !leadForm.name) return ''; - const { salutation, firstName, lastName } = leadForm.name; if (!salutation || !firstName || !lastName) return ''; - return `${salutation} ${firstName} ${lastName}`; } get opportunityFormName() { const { leadForm } = this.args.model; - - if (!leadForm || !leadForm.name || !leadForm.company) return ''; - + if (!leadForm || !leadForm.company) return ''; const { firstName } = leadForm.name; if (!firstName) return ''; - return `${firstName} ${leadForm.company}`; } From 7e6313eeccc3c831154e12bc6bbd54d7c3303c48 Mon Sep 17 00:00:00 2001 From: lucas Date: Sat, 29 Jun 2024 10:55:29 +0800 Subject: [PATCH 24/47] seperate and resorting the date by upcoming, today date and overdue --- .../bcbf2e7d-16ad-41e0-95a4-03dc988809e5.json | 59 ++++++++++++++++++- packages/drafts-realm/sale-hub.gts | 13 ++-- 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/packages/drafts-realm/SaleHub/bcbf2e7d-16ad-41e0-95a4-03dc988809e5.json b/packages/drafts-realm/SaleHub/bcbf2e7d-16ad-41e0-95a4-03dc988809e5.json index 124107945a6..84cf186f461 100644 --- a/packages/drafts-realm/SaleHub/bcbf2e7d-16ad-41e0-95a4-03dc988809e5.json +++ b/packages/drafts-realm/SaleHub/bcbf2e7d-16ad-41e0-95a4-03dc988809e5.json @@ -104,7 +104,44 @@ "percentage": null, "forecastCategory": null }, - "scheduledTask": [], + "scheduledTask": [ + { + "taskForm": { + "taskId": "0", + "subject": "1", + "dueDate": "2024-06-29", + "comments": null, + "isCompleted": false + } + }, + { + "taskForm": { + "taskId": "1", + "subject": "task b", + "dueDate": "2024-06-27", + "comments": null, + "isCompleted": false + } + }, + { + "taskForm": { + "taskId": "2", + "subject": "task ccc", + "dueDate": "2024-06-30", + "comments": null, + "isCompleted": false + } + }, + { + "taskForm": { + "taskId": "3", + "subject": "task xx", + "dueDate": "2024-07-03", + "comments": null, + "isCompleted": false + } + } + ], "title": null, "description": null, "thumbnailURL": null @@ -129,6 +166,26 @@ "links": { "self": null } + }, + "scheduledTask.0.taskForm.relatedTo": { + "links": { + "self": "http://localhost:4201/drafts/CardDef/7298c9c6-2964-41a0-87f7-42127a16e12b" + } + }, + "scheduledTask.1.taskForm.relatedTo": { + "links": { + "self": "http://localhost:4201/drafts/CrmAccount/4802eeed-bec6-4d7a-8f05-6370866edd40" + } + }, + "scheduledTask.2.taskForm.relatedTo": { + "links": { + "self": null + } + }, + "scheduledTask.3.taskForm.relatedTo": { + "links": { + "self": "http://localhost:4201/drafts/CardDef/992531db-9912-49eb-9b72-f710cbcdb085" + } } }, "meta": { diff --git a/packages/drafts-realm/sale-hub.gts b/packages/drafts-realm/sale-hub.gts index 5316a7925da..631e692d62d 100644 --- a/packages/drafts-realm/sale-hub.gts +++ b/packages/drafts-realm/sale-hub.gts @@ -969,8 +969,8 @@ class IsolatedSecForSaleHub extends Component { }, {}); const sortedMonths = Object.keys(groupedTasks).sort((a, b) => { - if (a === 'Upcoming & Overdue') return -1; - if (b === 'Upcoming & Overdue') return 1; + if (a === 'Upcoming') return -1; + if (b === 'Overdue') return 1; return new Date(a).getTime() - new Date(b).getTime(); }); @@ -990,15 +990,18 @@ class IsolatedSecForSaleHub extends Component { @action formatDueDate(date: Date) { const todayDate = new Date(); - if (date > todayDate || isToday(date)) - return formatDate(new Date(date), 'MMMM yyyy'); - return 'Upcoming & Overdue'; + + if (isToday(date)) return formatDate(new Date(date), 'MMMM yyyy'); + if (date > todayDate) return 'Upcoming'; + return 'Overdue'; } @action formatDay(date: Date) { const todayDate = new Date(); if (isToday(date)) return 'Today'; if (isTomorrow(date)) return 'Tomorrow'; + if (date > todayDate || isTomorrow(date)) + return formatDate(new Date(date), 'dd/MM/yyyy'); if (date < todayDate) return null; if (isThisMonth(date)) return 'This Month'; return null; From 618b4da9689b182982e99ab567777419217ce59a Mon Sep 17 00:00:00 2001 From: Richard Tan Date: Sat, 29 Jun 2024 16:37:22 +0800 Subject: [PATCH 25/47] refactor: put display name on top --- packages/drafts-realm/campaign-form.gts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/drafts-realm/campaign-form.gts b/packages/drafts-realm/campaign-form.gts index 5624d38186f..0b3149126df 100644 --- a/packages/drafts-realm/campaign-form.gts +++ b/packages/drafts-realm/campaign-form.gts @@ -390,6 +390,7 @@ class Edit extends Component { } export class CampaignForm extends CardDef { + static displayName = 'CampaignForm'; @field name = contains(StringField, { description: 'The campaign name', }); @@ -430,8 +431,6 @@ export class CampaignForm extends CardDef { description: 'The actual cost by RM in the campaign', }); - static displayName = 'CampaignForm'; - static isolated = Isolated; static embedded = Embedded; static atom = Embedded; From 6ff3a363d1013456a712ed1f58798c9262f311c8 Mon Sep 17 00:00:00 2001 From: Richard Tan Date: Sat, 29 Jun 2024 16:50:33 +0800 Subject: [PATCH 26/47] refactor: snake case to camel case --- .../4ee4a9ff-f61b-476f-9140-27308934024c.json | 20 +++--- .../b06e2425-70c2-47db-bafa-75a3ca349541.json | 16 ++--- packages/drafts-realm/campaign-form.gts | 70 +++++++++---------- 3 files changed, 53 insertions(+), 53 deletions(-) diff --git a/packages/drafts-realm/CampaignForm/4ee4a9ff-f61b-476f-9140-27308934024c.json b/packages/drafts-realm/CampaignForm/4ee4a9ff-f61b-476f-9140-27308934024c.json index 09cd9e7808c..178335ef15d 100644 --- a/packages/drafts-realm/CampaignForm/4ee4a9ff-f61b-476f-9140-27308934024c.json +++ b/packages/drafts-realm/CampaignForm/4ee4a9ff-f61b-476f-9140-27308934024c.json @@ -4,21 +4,21 @@ "attributes": { "name": "Advertisement Campaign Form", "status": "Planned", - "active": false, + "active": true, "type": "Email", - "description": "ssssssjkk", - "start_date": null, - "end_date": null, - "number_sent": 200003212, - "expected_response_percentage": 222.5355, - "expected_revenue": 231200, - "budgeted_cost": 1003039, - "actual_cost": 12034044, + "description": "This is advertisment campaign form", + "startDate": "2024-06-13", + "endDate": "2024-06-30", + "numberSent": 2000, + "expectedResponsePercentage": 100, + "expectedRevenue": 60000, + "budgetedCost": 40000, + "actualCost": 50000, "title": null, "thumbnailURL": null }, "relationships": { - "parent_campaign": { + "parentCampaign": { "links": { "self": null } diff --git a/packages/drafts-realm/CampaignForm/b06e2425-70c2-47db-bafa-75a3ca349541.json b/packages/drafts-realm/CampaignForm/b06e2425-70c2-47db-bafa-75a3ca349541.json index 80285d73cd1..a1a1b982fd3 100644 --- a/packages/drafts-realm/CampaignForm/b06e2425-70c2-47db-bafa-75a3ca349541.json +++ b/packages/drafts-realm/CampaignForm/b06e2425-70c2-47db-bafa-75a3ca349541.json @@ -7,18 +7,18 @@ "active": false, "type": "Advertisement", "description": "This is a test form", - "start_date": "2024-06-22", - "end_date": "2024-06-29", - "number_sent": 1000, - "expected_response_percentage": 100, - "expected_revenue": 20000, - "budgeted_cost": 1000, - "actual_cost": 1000, + "startDate": "2024-05-01", + "endDate": "2024-05-31", + "numberSent": 1000, + "expectedResponsePercentage": 100, + "expectedRevenue": 10000, + "budgetedCost": 500000, + "actualCost": 100000, "title": null, "thumbnailURL": null }, "relationships": { - "parent_campaign": { + "parentCampaign": { "links": { "self": "./4ee4a9ff-f61b-476f-9140-27308934024c" } diff --git a/packages/drafts-realm/campaign-form.gts b/packages/drafts-realm/campaign-form.gts index 0b3149126df..c1f517a69b3 100644 --- a/packages/drafts-realm/campaign-form.gts +++ b/packages/drafts-realm/campaign-form.gts @@ -73,7 +73,7 @@ const formatNumberWithSeparator = ( class Isolated extends Component { get numberSent() { let { model } = this.args; - const nearestRoundNumber = Math.round(model.number_sent || 0); + const nearestRoundNumber = Math.round(model.numberSent || 0); const formatNumber = nearestRoundNumber; return formatNumberWithSeparator(formatNumber); } @@ -81,7 +81,7 @@ class Isolated extends Component { get expectedResponsePercentage() { let { model } = this.args; const formatNumber = nearestDecimal( - model.expected_response_percentage || 0, + model.expectedResponsePercentage || 0, 2, ); return formatNumberWithSeparator(formatNumber, true); @@ -102,16 +102,16 @@ class Isolated extends Component { {{@model.type}} - <@fields.parent_campaign /> + <@fields.parentCampaign /> {{@model.description}} - <@fields.start_date /> + <@fields.startDate /> - <@fields.end_date /> + <@fields.endDate /> {{this.numberSent}} @@ -120,13 +120,13 @@ class Isolated extends Component { {{this.expectedResponsePercentage}} - {{formatCurrency @model.expected_revenue}} + {{formatCurrency @model.expectedRevenue}} - {{formatCurrency @model.budgeted_cost}} + {{formatCurrency @model.budgetedCost}} - {{formatCurrency @model.actual_cost}} + {{formatCurrency @model.actualCost}}
-// -// } - -// class ViewSecForLeadForm extends Component { -// -// } - -// class EditSecFoLeadForm extends Component { -// /* Lead Status Options */ -// get selectedLeadStatus() { -// return { name: this.args.model.leadStatus || 'None' }; -// } - -// @tracked leadStatusOptions = [ -// { name: 'None' }, -// { name: 'New' }, -// { name: 'Working' }, -// { name: 'Nurturing' }, -// { name: 'Qualified' }, -// { name: 'Unqualified' }, -// ] as Array; - -// @action updateLeadStatus(type: { name: string }) { -// this.args.model.leadStatus = type.name; -// } - -// /* No Of Employees */ -// @action updateNoOfEmployees(val: number) { -// this.args.model.noOfEmployees = val; -// } - -// get getFormattedNoOfEmployees() { -// if (!this.args.model.noOfEmployees) return null; -// return Math.round(this.args.model.noOfEmployees); -// } - -// /* Lead Source Options */ -// get selectedLeadSource() { -// return { name: this.args.model.leadSource || 'None' }; -// } - -// @tracked leadSourceOptions = [ -// { name: 'None' }, -// { name: 'Advertisement' }, -// { name: 'Employee Referral' }, -// { name: 'External Referral' }, -// { name: 'Partner' }, -// { name: 'Public Relations' }, -// { name: 'Seminar - Internal' }, -// { name: 'Seminar - Partner' }, -// { name: 'Trade Show' }, -// { name: 'Web' }, -// { name: 'Word of mouth' }, -// { name: 'Other' }, -// ] as Array; - -// @action updateLeadSource(type: { name: string }) { -// this.args.model.leadSource = type.name; -// } - -// /* Industry Options */ -// get selectedIndustry() { -// return { name: this.args.model.industry || 'None' }; -// } - -// @tracked industryOptions = [ -// { name: 'None' }, -// { name: 'Agriculture' }, -// { name: 'Apparel' }, -// { name: 'Banking' }, -// { name: 'Biotechnology' }, -// { name: 'Chemicals' }, -// { name: 'Communications' }, -// { name: 'Construction' }, -// { name: 'Consulting' }, -// { name: 'Education' }, -// { name: 'Electronics' }, -// { name: 'Energy' }, -// { name: 'Engineering' }, -// { name: 'Entertainment' }, -// { name: 'Environmental' }, -// { name: 'Finance' }, -// { name: 'Food & Beverage' }, -// { name: 'Government' }, -// { name: 'Healthcare' }, -// { name: 'Hospitality' }, -// { name: 'Insurance' }, -// { name: 'Machinery' }, -// { name: 'Manufacturing' }, -// { name: 'Media' }, -// { name: 'Not For Profit' }, -// { name: 'Recreation' }, -// { name: 'Retail' }, -// { name: 'Shipping' }, -// { name: 'Technology' }, -// { name: 'Telecommunications' }, -// { name: 'Transportation' }, -// { name: 'Utilities' }, -// { name: 'Others' }, -// ] as Array; - -// @action updateIndustry(type: { name: string }) { -// this.args.model.industry = type.name; -// } - -// -// } - -// class LeadForm extends FieldDef { -// static displayName = 'Lead Form'; - -// @field name = contains(UserName, { -// description: `User's Full Name`, -// }); -// @field company = contains(StringField, { -// description: `User's Company Name`, -// }); -// @field title = contains(StringField, { -// description: `User's Title`, -// }); -// @field website = contains(StringField, { -// description: `User's Website`, -// }); -// @field description = contains(MarkdownField, { -// description: `User's Description`, -// }); -// @field leadStatus = contains(StringField, { -// description: `Lead Status`, -// }); -// @field phone = contains(StringField, { -// description: `User's phone number`, -// }); -// @field email = contains(UserEmail, { -// description: `User's Email`, -// }); -// @field addressInfo = contains(AddressInfo, { -// description: `User's AddressInfo`, -// }); -// @field noOfEmployees = contains(NumberField, { -// description: `No Of Employees`, -// }); -// @field annualRevenue = contains(CurrencyAmount, { -// description: `Annual Revenue`, -// }); -// @field leadSource = contains(StringField, { -// description: `Lead Source`, -// }); -// @field industry = contains(StringField, { -// description: `Industry`, -// }); - -// static isolated = IsolatedSecForLeadForm; -// static atom = ViewSecForLeadForm; -// static embedded = ViewSecForLeadForm; -// static edit = EditSecFoLeadForm; -// } - //*task-form class TaskForm extends FieldDef { static displayName = 'Task Form'; @@ -586,11 +110,17 @@ class Steps extends GlimmerComponent { @tracked currentStepIndex = this.steps[0].step || 0; get isStepCompleted() { - return this.currentStepStatus === 'Step Completed'; + return ( + this.currentStepStatus === 'Step Completed' || + this.args.isLeadFormConverted + ); } get currentStepStatus() { - if (this.currentStepIndex >= this.steps.length - 1) return 'Convert'; + if (this.currentStepIndex >= this.steps.length - 1) { + if (this.args.isLeadFormConverted) return 'Converted'; + return 'Convert'; + } if (this.steps[this.currentStepIndex].isCompleted) return 'Step Completed'; return 'Mark Status as Complete'; } @@ -923,7 +453,9 @@ class IsolatedSecForSaleHub extends Component { ); this.initStepOptions.forEach((option, index) => { - option.isCompleted = index <= currentCompletedStep; + option.isCompleted = + index <= currentCompletedStep || + this.args.model.leadForm?.leadStatus === 'Qualified'; option.isProceedToNextStep = index === currentCompletedStep + 1; }); @@ -1021,6 +553,41 @@ class IsolatedSecForSaleHub extends Component { return this.args.model.scheduledTask; } + /* Converted Status Options */ + get selectedConvertedStatus() { + return { + name: + this.args.model.convertedStatus || + this.convertedStatusOptions[0].name || + 'None', + }; + } + + @tracked convertedStatusOptions = [ + { name: 'Qualified' }, + { name: 'Unqualified' }, + ] as Array; + + @action updateConvertedStatus(type: { name: string }) { + this.args.model.convertedStatus = type.name; + + if (this.args.model.leadForm) { + this.args.model.leadForm.leadStatus = type.name; + } + } + + @action cancel() { + this.args.model.isLeadFormConverted = + this.args.model.isLeadFormConverted || false; + + this.closeModal(); + } + + @action convert() { + this.args.model.isLeadFormConverted = true; + this.closeModal(); + } + @@ -1314,10 +929,34 @@ class IsolatedSecForSaleHub extends Component { export class SaleHub extends CardDef { static displayName = 'sale hub'; - @field leadForm = contains(LeadFormField); - @field accountForm = contains(CrmAccountField); - @field contactForm = contains(ContactFormField); - @field opportunityForm = contains(OpportunityFormField); - @field scheduledTask = containsMany(ScheduledTask); + + @field targetPage = contains(StringField, { + description: `Show which page is clicked`, + }); + @field leadForm = contains(LeadFormField, { + description: `Lead form`, + }); + @field accountForm = contains(CrmAccountField, { + description: `crm account`, + }); + @field contactForm = contains(ContactFormField, { + description: `Contact Form`, + }); + @field opportunityForm = contains(OpportunityFormField, { + description: `Opportunity Form`, + }); + @field scheduledTask = containsMany(ScheduledTask, { + description: `Upcoming & overdue tasks`, + }); + @field recordOwner = linksTo(MatrixUser, { + description: `Record owner`, + }); + @field convertedStatus = contains(StringField, { + description: `Converted Status`, + }); + @field isLeadFormConverted = contains(BooleanField, { + description: `Check if leadForm is converted`, + }); + static isolated = IsolatedSecForSaleHub; } From c45beb7e458281c07e298aa742d40198e67602a5 Mon Sep 17 00:00:00 2001 From: lucas Date: Sun, 30 Jun 2024 17:20:02 +0800 Subject: [PATCH 28/47] create targetPage tab logic for each converted form --- .../bcbf2e7d-16ad-41e0-95a4-03dc988809e5.json | 1 + packages/drafts-realm/sale-hub.gts | 116 +++++++++++++++++- 2 files changed, 113 insertions(+), 4 deletions(-) diff --git a/packages/drafts-realm/SaleHub/bcbf2e7d-16ad-41e0-95a4-03dc988809e5.json b/packages/drafts-realm/SaleHub/bcbf2e7d-16ad-41e0-95a4-03dc988809e5.json index afa26e92ede..57dfdb6fe6c 100644 --- a/packages/drafts-realm/SaleHub/bcbf2e7d-16ad-41e0-95a4-03dc988809e5.json +++ b/packages/drafts-realm/SaleHub/bcbf2e7d-16ad-41e0-95a4-03dc988809e5.json @@ -2,6 +2,7 @@ "data": { "type": "card", "attributes": { + "targetPage": "Account Form", "leadForm": { "name": { "salutation": "Mr.", diff --git a/packages/drafts-realm/sale-hub.gts b/packages/drafts-realm/sale-hub.gts index 39b5118e9c8..6f2a65a0986 100644 --- a/packages/drafts-realm/sale-hub.gts +++ b/packages/drafts-realm/sale-hub.gts @@ -40,6 +40,21 @@ import { LeadFormField } from './lead-form'; import { ContactFormField } from './contact-form'; import { MatrixUser } from './matrix-user'; +interface TargetPageLinkSingnature { + name: string; + isActive: boolean; + shouldShowFormData: boolean; +} + +interface TargetPageLinksSignature { + Element: HTMLElement; + Args: { + targetPageLinks: TargetPageLinkSingnature[]; + targetPage: string | undefined; + onSelectPage: (val: string) => void; + }; +} + interface StepSignature { step: number; name: string; @@ -103,6 +118,39 @@ class TaskForm extends FieldDef { }); } +//*Target Page Links +class TargetPageLinks extends GlimmerComponent { + +} + //*steps class Steps extends GlimmerComponent { @tracked steps = this.args.steps.map((step) => ({ ...step })); @@ -408,7 +456,52 @@ class IsolatedSecForSaleHub extends Component { return `${firstName} ${leadForm.company}`; } - //step + //targetPageLinks + @tracked initTargetPageLinks = [ + { + name: 'Lead Form', + isActive: true, + shouldShowFormData: true, + }, + { + name: 'Account Form', + isActive: false, + shouldShowFormData: false, + }, + { + name: 'Contact Form', + isActive: false, + shouldShowFormData: false, + }, + { + name: 'Opportunity Form', + isActive: false, + shouldShowFormData: false, + }, + ] as Array; + + get targetPageLinks() { + return this.initTargetPageLinks.map((page) => { + return { ...page, isActive: page.name === this.targetPage }; + }); + } + + get targetPage() { + return this.args.model.targetPage; + } + + @action onSelectPage(name: string) { + this.args.model.targetPage = name; + + this.initTargetPageLinks = this.initTargetPageLinks.map((page) => { + return { + ...page, + isActive: page.name === name, + }; + }); + } + + //steps @tracked initStepOptions = [ { step: 0, @@ -696,9 +789,16 @@ class IsolatedSecForSaleHub extends Component { -
- +
+ +
+ +
@@ -242,11 +242,12 @@ class ViewSecForLeadForm extends Component { overflow: hidden; } .field-group-title { - font-size: var(--boxel-font-size); - font-weight: 800; - margin-bottom: var(--boxel-sp-xs); + font-size: 1rem; + font-weight: bold; + margin-bottom: 0.75rem; } .field-input-group { + overflow: overlay; display: flex; flex-direction: column; justify-content: space-evenly; @@ -254,7 +255,7 @@ class ViewSecForLeadForm extends Component { background-color: #fbfbfb; border: 1px solid var(--boxel-300); border-radius: var(--boxel-border-radius); - padding: var(--boxel-sp); + padding: var(--boxel-sp-xxs); } diff --git a/packages/drafts-realm/opportunity-form.gts b/packages/drafts-realm/opportunity-form.gts index a4abc9addb2..23d9803da50 100644 --- a/packages/drafts-realm/opportunity-form.gts +++ b/packages/drafts-realm/opportunity-form.gts @@ -34,6 +34,19 @@ const formatNumber = (val: number) => { }; /* Amount */ + +class EmbeddedSecForAmount extends Component { + +} + class EditSecForAmount extends Component { @tracked currencyOptions = ['Select', 'RM']; get selectedCurrency() { @@ -114,6 +127,7 @@ class AmountField extends FieldDef { description: `Total Amount`, }); + static embedded = EmbeddedSecForAmount; static edit = EditSecForAmount; } @@ -174,7 +188,6 @@ class IsolatedSecForOpportunityForm extends Component {
-
<@fields.accountName /> @@ -341,27 +354,27 @@ class EditSecForOpportunityForm extends Component {