From f82e938e88c7e3633573c6045d07c2d97ba76a80 Mon Sep 17 00:00:00 2001 From: Richard Tan Date: Mon, 20 Jan 2025 22:38:14 +0800 Subject: [PATCH 1/7] Add task and query tasks in deal page --- packages/experiments-realm/crm/deal.gts | 118 +++++++++++++++++++++++- 1 file changed, 117 insertions(+), 1 deletion(-) diff --git a/packages/experiments-realm/crm/deal.gts b/packages/experiments-realm/crm/deal.gts index d9450cec00..353cedc83b 100644 --- a/packages/experiments-realm/crm/deal.gts +++ b/packages/experiments-realm/crm/deal.gts @@ -14,7 +14,7 @@ import DateField from 'https://cardstack.com/base/date'; import GlimmerComponent from '@glimmer/component'; import SummaryCard from '../components/summary-card'; import SummaryGridContainer from '../components/summary-grid-container'; -import { Pill } from '@cardstack/boxel-ui/components'; +import { Pill, BoxelButton } from '@cardstack/boxel-ui/components'; import Info from '@cardstack/boxel-icons/info'; import AccountHeader from '../components/account-header'; import CrmProgressBar from '../components/crm-progress-bar'; @@ -43,6 +43,9 @@ import BooleanField from 'https://cardstack.com/base/boolean'; import { getCards } from '@cardstack/runtime-common'; import { Query } from '@cardstack/runtime-common/query'; import { Company } from './company'; +import type { LooseSingleCardDocument } from '@cardstack/runtime-common'; +import { restartableTask } from 'ember-concurrency'; +import { on } from '@ember/modifier'; interface DealSizeSummary { summary: string; @@ -50,6 +53,11 @@ interface DealSizeSummary { positive: boolean; } +const taskSource = { + module: new URL('./task', import.meta.url).href, + name: 'CRMTask', +}; + class IsolatedTemplate extends Component { get logoURL() { //We default to account thumbnail @@ -90,6 +98,10 @@ class IsolatedTemplate extends Component { return [this.realmURL?.href]; } + get dealId() { + return this.args.model.id; + } + get dealQuery(): Query { return { filter: { @@ -101,10 +113,83 @@ class IsolatedTemplate extends Component { }; } + get activeTasksQuery(): Query { + let everyArr = []; + if (this.dealId) { + everyArr.push({ + eq: { + 'deal.id': { + id: this.dealId, + }, + }, + }); + } + return { + filter: { + on: taskSource, + every: everyArr, + }, + }; + } + query = getCards(this.dealQuery, this.realmHrefs, { isLive: true, }); + activeTasks = getCards(this.activeTasksQuery, this.realmHrefs, { + isLive: true, + }); + + private _createNewTask = restartableTask(async () => { + let doc: LooseSingleCardDocument = { + data: { + type: 'card', + attributes: { + name: null, + details: null, + status: { + index: 1, + label: 'In Progress', + }, + priority: { + index: null, + label: null, + }, + description: null, + thumbnailURL: null, + }, + relationships: { + assignee: { + links: { + self: null, + }, + }, + deal: { + links: { + self: this.dealId ?? null, + }, + }, + }, + meta: { + adoptsFrom: taskSource, + }, + }, + }; + + await this.args.context?.actions?.createCard?.( + taskSource, + new URL(taskSource.module), + { + realmURL: this.realmURL, + doc, + }, + ); + }); + + createNewTask = () => { + this._createNewTask.perform(); + }; + @action dealSizeSummary(deals: CardDef[]): DealSizeSummary | null { //currently only assumes everything works in USD let nonZeroDeals = (deals as Deal[]).filter( @@ -360,6 +445,37 @@ class IsolatedTemplate extends Component { + + <:title> + + + <:icon> + + New Task + + + <:content> +
+ {{#if this.activeTasks.isLoading}} + Loading... + {{else if this.activeTasks.instances}} + {{this.activeTasks.instances.length}} + {{else}} +
+ No Active Tasks +
+ {{/if}} +
+ +
+ From 71b72e6ac45dfb502e29ffeba503da0398867b1c Mon Sep 17 00:00:00 2001 From: Richard Tan Date: Tue, 21 Jan 2025 11:00:06 +0800 Subject: [PATCH 2/7] Add task and query tasks in account page --- .../bf437dc3-1f96-45e5-a057-3487c6a5c2f7.json | 2 +- packages/experiments-realm/crm/account.gts | 128 +++++++++++++++++- 2 files changed, 128 insertions(+), 2 deletions(-) diff --git a/packages/experiments-realm/Account/bf437dc3-1f96-45e5-a057-3487c6a5c2f7.json b/packages/experiments-realm/Account/bf437dc3-1f96-45e5-a057-3487c6a5c2f7.json index 78917998eb..30d6e08f7e 100644 --- a/packages/experiments-realm/Account/bf437dc3-1f96-45e5-a057-3487c6a5c2f7.json +++ b/packages/experiments-realm/Account/bf437dc3-1f96-45e5-a057-3487c6a5c2f7.json @@ -46,7 +46,7 @@ }, "primaryContact": { "links": { - "self": "../Customer/dddaeabf-1e95-480c-b158-0873e31fc66c" + "self": "../Lead/4e70a791-dd4e-4b39-99a1-bb8070392437" } }, "contacts": { diff --git a/packages/experiments-realm/crm/account.gts b/packages/experiments-realm/crm/account.gts index b13dcdad7d..1f7c3acf6b 100644 --- a/packages/experiments-realm/crm/account.gts +++ b/packages/experiments-realm/crm/account.gts @@ -38,6 +38,15 @@ import { Pill } from '@cardstack/boxel-ui/components'; import { Query } from '@cardstack/runtime-common/query'; import { getCards } from '@cardstack/runtime-common'; import { Deal } from './deal'; +import type { LooseSingleCardDocument } from '@cardstack/runtime-common'; +import { restartableTask } from 'ember-concurrency'; +import { on } from '@ember/modifier'; +import { not } from '@cardstack/boxel-ui/helpers'; + +const taskSource = { + module: new URL('./task', import.meta.url).href, + name: 'CRMTask', +}; export const urgencyTagValues = [ { @@ -139,6 +148,10 @@ class IsolatedTemplate extends Component { return [this.realmURL?.href]; } + get accountId() { + return this.args.model.id; + } + // Query All Active Deal that linked to current Account get dealQuery(): Query { return { @@ -155,10 +168,81 @@ class IsolatedTemplate extends Component { }; } + get activeTasksQuery(): Query { + let everyArr = []; + if (this.accountId) { + everyArr.push({ + eq: { + 'account.id': this.accountId, + }, + }); + } + return { + filter: { + on: taskSource, + every: everyArr, + }, + }; + } + deals = getCards(this.dealQuery, this.realmHrefs, { isLive: true, }); + activeTasks = getCards(this.activeTasksQuery, this.realmHrefs, { + isLive: true, + }); + + private _createNewTask = restartableTask(async () => { + let doc: LooseSingleCardDocument = { + data: { + type: 'card', + attributes: { + name: null, + details: null, + status: { + index: 1, + label: 'In Progress', + }, + priority: { + index: null, + label: null, + }, + description: null, + thumbnailURL: null, + }, + relationships: { + assignee: { + links: { + self: null, + }, + }, + account: { + links: { + self: this.accountId ?? null, + }, + }, + }, + meta: { + adoptsFrom: taskSource, + }, + }, + }; + + await this.args.context?.actions?.createCard?.( + taskSource, + new URL(taskSource.module), + { + realmURL: this.realmURL, + doc, + }, + ); + }); + + createNewTask = () => { + this._createNewTask.perform(); + }; + get activeDealsCount() { const deals = this.deals; if (!deals || deals.isLoading) { @@ -445,6 +529,45 @@ class IsolatedTemplate extends Component { + + <:tasks> + + <:title> +

Upcoming Tasks

+ + <:icon> + + {{#if (not this._createNewTask.isRunning)}} + + {{/if}} + + + {{#if (not this._createNewTask.isRunning)}} + + {{/if}} + New Task + + + <:content> + {{! UI for upcoming tasks }} + +
+ @@ -135,18 +169,10 @@ class FittedTemplate extends Component {
-
- <@fields.primaryEmail @format='atom' /> -
-
- <@fields.secondaryEmail @format='atom' /> -
-
- <@fields.phoneMobile @format='atom' /> -
-
- <@fields.phoneOffice @format='atom' /> -
+ <@fields.primaryEmail @format='atom' /> + <@fields.secondaryEmail @format='atom' /> + <@fields.phoneMobile @format='atom' /> + <@fields.phoneOffice @format='atom' />
{{#if this.hasSocialLinks}} @@ -162,21 +188,25 @@ class FittedTemplate extends Component { } + +function getComponent(cardOrField: BaseDef) { + return cardOrField.constructor.getComponent(cardOrField); +} diff --git a/packages/experiments-realm/crm/task.gts b/packages/experiments-realm/crm/task.gts index be0d77f374..a317ff4628 100644 --- a/packages/experiments-realm/crm/task.gts +++ b/packages/experiments-realm/crm/task.gts @@ -18,8 +18,14 @@ import { Contact } from './contact'; import { Representative } from './representative'; import { Account } from './account'; import { Deal } from './deal'; -import { Task, TaskStatusField, getDueDateStatus } from '../task'; +import { + Task, + TaskStatusField, + getDueDateStatus, + TaskCompletionStatus, +} from '../task'; import { CrmApp } from '../crm-app'; +import EntityDisplayWithIcon from '../components/entity-icon-display'; export class Issues extends CardDef { static displayName = 'Issues'; @@ -40,6 +46,10 @@ function shortenId(id: string): string { } class TaskIsolated extends Component { + get taskTitle() { + return this.args.model.name ?? 'No Task Title'; + } + get dueDate() { return this.args.model.dateRange?.end; } @@ -60,7 +70,7 @@ class TaskIsolated extends Component {
-

{{@model.name}}

+

{{this.taskTitle}}

in {{@model.status.label}} @@ -188,7 +198,7 @@ class TaskIsolated extends Component { .task-container { --task-font-weight-500: 500; --task-font-weight-600: 600; - --tasl-font-size-extra-small: calc(var(--boxel-font-size-xs) * 0.95); + --task-font-size-extra-small: calc(var(--boxel-font-size-xs) * 0.95); padding: var(--boxel-sp-lg); container-type: inline-size; } @@ -270,7 +280,7 @@ class TaskIsolated extends Component { height: 24px; border: none; border-radius: 5px 0 0 5px; - font-size: var(--tasl-font-size-extra-small); + font-size: var(--task-font-size-extra-small); font-weight: var(--task-font-weight-500); position: relative; padding: var(--boxel-sp-5xs) var(--boxel-sp-sm) var(--boxel-sp-5xs) @@ -284,7 +294,7 @@ class TaskIsolated extends Component { ); } .calendar-icon-container { - font-size: var(--tasl-font-size-extra-small); + font-size: var(--task-font-size-extra-small); display: inline-flex; align-items: center; gap: var(--boxel-sp-xxxs); @@ -356,6 +366,153 @@ class TaskIsolated extends Component { } } +export class TaskEmbedded extends Component { + get taskTitle() { + return this.args.model.name ?? 'No Task Title'; + } + + get shortId() { + return this.args.model.shortId; + } + + get hasShortId() { + return Boolean(this.shortId); + } + + get visibleTags() { + return [this.args.fields.tags[0], this.args.fields.tags[1]].filter(Boolean); + } + + get dueDate() { + return this.args.model.dateRange?.end; + } + + get dueDateStatus() { + return this.dueDate ? getDueDateStatus(this.dueDate.toString()) : undefined; + } + + get hasDueDate() { + return Boolean(this.dueDate); + } + + get hasDueDateStatus() { + return Boolean(this.dueDateStatus); + } + + get isCompleted() { + return this.args.model.status?.completed ?? false; + } + + get hasStatus() { + return this.args.model.status?.label ?? false; + } + + +} + export class CRMTaskStatusField extends TaskStatusField { static values = [ { index: 0, label: 'Not Started', color: '#B0BEC5', completed: false }, @@ -405,4 +562,5 @@ export class CRMTask extends Task { }); static isolated = TaskIsolated; + static embedded = TaskEmbedded; }