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/CampaignForm/4ee4a9ff-f61b-476f-9140-27308934024c.json b/packages/drafts-realm/CampaignForm/4ee4a9ff-f61b-476f-9140-27308934024c.json new file mode 100644 index 00000000000..178335ef15d --- /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": "Email", + "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": { + "parentCampaign": { + "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..a1a1b982fd3 --- /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", + "startDate": "2024-05-01", + "endDate": "2024-05-31", + "numberSent": 1000, + "expectedResponsePercentage": 100, + "expectedRevenue": 10000, + "budgetedCost": 500000, + "actualCost": 100000, + "title": null, + "thumbnailURL": null + }, + "relationships": { + "parentCampaign": { + "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/CampaignMembersChart/89e02ea7-7d04-45c9-90c9-7062e38c87a2.json b/packages/drafts-realm/CampaignMembersChart/89e02ea7-7d04-45c9-90c9-7062e38c87a2.json new file mode 100644 index 00000000000..40698c193b6 --- /dev/null +++ b/packages/drafts-realm/CampaignMembersChart/89e02ea7-7d04-45c9-90c9-7062e38c87a2.json @@ -0,0 +1,48 @@ +{ + "data": { + "type": "card", + "attributes": { + "name": "Email July 2024", + "chartType": "Vertical Bar", + "contactMembers": [ + { + "responseStatus": "Sent" + }, + { + "responseStatus": "Responded" + } + ], + "leadMembers": [ + { + "responseStatus": "Responded" + } + ], + "title": null, + "description": null, + "thumbnailURL": null + }, + "relationships": { + "contactMembers.0.contactForm": { + "links": { + "self": "../ContactForm/24fad614-e49b-44cf-9bf4-f9ead589ffa5" + } + }, + "contactMembers.1.contactForm": { + "links": { + "self": "../ContactForm/24fad614-e49b-44cf-9bf4-f9ead589ffa5" + } + }, + "leadMembers.0.leadForm": { + "links": { + "self": "../LeadForm/ad388806-9aa7-485b-90ed-66331a6da6bb" + } + } + }, + "meta": { + "adoptsFrom": { + "module": "../campaign-members-chart", + "name": "CampaignMembersChart" + } + } + } +} \ 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/Company/9ad68df6-b27e-4164-8736-e9347ece4d1e.json b/packages/drafts-realm/Company/9ad68df6-b27e-4164-8736-e9347ece4d1e.json new file mode 100644 index 00000000000..ed27746b82c --- /dev/null +++ b/packages/drafts-realm/Company/9ad68df6-b27e-4164-8736-e9347ece4d1e.json @@ -0,0 +1,49 @@ +{ + "data": { + "type": "card", + "attributes": { + "name": "law bom bom", + "regstrationNumber": null, + "address": { + "streetAddress": null, + "city": null, + "region": null, + "postalCode": null, + "poBoxNumber": null, + "country": null + }, + "contactPerson": { + "salutation": null, + "firstName": null, + "lastName": null, + "address": { + "streetAddress": null, + "city": null, + "region": null, + "postalCode": null, + "poBoxNumber": null, + "country": null + }, + "phoneNumber": { + "country": null, + "area": null, + "number": null + }, + "email": { + "value": null + }, + "website": { + "value": null + } + }, + "description": null, + "thumbnailURL": null + }, + "meta": { + "adoptsFrom": { + "module": "../crm/account", + "name": "Company" + } + } + } +} \ No newline at end of file diff --git a/packages/drafts-realm/Company/b57e5ffd-b64f-4531-a7e4-5a1d0420e2bc.json b/packages/drafts-realm/Company/b57e5ffd-b64f-4531-a7e4-5a1d0420e2bc.json new file mode 100644 index 00000000000..4c3a90b69ae --- /dev/null +++ b/packages/drafts-realm/Company/b57e5ffd-b64f-4531-a7e4-5a1d0420e2bc.json @@ -0,0 +1,49 @@ +{ + "data": { + "type": "card", + "attributes": { + "name": "Law Grocery", + "regstrationNumber": null, + "address": { + "streetAddress": null, + "city": null, + "region": null, + "postalCode": null, + "poBoxNumber": null, + "country": null + }, + "contactPerson": { + "salutation": null, + "firstName": null, + "lastName": null, + "address": { + "streetAddress": null, + "city": null, + "region": null, + "postalCode": null, + "poBoxNumber": null, + "country": null + }, + "phoneNumber": { + "country": null, + "area": null, + "number": null + }, + "email": { + "value": null + }, + "website": { + "value": null + } + }, + "description": null, + "thumbnailURL": null + }, + "meta": { + "adoptsFrom": { + "module": "../crm/account", + "name": "Company" + } + } + } +} \ No newline at end of file diff --git a/packages/drafts-realm/ContactForm/24fad614-e49b-44cf-9bf4-f9ead589ffa5.json b/packages/drafts-realm/ContactForm/24fad614-e49b-44cf-9bf4-f9ead589ffa5.json new file mode 100644 index 00000000000..aa735bf04ea --- /dev/null +++ b/packages/drafts-realm/ContactForm/24fad614-e49b-44cf-9bf4-f9ead589ffa5.json @@ -0,0 +1,41 @@ +{ + "data": { + "type": "card", + "attributes": { + "user": { + "salutation": null, + "firstName": null, + "lastName": null + }, + "accountName": null, + "email": null, + "phone": null, + "fax": null, + "department": null, + "addressInfo": { + "address": null, + "zip": null, + "city": null, + "state": null, + "country": null, + "countryCode": null, + "stateCode": null + }, + "description": null, + "thumbnailURL": null + }, + "relationships": { + "owner": { + "links": { + "self": null + } + } + }, + "meta": { + "adoptsFrom": { + "module": "../contact-form", + "name": "ContactForm" + } + } + } +} \ 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..7d68816bdad --- /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": "../Company/9ad68df6-b27e-4164-8736-e9347ece4d1e" + } + } + }, + "meta": { + "adoptsFrom": { + "module": "../crm/account", + "name": "CrmAccount" + } + } + } +} \ No newline at end of file diff --git a/packages/drafts-realm/CrmAccount/a9289420-798a-49cf-b746-9aece2295d70.json b/packages/drafts-realm/CrmAccount/a9289420-798a-49cf-b746-9aece2295d70.json new file mode 100644 index 00000000000..7b7dbf337ae --- /dev/null +++ b/packages/drafts-realm/CrmAccount/a9289420-798a-49cf-b746-9aece2295d70.json @@ -0,0 +1,67 @@ +{ + "data": { + "type": "card", + "attributes": { + "accountName": null, + "accountAlias": null, + "description": null, + "contactInformation": { + "salutation": null, + "firstName": null, + "lastName": null, + "address": { + "streetAddress": null, + "city": null, + "region": null, + "postalCode": null, + "poBoxNumber": null, + "country": null + }, + "phoneNumber": { + "country": null, + "area": null, + "number": null + }, + "email": { + "value": null + }, + "website": { + "value": null + } + }, + "billingAddress": { + "streetAddress": null, + "city": null, + "region": null, + "postalCode": null, + "poBoxNumber": null, + "country": null + }, + "numberOfEmployees": null, + "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/LeadForm/ad388806-9aa7-485b-90ed-66331a6da6bb.json b/packages/drafts-realm/LeadForm/ad388806-9aa7-485b-90ed-66331a6da6bb.json new file mode 100644 index 00000000000..7b0b0cb350e --- /dev/null +++ b/packages/drafts-realm/LeadForm/ad388806-9aa7-485b-90ed-66331a6da6bb.json @@ -0,0 +1,52 @@ +{ + "data": { + "type": "card", + "attributes": { + "user": { + "salutation": "Mr.", + "firstName": "Law", + "lastName": "Yung Lum" + }, + "website": "law.com", + "description": null, + "leadStatus": "New", + "phone": "019999999999", + "email": "lawgrocery@g.com", + "addressInfo": { + "address": null, + "zip": null, + "city": null, + "state": null, + "country": null, + "countryCode": null, + "stateCode": null + }, + "noOfEmployees": 200, + "annualRevenue": { + "currency": "RM", + "totalAmount": 1200000 + }, + "leadSource": "Advertisement", + "industry": "Biotechnology", + "thumbnailURL": null + }, + "relationships": { + "company": { + "links": { + "self": "../Company/b57e5ffd-b64f-4531-a7e4-5a1d0420e2bc" + } + }, + "owner": { + "links": { + "self": "../MatrixUser/485ca453-b328-4e85-a761-b2fe7d145430" + } + } + }, + "meta": { + "adoptsFrom": { + "module": "../lead-form", + "name": "LeadForm" + } + } + } +} \ No newline at end of file diff --git a/packages/drafts-realm/MatrixUser/485ca453-b328-4e85-a761-b2fe7d145430.json b/packages/drafts-realm/MatrixUser/485ca453-b328-4e85-a761-b2fe7d145430.json new file mode 100644 index 00000000000..564a12e2864 --- /dev/null +++ b/packages/drafts-realm/MatrixUser/485ca453-b328-4e85-a761-b2fe7d145430.json @@ -0,0 +1,19 @@ +{ + "data": { + "type": "card", + "attributes": { + "username": "Law Yung Lom", + "email": { + "value": "lawyunglom@gmail.com" + }, + "description": null, + "thumbnailURL": null + }, + "meta": { + "adoptsFrom": { + "module": "../matrix-user", + "name": "MatrixUser" + } + } + } +} \ 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/OpportunityForm/187ec912-522a-44dc-b17c-4fe19ac3066d.json b/packages/drafts-realm/OpportunityForm/187ec912-522a-44dc-b17c-4fe19ac3066d.json new file mode 100644 index 00000000000..925f6c3ba8f --- /dev/null +++ b/packages/drafts-realm/OpportunityForm/187ec912-522a-44dc-b17c-4fe19ac3066d.json @@ -0,0 +1,37 @@ +{ + "data": { + "type": "card", + "attributes": { + "opportunityName": null, + "accountName": null, + "closeDate": null, + "amount": { + "currency": null, + "totalAmount": null + }, + "description": null, + "stage": null, + "percentage": null, + "forecastCategory": null, + "thumbnailURL": null + }, + "relationships": { + "company": { + "links": { + "self": null + } + }, + "owner": { + "links": { + "self": null + } + } + }, + "meta": { + "adoptsFrom": { + "module": "../opportunity-form", + "name": "OpportunityForm" + } + } + } +} \ No newline at end of file diff --git a/packages/drafts-realm/OpportunityForm/5ab75971-9227-4a3e-84cc-537d84587994.json b/packages/drafts-realm/OpportunityForm/5ab75971-9227-4a3e-84cc-537d84587994.json new file mode 100644 index 00000000000..2d741f64ff7 --- /dev/null +++ b/packages/drafts-realm/OpportunityForm/5ab75971-9227-4a3e-84cc-537d84587994.json @@ -0,0 +1,31 @@ +{ + "data": { + "type": "card", + "attributes": { + "opportunityName": null, + "closeDate": null, + "amount": { + "currency": null, + "totalAmount": null + }, + "description": null, + "stage": null, + "percentage": null, + "forecastCategory": null, + "thumbnailURL": null + }, + "relationships": { + "company": { + "links": { + "self": null + } + } + }, + "meta": { + "adoptsFrom": { + "module": "../opportunity-form", + "name": "OpportunityForm" + } + } + } +} diff --git a/packages/drafts-realm/OpportunityForm/bb174fbc-c07a-4e41-8851-0d624902bbd4.json b/packages/drafts-realm/OpportunityForm/bb174fbc-c07a-4e41-8851-0d624902bbd4.json new file mode 100644 index 00000000000..f4790b4a144 --- /dev/null +++ b/packages/drafts-realm/OpportunityForm/bb174fbc-c07a-4e41-8851-0d624902bbd4.json @@ -0,0 +1,36 @@ +{ + "data": { + "type": "card", + "attributes": { + "opportunityName": "Law Grocery", + "closeDate": null, + "amount": { + "currency": null, + "totalAmount": null + }, + "description": null, + "stage": null, + "percentage": null, + "forecastCategory": null, + "thumbnailURL": null + }, + "relationships": { + "accountName": { + "links": { + "self": null + } + }, + "company": { + "links": { + "self": "../Company/b57e5ffd-b64f-4531-a7e4-5a1d0420e2bc" + } + } + }, + "meta": { + "adoptsFrom": { + "module": "../opportunity-form", + "name": "OpportunityForm" + } + } + } +} \ No newline at end of file diff --git a/packages/drafts-realm/OpportunityForm/d4f9ef0b-d19a-461d-a6ed-5a6886e2004b.json b/packages/drafts-realm/OpportunityForm/d4f9ef0b-d19a-461d-a6ed-5a6886e2004b.json new file mode 100644 index 00000000000..2f823e2000f --- /dev/null +++ b/packages/drafts-realm/OpportunityForm/d4f9ef0b-d19a-461d-a6ed-5a6886e2004b.json @@ -0,0 +1,32 @@ +{ + "data": { + "type": "card", + "attributes": { + "opportunityName": "L", + "accountName": null, + "closeDate": null, + "amount": { + "currency": null, + "totalAmount": null + }, + "description": null, + "stage": null, + "percentage": null, + "forecastCategory": null, + "thumbnailURL": null + }, + "relationships": { + "company": { + "links": { + "self": null + } + } + }, + "meta": { + "adoptsFrom": { + "module": "../opportunity-form", + "name": "OpportunityForm" + } + } + } +} \ No newline at end of file diff --git a/packages/drafts-realm/SaleHub/236608d5-d07a-4006-bb3a-bad875503f4f.json b/packages/drafts-realm/SaleHub/236608d5-d07a-4006-bb3a-bad875503f4f.json new file mode 100644 index 00000000000..c6d659f3d07 --- /dev/null +++ b/packages/drafts-realm/SaleHub/236608d5-d07a-4006-bb3a-bad875503f4f.json @@ -0,0 +1,90 @@ +{ + "data": { + "type": "card", + "attributes": { + "targetPage": "Lead Form", + "scheduledTask": [ + { + "taskForm": { + "taskId": "0", + "subject": "task a", + "dueDate": "2024-07-06", + "comments": null, + "isCompleted": false + } + }, + { + "taskForm": { + "taskId": "1", + "subject": "task b", + "dueDate": "2024-07-05", + "comments": null, + "isCompleted": false + } + }, + { + "taskForm": { + "taskId": "2", + "subject": "task c", + "dueDate": "2024-06-06", + "comments": "testing", + "isCompleted": false + } + } + ], + "convertedStatus": "", + "isLeadFormConverted": false, + "title": null, + "description": null, + "thumbnailURL": null + }, + "relationships": { + "leadForm": { + "links": { + "self": "../LeadForm/ad388806-9aa7-485b-90ed-66331a6da6bb" + } + }, + "accountForm": { + "links": { + "self": "../CrmAccount/a9289420-798a-49cf-b746-9aece2295d70" + } + }, + "contactForm": { + "links": { + "self": "../ContactForm/24fad614-e49b-44cf-9bf4-f9ead589ffa5" + } + }, + "opportunityForm": { + "links": { + "self": "../OpportunityForm/187ec912-522a-44dc-b17c-4fe19ac3066d" + } + }, + "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 + } + }, + "recordOwner": { + "links": { + "self": "../MatrixUser/485ca453-b328-4e85-a761-b2fe7d145430" + } + } + }, + "meta": { + "adoptsFrom": { + "module": "../sale-hub", + "name": "SaleHub" + } + } + } +} \ No newline at end of file diff --git a/packages/drafts-realm/TaskForm/1066b3d4-32e2-44dd-8a71-b5b996100cf4.json b/packages/drafts-realm/TaskForm/1066b3d4-32e2-44dd-8a71-b5b996100cf4.json new file mode 100644 index 00000000000..c8b3bffbf95 --- /dev/null +++ b/packages/drafts-realm/TaskForm/1066b3d4-32e2-44dd-8a71-b5b996100cf4.json @@ -0,0 +1,26 @@ +{ + "data": { + "type": "card", + "attributes": { + "subject": "Send Quote", + "dueDate": "2024-05-16", + "comments": "We are approaching the deadline for our upcoming project, and we need to gather all necessary quotes to ensure we stay on schedule. Kindly submit your quotes by the due date to facilitate a smooth and timely completion of our tasks. Your prompt attention to this matter is greatly appreciated, as it will enable us to move forward efficiently and meet our objectives. Thank you for your cooperation and timely response.", + "title": "Call", + "description": null, + "thumbnailURL": null + }, + "relationships": { + "relatedTo": { + "links": { + "self": "../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/address-info.gts b/packages/drafts-realm/address-info.gts new file mode 100644 index 00000000000..91294dac985 --- /dev/null +++ b/packages/drafts-realm/address-info.gts @@ -0,0 +1,488 @@ +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'; +// @ts-ignore +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) { + 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/campaign-form.gts b/packages/drafts-realm/campaign-form.gts new file mode 100644 index 00000000000..c1f517a69b3 --- /dev/null +++ b/packages/drafts-realm/campaign-form.gts @@ -0,0 +1,438 @@ +import DateField from 'https://cardstack.com/base/date'; +import BooleanField from 'https://cardstack.com/base/boolean'; +import NumberField from 'https://cardstack.com/base/number'; +import { + Component, + CardDef, + field, + contains, + StringField, + linksTo, +} from 'https://cardstack.com/base/card-api'; + +import { + FieldContainer, + BoxelSelect, + BoxelInput, +} from '@cardstack/boxel-ui/components'; +import { action } from '@ember/object'; +import { fn } from '@ember/helper'; +import { on } from '@ember/modifier'; +import { parse, format, isBefore, isAfter } from 'date-fns'; + +const dateFormat = `yyyy-MM-dd`; + +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); + } + + const formatNumber = Math.round(currentNumber as number); + + return formatter.format(formatNumber); +}; + +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 { + get numberSent() { + let { model } = this.args; + const nearestRoundNumber = Math.round(model.numberSent || 0); + const formatNumber = nearestRoundNumber; + return formatNumberWithSeparator(formatNumber); + } + + get expectedResponsePercentage() { + let { model } = this.args; + const formatNumber = nearestDecimal( + model.expectedResponsePercentage || 0, + 2, + ); + return formatNumberWithSeparator(formatNumber, true); + } + + +} + +class Embedded extends Component { + +} + +class Edit extends Component { + get selectedStatus() { + return { + name: this.args.model.status, + }; + } + + get selectedType() { + return { + name: this.args.model.type, + }; + } + + get selectedActive() { + return this.args.model.active; + } + + get selectedStartDate() { + return this.args.model.startDate + ? format(this.args.model.startDate, dateFormat) + : null; + } + + get selectedEndDate() { + return this.args.model.endDate + ? format(this.args.model.endDate, dateFormat) + : null; + } + + @action updateName(inputText: string) { + this.args.model.name = inputText; + } + + @action updateStatus(type: { name: string }) { + this.args.model.status = type.name; + } + + @action updateActive() { + this.args.model.active = !this.args.model.active; + } + + @action updateType(type: { name: string }) { + this.args.model.type = type.name; + } + + @action updateDescription(inputText: string) { + 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.endDate && isAfter(date, this.args.model.endDate)) { + this.args.model.endDate = date; + } + this.args.model.startDate = date; + } + + @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.startDate && + isBefore(date, this.args.model.startDate) + ) { + this.args.model.startDate = date; + } + this.args.model.endDate = date; + } + + @action parseDateInput(field: string, date: string) { + const newDate = parse(date, dateFormat, new Date()); + if (field === 'startDate') { + return this.updateStartDate(newDate); + } + return this.updateEndDate(newDate); + } + + 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 { + static displayName = 'CampaignForm'; + @field name = contains(StringField, { + description: 'The campaign name', + }); + @field status = contains(StringField, { + description: 'The campaign current status', + }); + @field active = contains(BooleanField, { + description: 'Tells whether the campaign is active or not', + }); + @field type = contains(StringField, { + description: 'The type of campaign', + }); + @field parentCampaign = linksTo(() => CampaignForm, { + description: 'The parent campaign', + }); + @field description = contains(StringField, { + description: 'The campaign description', + }); + @field startDate = contains(DateField, { + description: 'The campaign start date', + }); + @field endDate = contains(DateField, { + description: 'The campaign end date', + }); + @field numberSent = contains(NumberField, { + description: 'The number of forms sent in the campaign', + }); + @field expectedResponsePercentage = contains(NumberField, { + description: 'The expected response by percentage (%) in the campaign', + }); + @field expectedRevenue = contains(NumberField, { + description: 'The expected revenue by RM in the campaign', + }); + @field budgetedCost = contains(NumberField, { + description: 'The budgeted cost by RM in the campaign', + }); + @field actualCost = contains(NumberField, { + description: 'The actual cost by RM in the campaign', + }); + + static isolated = Isolated; + static embedded = Embedded; + static atom = Embedded; + static edit = Edit; +} diff --git a/packages/drafts-realm/campaign-members-chart.gts b/packages/drafts-realm/campaign-members-chart.gts new file mode 100644 index 00000000000..a2bcc4f57e1 --- /dev/null +++ b/packages/drafts-realm/campaign-members-chart.gts @@ -0,0 +1,627 @@ +import { ContactForm } from './contact-form'; +import { LeadForm } from './lead-form'; + +import { + Component, + CardDef, + FieldDef, + field, + contains, + StringField, + linksTo, + containsMany, +} from 'https://cardstack.com/base/card-api'; +import { + FieldContainer, + BoxelSelect, + BoxelInput, +} from '@cardstack/boxel-ui/components'; +import GlimmerComponent from '@glimmer/component'; +import { action } from '@ember/object'; +import { htmlSafe } from '@ember/template'; + +// @ts-ignore +import * as d3 from 'https://cdn.jsdelivr.net/npm/d3@7.9.0/+esm'; + +class ContactMembersFieldEdit extends Component { + get selectedResponseStatus() { + return { + name: this.args.model.responseStatus, + }; + } + + get responseStatusFieldStyle() { + let css: string[] = []; + css.push('margin-top:var(--boxel-sp-sm);'); + return htmlSafe(css.join(' ')); + } + + @action updateResponseStatus(type: { name: string }) { + this.args.model.responseStatus = type.name; + } + + private responseStatuses = [{ name: 'Sent' }, { name: 'Responded' }]; + + +} + +class ContactMembersField extends FieldDef { + static displayName = 'ContactMember'; + @field contactForm = linksTo(ContactForm); + @field responseStatus = contains(StringField); + + static edit = ContactMembersFieldEdit; +} + +class LeadMembersFieldEdit extends Component { + get selectedResponseStatus() { + return { + name: this.args.model.responseStatus, + }; + } + + get responseStatusFieldStyle() { + let css: string[] = []; + css.push('margin-top:var(--boxel-sp-sm);'); + return htmlSafe(css.join(' ')); + } + + @action updateResponseStatus(type: { name: string }) { + this.args.model.responseStatus = type.name; + } + + private responseStatuses = [{ name: 'Sent' }, { name: 'Responded' }]; + + +} + +class LeadMembersField extends FieldDef { + static displayName = 'LeadMember'; + @field leadForm = linksTo(LeadForm); + @field responseStatus = contains(StringField); + + static edit = LeadMembersFieldEdit; +} + +interface ChartSignature { + Args: { + numberSent: number; + numberResponsed: number; + }; + Element: HTMLElement; +} + +class DonutChart extends GlimmerComponent { + get displayDonut() { + if (typeof document === 'undefined') { + return; + } + + const data = [ + { name: 'Sent', value: this.args.numberSent }, + { name: 'Responded', value: this.args.numberResponsed }, + ]; + + const width = 200; + const height = 200; + const radius = Math.min(width, height) / 2; + + const color = d3.scaleOrdinal(d3.schemeCategory10); + + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('width', width.toString()); + svg.setAttribute('height', height.toString()); + + const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); + g.setAttribute('transform', `translate(${width / 2}, ${height / 2})`); + + svg.appendChild(g); + + // Add middle text + const middleText = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'text', + ); + middleText.setAttribute('text-anchor', 'middle'); + middleText.setAttribute('dy', '.35em'); + middleText.setAttribute('font-size', '20px'); + middleText.textContent = ( + this.args.numberSent + this.args.numberResponsed + ).toString(); + g.appendChild(middleText); + + const tooltip = d3 + .select('.donut-chart') + .append('div') + .attr('class', 'tooltip') + .style('opacity', 0) + .style('position', 'absolute') + .style('background-color', '#ffffff') + .style('border', 'solid') + .style('border-width', '1px') + .style('border-radius', '8px') + .style('padding', '0.5rem') + .style('color', 'black') + .style('width', '160px') + .style('pointer-events', 'none'); + + const arc = d3 + .arc() + .innerRadius(radius - 50) + .outerRadius(radius); + + const pie = d3.pie().value((d: { value: any }) => d.value); + + const arcs = d3 + .select(g) + .selectAll('.arc') + .data(pie(data)) + .enter() + .append('g') + .attr('class', 'arc'); + + arcs + .append('path') + .attr('d', arc) + .style('fill', (d: any) => color(d.data.name)) + .on('mouseover', function (event, d) { + d3.select(this).style('opacity', 0.7); + tooltip + .style('opacity', 1) + .html( + `
Status
${d.data.name}
Number of members: ${d.data.value}
`, + ) + .style('left', event.layerX + 10 + 'px') + .style('top', event.layerY + 10 + 'px'); + }) + .on('mouseout', function (event, d) { + d3.select(this).style('opacity', 1); + tooltip.style('opacity', 0); + }); + + arcs + .append('text') + .attr('transform', (d: any) => 'translate(' + arc.centroid(d) + ')') + .attr('dy', '.35em') + .style('text-anchor', 'middle') + .text((d: any) => d.data.value); + + return svg; + } + + +} + +class HorizontalBarChart extends GlimmerComponent { + get displayHorizontalBar() { + if (typeof document === 'undefined') { + return; + } + + const data = [ + { name: 'Sent', value: this.args.numberSent }, + { name: 'Responded', value: this.args.numberResponsed }, + ]; + + const width = 400; + const height = 250; + const marginTop = 40; + const marginRight = 20; + const marginBottom = 40; + const marginLeft = 100; + + const color = d3.scaleOrdinal(d3.schemeCategory10); + + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('width', width.toString()); + svg.setAttribute('height', height.toString()); + + // create horizontal bar chart with category/value axis label and value axis text + const x = d3 + .scaleLinear() + .domain([0, d3.max(data, (d) => d.value)]) + .range([0, width - marginLeft - marginRight]); + + const y = d3 + .scaleBand() + .domain(data.map((d) => d.name)) + .range([0, height - marginTop - marginBottom]) + .padding(0.1); + + const g = d3 + .select(svg) + .append('g') + .attr('transform', `translate(${marginLeft}, ${marginTop})`); + + g.append('g') + .selectAll('rect') + .data(data) + .enter() + .append('rect') + .attr('y', (d) => y(d.name)) + .attr('width', (d) => x(d.value)) + .attr('height', y.bandwidth()) + .attr('fill', (d) => color(d.name)); + + g.append('g') + .selectAll('text') + .data(data) + .enter() + .append('text') + .attr('x', (d) => x(d.value) - 20) + .attr('y', (d) => y(d.name) + y.bandwidth() / 2) + .attr('dy', '0.35em') + .text((d) => d.value); + + g.append('g') + .attr('transform', `translate(0, ${height - marginTop - marginBottom})`) + .call(d3.axisBottom(x)); + + g.append('g').call(d3.axisLeft(y)); + + return svg; + } + + +} + +class VerticalBarChart extends GlimmerComponent { + get displayVerticalBar() { + if (typeof document === 'undefined') { + return; + } + + const data = [ + { name: 'Sent', value: this.args.numberSent }, + { name: 'Responded', value: this.args.numberResponsed }, + ]; + + const width = 400; + const height = 250; + const marginTop = 40; + const marginRight = 20; + const marginBottom = 40; + const marginLeft = 100; + + const color = d3.scaleOrdinal(d3.schemeCategory10); + + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('width', width.toString()); + svg.setAttribute('height', height.toString()); + + // create vertical bar chart with category/value axis label and value axis text + const x = d3 + .scaleBand() + .domain(data.map((d) => d.name)) + .range([0, width - marginLeft - marginRight]) + .padding(0.1); + + const y = d3 + .scaleLinear() + .domain([0, d3.max(data, (d) => d.value)]) + .range([height - marginTop - marginBottom, 0]); + + const g = d3 + .select(svg) + .append('g') + .attr('transform', `translate(${marginLeft}, ${marginTop})`); + + g.append('g') + .selectAll('rect') + .data(data) + .enter() + .append('rect') + .attr('x', (d) => x(d.name)) + .attr('y', (d) => y(d.value)) + .attr('width', x.bandwidth()) + .attr('height', (d) => height - marginTop - marginBottom - y(d.value)) + .attr('fill', (d) => color(d.name)); + + g.append('g') + .selectAll('text') + .data(data) + .enter() + .append('text') + .attr('x', (d) => x(d.name) + x.bandwidth() / 2) + .attr('y', (d) => y(d.value) + 15) + .attr('dy', '0.35em') + .text((d) => d.value); + + g.append('g').call(d3.axisLeft(y)); + + g.append('g') + .attr('transform', `translate(0, ${height - marginTop - marginBottom})`) + .call(d3.axisBottom(x)); + + return svg; + } + + +} + +class Isolated extends Component { + get numberSent() { + let { model } = this.args; + const contactMembers = + model.contactMembers?.filter( + (contactMember) => contactMember.responseStatus === 'Sent', + ) || []; + const leadMembers = + model.leadMembers?.filter( + (leadMember) => leadMember.responseStatus === 'Sent', + ) || []; + return contactMembers.length + leadMembers.length; + } + + get numberResponsed() { + let { model } = this.args; + const contactMembers = + model.contactMembers?.filter( + (contactMember) => contactMember.responseStatus === 'Responded', + ) || []; + const leadMembers = + model.leadMembers?.filter( + (leadMember) => leadMember.responseStatus === 'Responded', + ) || []; + return contactMembers.length + leadMembers.length; + } + + get chartType() { + return this.args.model.chartType; + } + + get isChartTypeDonut() { + return this.chartType === 'Donut'; + } + + get isChartTypeHorizontalBar() { + return this.chartType === 'Horizontal Bar'; + } + + get isChartTypeVerticalBar() { + return this.chartType === 'Vertical Bar'; + } + + +} + +class Embedded extends Component { + +} + +class Edit extends Component { + get selectedChartType() { + return { + name: this.args.model.chartType, + }; + } + + @action updateName(inputText: string) { + this.args.model.name = inputText; + } + + @action updateChartType(type: { name: string }) { + this.args.model.chartType = type.name; + } + + private campaignChartTypes = [ + { name: 'Donut' }, + { name: 'Vertical Bar' }, + { name: 'Horizontal Bar' }, + ]; + + +} + +export class CampaignMembersChart extends CardDef { + static displayName = 'CampaignMembersChart'; + + @field name = contains(StringField, { + description: 'The campaign name', + }); + @field chartType = contains(StringField, { + description: + 'Chart type that will be displayed for showing sent and responded members', + }); + @field contactMembers = containsMany(ContactMembersField, { + description: 'Contact members of the campaign, each with response status', + }); + @field leadMembers = containsMany(LeadMembersField, { + description: 'Lead members of the campaign, each with response status', + }); + + static isolated = Isolated; + static embedded = Embedded; + static atom = Embedded; + static edit = Edit; +} 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/contact-form.gts b/packages/drafts-realm/contact-form.gts new file mode 100644 index 00000000000..80356099e23 --- /dev/null +++ b/packages/drafts-realm/contact-form.gts @@ -0,0 +1,302 @@ +import { UserName } from './user-name'; +import { UserEmail } from './user-email'; +import { AddressInfo } from './address-info'; +import { + CardDef, + field, + contains, + linksTo, +} 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 } from '@cardstack/boxel-ui/components'; +import { MatrixUser } from './matrix-user'; + +class Isolated extends Component { + +} + +class View extends Component { + +} + +class Edit extends Component { + +} + +export class ContactForm extends CardDef { + @field title = contains(StringField, { + computeVia: function (this: ContactForm) { + const { salutation, firstName, lastName } = this.user; + + if (!salutation || !firstName || !lastName) return 'User Not Found'; + return `${salutation} ${firstName} ${lastName}`; + }, + }); + @field user = contains(UserName, { + description: `User's Full Name`, + }); + @field accountName = contains(StringField, { + description: `User's Account 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`, + }); + @field owner = linksTo(MatrixUser, { + description: `Owner`, + }); + + static displayName = 'Contact Form'; + static isolated = Isolated; + static embedded = View; + static atom = View; + static edit = Edit; +} 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/currency-amount.gts b/packages/drafts-realm/currency-amount.gts new file mode 100644 index 00000000000..4e566a078ef --- /dev/null +++ b/packages/drafts-realm/currency-amount.gts @@ -0,0 +1,132 @@ +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, + 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..b69118affe1 --- /dev/null +++ b/packages/drafts-realm/lead-form.gts @@ -0,0 +1,513 @@ +import MarkdownField from 'https://cardstack.com/base/markdown'; +import { + CardDef, + contains, + field, + linksTo, +} 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'; +import { MatrixUser } from './matrix-user'; +import { Company } from './crm/account'; + +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 }; + } + + @tracked leadStatusOptions = [ + { name: 'New' }, + { name: 'Working' }, + { name: 'Nurturing' }, + { name: 'Unqualified' }, + { name: 'Qualified' }, + ] 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 }; + } + + @tracked leadSourceOptions = [ + { 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 }; + } + + @tracked industryOptions = [ + { 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 title = contains(StringField, { + computeVia: function (this: LeadForm) { + const { salutation, firstName, lastName } = this.user; + + if (!salutation || !firstName || !lastName) return 'User Not Found'; + return `${salutation} ${firstName} ${lastName}`; + }, + }); + @field user = contains(UserName, { + description: `User's Full Name`, + }); + @field company = linksTo(Company, { + description: `User's Company Name`, + }); + @field website = contains(StringField, { + description: `User's Website`, + }); + @field description = contains(MarkdownField, { + description: `User's Description`, + }); + @field leadStatus = contains(StringField, { + description: `Lead Status`, + }); + @field owner = linksTo(MatrixUser, { + description: `Owner`, + }); + @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; +} 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/opportunity-form.gts b/packages/drafts-realm/opportunity-form.gts new file mode 100644 index 00000000000..a56bb1cf835 --- /dev/null +++ b/packages/drafts-realm/opportunity-form.gts @@ -0,0 +1,569 @@ +import MarkdownField from 'https://cardstack.com/base/markdown'; +import { + CardDef, + FieldDef, + contains, + field, + linksTo, +} 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 { + BoxelInput, + BoxelSelect, + CardContainer, + FieldContainer, +} from '@cardstack/boxel-ui/components'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { Company } from './crm/account'; +import NumberField from '../base/number'; +import { MatrixUser } from './matrix-user'; + +interface CategorySignature { + name: string; + percentage?: number; +} + +const formatNumber = (val: number) => { + let formatter = new Intl.NumberFormat('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }); + return formatter.format(val); +}; + +/* Amount */ +class EmbeddedSecForAmount extends Component { + +} + +class EditSecForAmount extends Component { + @tracked currencyOptions = ['Select', 'RM']; + 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; + } + + +} + +class AmountField extends FieldDef { + @field currency = contains(StringField, { + description: `Currency`, + }); + + @field totalAmount = contains(NumberField, { + description: `Total Amount`, + }); + + static embedded = EmbeddedSecForAmount; + static edit = EditSecForAmount; +} + +/* Opportunity Form */ +class IsolatedSecForOpportunityForm extends Component { + get getCompanyName() { + if (!this.args.model.company) return '-'; + return this.args.model.company; + } + + get getFormattedAmount() { + const amount = this.args.model.amount; + const hasAmount = amount && amount.totalAmount; + if (!hasAmount) return null; + const formattedNumber = formatNumber(amount.totalAmount); + return formattedNumber; + } + + get getTotalCurrencyAmount() { + const amount = this.args.model.amount; + const hasTotalCurrency = + amount && + amount.currency && + amount.currency !== 'Select' && + this.getFormattedAmount; + if (!hasTotalCurrency) return '-'; + return amount.currency + ' ' + this.getFormattedAmount; + } + + get getStage() { + if (!this.args.model.stage) return '-'; + return this.args.model.stage; + } + + get getFormattedPercentage() { + if (!this.args.model.percentage) return '-'; + + const formattedNumber = formatNumber( + Math.round(this.args.model.percentage), + ); + + return formattedNumber + '%'; + } + + get getForestCategory() { + if (!this.args.model.forecastCategory) return '-'; + return this.args.model.forecastCategory; + } + + +} + +class EditSecForOpportunityForm extends Component { + /* Stage Category */ + get selectedStageCategory() { + return { + name: + this.args.model.stage || this.stageCategoryOptions[0].name || 'Select', + }; + } + + @tracked stageCategoryOptions = [ + { name: 'None', percentage: 0 }, + { name: 'Qualification', percentage: 10 }, + { name: 'Needs Analysis', percentage: 25 }, + { name: 'Proposal', percentage: 50 }, + { name: 'Negotiation', percentage: 75 }, + { name: 'Closed Won', percentage: 100 }, + { name: 'Closed Lost', percentage: 100 }, + ] as unknown as Array; + + @action updateStageCategory(type: { name: string; percentage: number }) { + this.args.model.stage = type.name; + this.args.model.percentage = type.percentage; + } + + /* Percentage */ + @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 + '%'; + } + + /* Forecast Category */ + @tracked forecastCategoryOptions = [ + { name: 'None' }, + { name: 'Omitted' }, + { name: 'Pipeline' }, + { name: 'Best Case' }, + { name: 'Commit' }, + { name: 'Closed' }, + ] as Array; + + @tracked selectedForecastCategory = { + name: + this.args.model.forecastCategory || + this.forecastCategoryOptions[0].name || + 'Select', + }; + + @action updateForecastCategory(type: { name: string }) { + this.selectedForecastCategory = type; + this.args.model.forecastCategory = type.name; + } + + +} + +class ViewSecForOpportunityForm extends Component { + +} + +export class OpportunityForm extends CardDef { + static displayName = 'Opportunity Form'; + @field title = contains(StringField, { + computeVia: function (this: OpportunityForm) { + return this.opportunityName; + }, + }); + @field opportunityName = contains(StringField, { + description: `Opportunity Name`, + }); + @field accountName = contains(StringField, { + description: `Account Name`, + }); + @field company = linksTo(Company, { + description: `User's Company Name`, + }); + @field closeDate = contains(DateCard, { + description: `Close Date`, + }); + @field amount = contains(AmountField, { + description: `Amount`, + }); + @field description = contains(MarkdownField, { + description: `Description`, + }); + @field stage = contains(StringField, { + description: `Stage`, + }); + @field percentage = contains(NumberField, { + description: `Percentage`, + }); + @field forecastCategory = contains(StringField, { + description: `Forecast Category`, + }); + @field owner = linksTo(MatrixUser, { + description: `Owner`, + }); + + static isolated = IsolatedSecForOpportunityForm; + static edit = EditSecForOpportunityForm; + static embedded = ViewSecForOpportunityForm; + static atom = ViewSecForOpportunityForm; +} 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 { + + }; +} diff --git a/packages/drafts-realm/sale-hub.gts b/packages/drafts-realm/sale-hub.gts new file mode 100644 index 00000000000..8b1d63e0bed --- /dev/null +++ b/packages/drafts-realm/sale-hub.gts @@ -0,0 +1,1256 @@ +import { + CardDef, + FieldDef, + contains, + containsMany, + field, + linksTo, +} from 'https://cardstack.com/base/card-api'; +import DateCard from 'https://cardstack.com/base/date'; +import { Component } from 'https://cardstack.com/base/card-api'; + +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { on } from '@ember/modifier'; +import { concat, fn } from '@ember/helper'; +import { IconX } from '@cardstack/boxel-ui/icons'; +import GlimmerComponent from '@glimmer/component'; +import { + BoxelSelect, + Button, + CardContainer, + FieldContainer, + GridContainer, + IconButton, + Modal, +} from '@cardstack/boxel-ui/components'; +import MarkdownField from '../base/markdown'; +import { CrmAccount } from './crm/account'; +import StringField from 'https://cardstack.com/base/string'; +import BooleanField from 'https://cardstack.com/base/boolean'; +import { + format as formatDate, + isToday, + isTomorrow, + isThisMonth, +} from 'date-fns'; +import { OpportunityForm } from './opportunity-form'; +import { LeadForm } from './lead-form'; +import { ContactForm } from './contact-form'; +import { MatrixUser } from './matrix-user'; +import { eq } from '@cardstack/boxel-ui/helpers'; + +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; + isActive: boolean; + isCompleted: boolean; + isProceedToNextStep: boolean; +} + +interface StepsSignature { + Element: HTMLElement; + Args: { + steps: StepSignature[]; + updateLeadStatus: (arg0: string) => void; + isLeadFormConverted: boolean | undefined; + handleConvert: () => void; + }; +} + +interface CategorySignature { + name: string; +} + +interface GroupedTasksSignature { + month: string | null; + taskId: string | null; + subject: string | null; + dueDate: Date | any; + comments: string | null; + isCompleted: boolean; + relatedTo: any; +} + +//*task-form +class TaskForm extends FieldDef { + static displayName = 'Task Form'; + + @field taskId = contains(StringField, { + description: `Task Id`, + }); + @field subject = contains(StringField, { + description: `Subject`, + }); + @field dueDate = contains(DateCard, { + description: `Due Date`, + }); + @field comments = contains(MarkdownField, { + description: `Comments`, + }); + @field relatedTo = linksTo(CrmAccount, { + description: `Related to Crm Account`, + }); + @field isCompleted = contains(BooleanField, { + description: `Is Task Completed`, + }); +} + +//*Target Page Links +class TargetPageLinks extends GlimmerComponent { + +} + +//*steps +class Steps extends GlimmerComponent { + @tracked steps = this.args.steps.map((step) => { + return { ...step }; + }); + @tracked currentStepName = this.steps[0].name || ''; + @tracked currentStepIndex = this.steps[0].step || 0; + + get isStepCompleted() { + return this.currentStepStatus === 'Step Completed'; + } + + get currentStepStatus() { + if (this.currentStepIndex >= this.steps.length - 1) { + if (this.args.isLeadFormConverted) return 'Converted'; + return 'Select Converted Status'; + } + if (this.steps[this.currentStepIndex].isCompleted) return 'Step Completed'; + return 'Mark Status as Complete'; + } + + get shouldDisableClick() { + return this.isStepCompleted || this.args.isLeadFormConverted; + } + + @action + handleOnStepClick(clickedStep: number) { + this.currentStepIndex = clickedStep; + this.currentStepName = this.steps[this.currentStepIndex].name; + + this.steps = this.steps.map((step, index) => { + return { ...step, isActive: index === clickedStep }; + }); + } + + @action handleCompleteStep() { + if (this.currentStepIndex < this.steps.length - 1) { + this.steps = this.steps.map((step, index) => { + return { + ...step, + isCompleted: index <= this.currentStepIndex, + isProceedToNextStep: index === this.currentStepIndex + 1, + }; + }); + } + + this.args.updateLeadStatus(this.currentStepName); + } + + @action + handleButtonClick() { + if (this.currentStepStatus === 'Select Converted Status') { + this.args.handleConvert(); + } else { + this.handleCompleteStep(); + } + } + + +} + +//*scheduled task +class ScheduledTask extends FieldDef { + static displayName = 'Scheduled Task'; + @field taskForm = contains(TaskForm); + + static embedded = class Embedded extends Component { + + }; +} + +class IsolatedSecForSaleHub extends Component { + //convert lead-form Modal + @tracked isModalVisible = false; + + @action + toggleModal() { + this.isModalVisible = !this.isModalVisible; + } + + @action + openModal() { + this.isModalVisible = true; + } + + @action + closeModal() { + this.isModalVisible = false; + } + + @action + handleConvert() { + this.openModal(); + this.updateAccountForm(); + this.updateContactForm(); + this.updateOpportunityAccount(); + } + + //task-form modal + @tracked isTaskFormModalVisible = false; + + @action + openTaskFormModal() { + this.isTaskFormModalVisible = true; + } + + @action + closeTaskFormModal() { + this.isTaskFormModalVisible = false; + } + + //task-form modal by Id + @tracked selectedTask: GroupedTasksSignature | null = null; + @tracked isTaskFormModalByIdVisible = false; + + @action + openTaskFormModaById(id: string) { + this.isTaskFormModalByIdVisible = true; + const filteredTask = this.tasks.find((task) => task.taskId == id); + if (!filteredTask) return; + this.selectedTask = filteredTask; + } + + @action + closeTaskFormModalById() { + this.isTaskFormModalByIdVisible = false; + } + + //auto bind form + @action + updateAccountForm() { + if ( + this.args.model && + this.args.model.accountForm && + this.args.model.leadForm + ) { + const { name } = this.args.model.leadForm.company; + this.args.model.accountForm.accountName = name; + } + } + + @action + updateContactForm() { + if ( + this.args.model && + this.args.model.contactForm && + this.args.model.leadForm + ) { + const { company, user } = this.args.model.leadForm; + const { salutation, firstName, lastName } = user; + + this.args.model.contactForm.user.salutation = salutation; + this.args.model.contactForm.user.firstName = firstName; + this.args.model.contactForm.user.lastName = lastName; + + this.args.model.contactForm.accountName = company.name; + } + } + + @action + updateOpportunityAccount() { + if ( + this.args.model && + this.args.model.opportunityForm && + this.args.model.leadForm + ) { + const { company } = this.args.model.leadForm; + + this.args.model.opportunityForm.opportunityName = company.name; + this.args.model.opportunityForm.accountName = company.name; + this.args.model.opportunityForm.company = company; + } + } + + get accountName() { + if (!this.args.model.leadForm) return '-'; + const { company } = this.args.model.leadForm; + + if (!company) return '-'; + return company.name; + } + + get user() { + if (!this.args.model.leadForm || !this.args.model.leadForm.user) return '-'; + const { salutation, firstName, lastName } = this.args.model.leadForm.user; + + if (!salutation || !firstName || !lastName) return 'User Not Found'; + return `${salutation} ${firstName} ${lastName}`; + } + + //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, + name: 'New', + isActive: true, + isCompleted: false, + isProceedToNextStep: false, + }, + { + step: 1, + name: 'Working', + isActive: false, + isCompleted: false, + isProceedToNextStep: false, + }, + { + step: 2, + name: 'Nurturing', + isActive: false, + isCompleted: false, + isProceedToNextStep: false, + }, + { + step: 3, + name: 'Unqualified', + isActive: false, + isCompleted: false, + isProceedToNextStep: false, + }, + { + step: 4, + name: 'Converted', + isActive: false, + isCompleted: false, + isProceedToNextStep: false, + }, + ] as Array; + + get stepOptions() { + let currentCompletedStep = this.initStepOptions.findIndex( + (option) => option.name === this.args.model.leadForm?.leadStatus, + ); + + this.initStepOptions.map((option, index) => { + option.isCompleted = + index <= currentCompletedStep || + this.args.model.leadForm?.leadStatus === 'Qualified' || + !!this.args.model.isLeadFormConverted; + option.isProceedToNextStep = index === currentCompletedStep + 1; + }); + + return this.initStepOptions; + } + @action rerenderState() { + this.stepOptions.map((option) => { + option.isCompleted = !!this.args.model.isLeadFormConverted; + }); + + return this.stepOptions; + } + + @action updateLeadStatus(status: string) { + if (!(this.args.model.leadForm && this.args.model.leadForm.leadStatus)) + return; + + //bug for sync + this.args.model.leadForm.leadStatus = status; + } + + //upcoming task list + get tasks() { + const { scheduledTask } = this.args.model; + + if (!scheduledTask) return []; + + return scheduledTask.map((task) => { + const { taskForm } = task; + return { + month: taskForm.dueDate ? this.formatDueDate(taskForm.dueDate) : null, + taskId: taskForm.taskId, + subject: taskForm.subject, + dueDate: taskForm.dueDate, + comments: taskForm.comments, + isCompleted: taskForm.isCompleted, + relatedTo: taskForm.relatedTo, + }; + }); + } + + get groupedTasks() { + if (!this.tasks) return; + + const groupedTasks: { [key: string]: GroupedTasksSignature[] } = + this.tasks.reduce( + ( + acc: { [key: string]: GroupedTasksSignature[] }, + task: GroupedTasksSignature, + ) => { + if (!!task.month) { + if (!acc[task.month]) { + acc[task.month] = []; + } + acc[task.month].push(task); + } + return acc; + }, + {}, + ); + + const sortedMonths = Object.keys(groupedTasks).sort((a, b) => { + if (a === 'Upcoming') return -1; + if (b === 'Overdue') return 1; + return new Date(a).getTime() - new Date(b).getTime(); + }); + + const sortedGroupedTasks: any = sortedMonths.reduce( + (acc: any, month: string) => { + acc[month] = groupedTasks[month].sort( + (a, b) => + new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime(), + ); + return acc; + }, + {}, + ); + + return sortedGroupedTasks; + } + + @action formatDueDate(date: Date) { + const todayDate = new Date(); + + 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; + } + + @action toggleOnCheckTask(taskIdChecked: string) { + this.args.model.scheduledTask = this.args.model.scheduledTask?.map( + (task) => { + task.taskForm.taskId === taskIdChecked + ? (task.taskForm.isCompleted = !task.taskForm.isCompleted) + : task.taskForm.isCompleted; + + return task; + }, + ); + + return this.args.model.scheduledTask; + } + + /* Converted Status Options */ + get selectedConvertedStatus() { + return { + name: + this.args.model.convertedStatus || + this.convertedStatusOptions[0].name || + null, + }; + } + + @tracked convertedStatusOptions = [ + { name: 'Unqualified' }, + { name: 'Qualified' }, + ] 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.rerenderState(); + this.closeModal(); + } + + //if else condition + get shouldShowLeadForm() { + return !!this.args.model.leadForm ? true : false; + } + + get shouldShowAccountForm() { + return !!this.args.model.accountForm ? true : false; + } + + get shouldShowContactForm() { + return !!this.args.model.contactForm ? true : false; + } + + get shouldShowOpportunityForm() { + return !!this.args.model.opportunityForm ? true : false; + } + + +} + +export class SaleHub extends CardDef { + static displayName = 'sale hub'; + + @field targetPage = contains(StringField, { + description: `Show which page is clicked`, + }); + @field leadForm = linksTo(LeadForm, { + description: `Lead form`, + }); + @field accountForm = linksTo(CrmAccount, { + description: `Account Form`, + }); + @field contactForm = linksTo(ContactForm, { + description: `Contact Form`, + }); + @field opportunityForm = linksTo(OpportunityForm, { + 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; +} diff --git a/packages/drafts-realm/task-form.gts b/packages/drafts-realm/task-form.gts new file mode 100644 index 00000000000..b0b05b8dadd --- /dev/null +++ b/packages/drafts-realm/task-form.gts @@ -0,0 +1,223 @@ +import MarkdownField from 'https://cardstack.com/base/markdown'; +import { + CardDef, + contains, + field, + linksTo, +} 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; +} + +/* Task Form */ +class IsolatedSecForTaskForm extends Component { + +} + +class ViewSecForTaskForm extends Component { + +} + +class EditSecForTaskForm extends Component { + get selectedSubject() { + return { name: this.args.model.subject || 'None' }; + } + + @tracked subjectOptions = [ + { name: 'None' }, + { name: 'Email' }, + { name: 'Call' }, + { name: 'Send Letter' }, + { name: 'Send Quote' }, + { name: 'Other' }, + ] as Array; + + @action updateSubject(type: { name: string }) { + this.args.model.subject = type.name; + } + + +} + +export class TaskForm extends CardDef { + static displayName = 'Task Form'; + @field taskId = contains(StringField, { + description: `Task Id`, + }); + @field subject = contains(StringField, { + description: `Subject`, + }); + @field dueDate = contains(DateCard, { + description: `Due Date`, + }); + @field comments = contains(MarkdownField, { + description: `Comments`, + }); + @field relatedTo = linksTo(CrmAccount, { + description: `Related to Crm Account`, + }); + + static isolated = IsolatedSecForTaskForm; + static atom = ViewSecForTaskForm; + static embedded = ViewSecForTaskForm; + static edit = EditSecForTaskForm; +} diff --git a/packages/drafts-realm/user-email.gts b/packages/drafts-realm/user-email.gts new file mode 100644 index 00000000000..a1c002d13f4 --- /dev/null +++ b/packages/drafts-realm/user-email.gts @@ -0,0 +1,115 @@ +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 } 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..098a4f254e2 --- /dev/null +++ b/packages/drafts-realm/user-name.gts @@ -0,0 +1,124 @@ +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 { + get selectedSalutationCategory() { + return { + name: + this.args.model.salutation || + this.salutationCategory[0].name || + 'Select', + }; + } + + @tracked salutationCategory = [ + { name: 'None' }, + { name: 'Mr.' }, + { name: 'Ms.' }, + { name: 'Mrs.' }, + { name: 'Dr.' }, + { name: 'Prof.' }, + { name: 'Mx.' }, + ] as Array; + + @action updateSalutationCategory(type: { name: string }) { + 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; +}