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 0000000000..178335ef15 --- /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 0000000000..a1a1b982fd --- /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/campaign-form.gts b/packages/drafts-realm/campaign-form.gts new file mode 100644 index 0000000000..c1f517a69b --- /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; +}