@things-factory/sales 10.1.255 → 10.1.257
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-client/entity360/opportunity-360.d.ts +3 -4
- package/dist-client/entity360/opportunity-360.js +16 -38
- package/dist-client/entity360/opportunity-360.js.map +1 -1
- package/dist-client/pages/opportunity-editor.d.ts +25 -33
- package/dist-client/pages/opportunity-editor.js +108 -175
- package/dist-client/pages/opportunity-editor.js.map +1 -1
- package/dist-client/pages/opportunity-list.d.ts +2 -3
- package/dist-client/pages/opportunity-list.js +6 -12
- package/dist-client/pages/opportunity-list.js.map +1 -1
- package/dist-client/pages/quotation-editor.d.ts +45 -26
- package/dist-client/pages/quotation-editor.js +225 -248
- package/dist-client/pages/quotation-editor.js.map +1 -1
- package/dist-client/pages/quotation-list.d.ts +7 -3
- package/dist-client/pages/quotation-list.js +26 -27
- package/dist-client/pages/quotation-list.js.map +1 -1
- package/dist-client/pages/sales-product-list.d.ts +7 -2
- package/dist-client/pages/sales-product-list.js +36 -55
- package/dist-client/pages/sales-product-list.js.map +1 -1
- package/dist-client/sales-product-pick.d.ts +4 -2
- package/dist-client/sales-product-pick.js +36 -32
- package/dist-client/sales-product-pick.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/index.d.ts +1 -0
- package/dist-server/index.js +1 -0
- package/dist-server/index.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/dist-server/work-doors.d.ts +1 -0
- package/dist-server/work-doors.js +13 -0
- package/dist-server/work-doors.js.map +1 -0
- package/package.json +10 -10
- package/translations/en.json +4 -1
- package/translations/es.json +4 -1
- package/translations/ja.json +4 -1
- package/translations/ko.json +4 -1
- package/translations/ms.json +4 -1
- package/translations/zh.json +4 -1
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { LitElement } from 'lit';
|
|
2
|
-
import { type
|
|
2
|
+
import { type Entity360Subject } from '@operato/layout';
|
|
3
3
|
/** An opportunity's 360 (ADR-0067). The module draws the opportunity; an application hangs its own sections on 'opportunity'. */
|
|
4
4
|
export declare function registerOpportunityKind(): void;
|
|
5
5
|
export declare class OpportunityOverviewSection extends LitElement {
|
|
6
6
|
static styles: import("lit").CSSResult;
|
|
7
7
|
subject?: Entity360Subject;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
private read;
|
|
8
|
+
private opportunity;
|
|
9
|
+
get answer(): import("@operato/layout").Entity360DoorAnswer<any> | undefined;
|
|
11
10
|
render(): import("lit-html").TemplateResult<1>;
|
|
12
11
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { __decorate, __metadata } from "tslib";
|
|
2
2
|
import gql from 'graphql-tag';
|
|
3
3
|
import { css, html, LitElement } from 'lit';
|
|
4
|
-
import { customElement, property
|
|
5
|
-
import {
|
|
4
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
5
|
+
import { QueryController } from '@operato/graphql';
|
|
6
6
|
import { i18next } from '@operato/i18n';
|
|
7
|
-
import {
|
|
7
|
+
import { entity360Answer, entity360Name, registerEntity360Kind, registerEntity360Section } from '@operato/layout';
|
|
8
8
|
import { documentAmount } from '@things-factory/invoice/dist-client';
|
|
9
9
|
import { opportunityStageWord } from '../opportunity-words';
|
|
10
10
|
/** An opportunity's 360 (ADR-0067). The module draws the opportunity; an application hangs its own sections on 'opportunity'. */
|
|
@@ -23,6 +23,17 @@ export function registerOpportunityKind() {
|
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
25
|
let OpportunityOverviewSection = class OpportunityOverviewSection extends LitElement {
|
|
26
|
+
constructor() {
|
|
27
|
+
super(...arguments);
|
|
28
|
+
this.opportunity = new QueryController(this, gql `
|
|
29
|
+
query ($id: String!) {
|
|
30
|
+
opportunity(id: $id) {
|
|
31
|
+
id name stage probability amount currency expectedCloseMonth closedOn lostReason description
|
|
32
|
+
counterparty { id name } endCustomer { id name } owner { id name }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
`, { variables: () => (this.subject?.id ? { id: this.subject.id } : undefined) });
|
|
36
|
+
}
|
|
26
37
|
static { this.styles = css `
|
|
27
38
|
:host {
|
|
28
39
|
font-size: var(--ui-t-body);
|
|
@@ -43,37 +54,8 @@ let OpportunityOverviewSection = class OpportunityOverviewSection extends LitEle
|
|
|
43
54
|
font-variant-numeric: tabular-nums;
|
|
44
55
|
}
|
|
45
56
|
`; }
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
this.read();
|
|
49
|
-
}
|
|
50
|
-
async read() {
|
|
51
|
-
const id = this.subject?.id;
|
|
52
|
-
this.answer = undefined;
|
|
53
|
-
if (!id)
|
|
54
|
-
return;
|
|
55
|
-
let response;
|
|
56
|
-
try {
|
|
57
|
-
response = await client.query({
|
|
58
|
-
query: gql `
|
|
59
|
-
query ($id: String!) {
|
|
60
|
-
opportunity(id: $id) {
|
|
61
|
-
id name stage probability amount currency expectedCloseMonth closedOn lostReason description
|
|
62
|
-
counterparty { id name } endCustomer { id name } owner { id name }
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
`,
|
|
66
|
-
variables: { id },
|
|
67
|
-
fetchPolicy: 'no-cache',
|
|
68
|
-
errorPolicy: 'all'
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
catch (e) {
|
|
72
|
-
response = { data: null, errors: e?.graphQLErrors?.length ? e.graphQLErrors : [{ message: e?.message ?? String(e) }] };
|
|
73
|
-
}
|
|
74
|
-
if (this.subject?.id !== id)
|
|
75
|
-
return;
|
|
76
|
-
this.answer = readEntity360Door(response, data => data?.opportunity);
|
|
57
|
+
get answer() {
|
|
58
|
+
return entity360Answer(this.opportunity, data => data?.opportunity);
|
|
77
59
|
}
|
|
78
60
|
render() {
|
|
79
61
|
const one = this.answer?.data;
|
|
@@ -104,10 +86,6 @@ __decorate([
|
|
|
104
86
|
property({ attribute: false }),
|
|
105
87
|
__metadata("design:type", Object)
|
|
106
88
|
], OpportunityOverviewSection.prototype, "subject", void 0);
|
|
107
|
-
__decorate([
|
|
108
|
-
state(),
|
|
109
|
-
__metadata("design:type", Object)
|
|
110
|
-
], OpportunityOverviewSection.prototype, "answer", void 0);
|
|
111
89
|
OpportunityOverviewSection = __decorate([
|
|
112
90
|
customElement('opportunity-overview-section')
|
|
113
91
|
], OpportunityOverviewSection);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"opportunity-360.js","sourceRoot":"","sources":["../../client/entity360/opportunity-360.ts"],"names":[],"mappings":";AAAA,OAAO,GAAG,MAAM,aAAa,CAAA;AAC7B,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,KAAK,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,
|
|
1
|
+
{"version":3,"file":"opportunity-360.js","sourceRoot":"","sources":["../../client/entity360/opportunity-360.ts"],"names":[],"mappings":"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","sourcesContent":["import gql from 'graphql-tag'\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport { QueryController } from '@operato/graphql'\nimport { i18next } from '@operato/i18n'\nimport { entity360Answer, entity360Name, registerEntity360Kind, registerEntity360Section, type Entity360Subject } from '@operato/layout'\nimport { documentAmount } from '@things-factory/invoice/dist-client'\n\nimport { opportunityStageWord } from '../opportunity-words'\n\n/** An opportunity's 360 (ADR-0067). The module draws the opportunity; an application hangs its own sections on 'opportunity'. */\nexport function registerOpportunityKind() {\n registerEntity360Kind({\n kind: 'opportunity',\n typenames: ['Opportunity'],\n assistantSubject: true,\n title: ({ value }) => `${i18next.t('title.opportunity')} · ${value.name || ''}`,\n context: ({ value }) => [value.counterparty?.name, value.stage ? opportunityStageWord(value.stage) : '', value.expectedCloseMonth].filter(Boolean).join(' · ')\n })\n registerEntity360Section('opportunity', {\n name: 'overview',\n order: 10,\n render: subject => html`<opportunity-overview-section .subject=${subject}></opportunity-overview-section>`\n })\n}\n\n@customElement('opportunity-overview-section')\nexport class OpportunityOverviewSection extends LitElement {\n static styles = css`\n :host {\n font-size: var(--ui-t-body);\n line-height: 1.5;\n }\n dl {\n margin: 0;\n display: grid;\n grid-template-columns: max-content 1fr;\n gap: var(--spacing-small) var(--spacing-large);\n }\n dt {\n color: var(--ui-muted);\n }\n dd {\n margin: 0;\n overflow-wrap: anywhere;\n font-variant-numeric: tabular-nums;\n }\n `\n\n @property({ attribute: false }) subject?: Entity360Subject\n private opportunity = new QueryController(\n this,\n gql`\n query ($id: String!) {\n opportunity(id: $id) {\n id name stage probability amount currency expectedCloseMonth closedOn lostReason description\n counterparty { id name } endCustomer { id name } owner { id name }\n }\n }\n `,\n { variables: () => (this.subject?.id ? { id: this.subject.id } : undefined) }\n )\n\n get answer() {\n return entity360Answer(this.opportunity, data => data?.opportunity)\n }\n\n render() {\n const one = this.answer?.data\n const party = (value?: { id: string; name: string }) => (value ? entity360Name({ __typename: 'Counterparty', ...value }, value.name) : '—')\n return html`<ox-entity360-section .heading=${i18next.t('title.opportunity')} .state=${this.answer?.state ?? 'loading'} .message=${this.answer?.message ?? ''}>\n ${one\n ? html`<dl>\n <dt>${i18next.t('field.opportunity-stage')}</dt>\n <dd>${opportunityStageWord(one.stage)} · ${one.probability}%</dd>\n <dt>${i18next.t('field.opportunity-counterparty')}</dt>\n <dd>${party(one.counterparty)}</dd>\n <dt>${i18next.t('field.opportunity-end-customer')}</dt>\n <dd>${party(one.endCustomer)}</dd>\n <dt>${i18next.t('field.opportunity-amount')}</dt>\n <dd>${documentAmount(one.amount, one.currency || 'KRW')}</dd>\n <dt>${i18next.t('field.opportunity-expected-close-month')}</dt>\n <dd>${one.expectedCloseMonth}</dd>\n <dt>${i18next.t('field.opportunity-owner')}</dt>\n <dd>${one.owner?.name ?? '—'}</dd>\n ${one.closedOn ? html`<dt>${i18next.t('field.opportunity-closed-on')}</dt><dd>${one.closedOn}</dd>` : ''}\n ${one.lostReason ? html`<dt>${i18next.t('field.opportunity-lost-reason')}</dt><dd>${one.lostReason}</dd>` : ''}\n </dl>`\n : ''}\n </ox-entity360-section>`\n }\n}\n"]}
|
|
@@ -9,18 +9,6 @@ type Picker = {
|
|
|
9
9
|
search: string;
|
|
10
10
|
found: Party[];
|
|
11
11
|
};
|
|
12
|
-
type Form = {
|
|
13
|
-
name: string;
|
|
14
|
-
counterparty: Party | null;
|
|
15
|
-
endCustomer: Party | null;
|
|
16
|
-
amount: string;
|
|
17
|
-
currency: string;
|
|
18
|
-
expectedCloseMonth: string;
|
|
19
|
-
stage: string;
|
|
20
|
-
probability: string;
|
|
21
|
-
ownerId: string;
|
|
22
|
-
description: string;
|
|
23
|
-
};
|
|
24
12
|
declare const OpportunityEditor_base: (new (...args: any[]) => import("lit").LitElement) & typeof PageView;
|
|
25
13
|
/**
|
|
26
14
|
* 영업 기회 편집 — with whom, for how much, by when and how likely.
|
|
@@ -31,26 +19,28 @@ declare const OpportunityEditor_base: (new (...args: any[]) => import("lit").Lit
|
|
|
31
19
|
*/
|
|
32
20
|
export declare class OpportunityEditor extends OpportunityEditor_base {
|
|
33
21
|
static styles: import("lit").CSSResult[];
|
|
34
|
-
opportunity
|
|
35
|
-
|
|
22
|
+
/** The opportunity the address names; none for a new one. */
|
|
23
|
+
private opportunityId;
|
|
36
24
|
pickers: Record<'counterparty' | 'endCustomer', Picker>;
|
|
37
|
-
stages: {
|
|
38
|
-
stage: string;
|
|
39
|
-
probability: number;
|
|
40
|
-
open: boolean;
|
|
41
|
-
}[];
|
|
42
|
-
owners: {
|
|
43
|
-
id: string;
|
|
44
|
-
name: string;
|
|
45
|
-
email?: string;
|
|
46
|
-
}[];
|
|
47
25
|
/** Whether the application's section is this person's to read - asked once, before it is shown. */
|
|
48
26
|
sectionReadable: boolean;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
27
|
+
private choices;
|
|
28
|
+
private record;
|
|
29
|
+
private draft;
|
|
30
|
+
private loss;
|
|
31
|
+
private saves;
|
|
32
|
+
private change;
|
|
33
|
+
private get opportunity();
|
|
34
|
+
private get events();
|
|
35
|
+
private get stages();
|
|
36
|
+
private get owners();
|
|
37
|
+
/** The form as it shows: each field's edit, else what was read (blank while it is read). */
|
|
38
|
+
private get form();
|
|
39
|
+
private get reason();
|
|
40
|
+
/** A failed read, or an opportunity the address names that is not there. */
|
|
41
|
+
private get error();
|
|
42
|
+
private get loading();
|
|
43
|
+
private get busy();
|
|
54
44
|
private isOpen;
|
|
55
45
|
/** Won is final; a lost one is changed only by being opened again. */
|
|
56
46
|
get editable(): boolean;
|
|
@@ -69,16 +59,18 @@ export declare class OpportunityEditor extends OpportunityEditor_base {
|
|
|
69
59
|
/** The quotations that answer this opportunity; a new one is written from here and answers it. */
|
|
70
60
|
private renderQuotations;
|
|
71
61
|
private renderEvents;
|
|
62
|
+
private edit;
|
|
72
63
|
private setPicker;
|
|
73
64
|
private chooseParty;
|
|
74
65
|
/** The stage's own probability comes in with it; the person may change it afterwards. */
|
|
75
66
|
private chooseStage;
|
|
76
|
-
|
|
67
|
+
/**
|
|
68
|
+
* The counterparties the words find - a look-up on asking, not a read the page keeps: one match is taken at once,
|
|
69
|
+
* more are listed to choose from. A failure is told by the GraphQL link.
|
|
70
|
+
*/
|
|
77
71
|
private findParties;
|
|
78
|
-
private load;
|
|
79
|
-
private show;
|
|
80
|
-
private run;
|
|
81
72
|
private input;
|
|
73
|
+
/** A new one is opened once it is made; the edits sent are cleared once the save answered. */
|
|
82
74
|
private save;
|
|
83
75
|
private lose;
|
|
84
76
|
}
|
|
@@ -3,10 +3,9 @@ import '@operato/list-state/ox-page-state.js';
|
|
|
3
3
|
import gql from 'graphql-tag';
|
|
4
4
|
import { css, html, nothing } from 'lit';
|
|
5
5
|
import { customElement, state } from 'lit/decorators.js';
|
|
6
|
-
import { client } from '@operato/graphql';
|
|
6
|
+
import { client, MutationController, QueryController } from '@operato/graphql';
|
|
7
7
|
import { i18next, localize } from '@operato/i18n';
|
|
8
|
-
import {
|
|
9
|
-
import { navigate, PageView } from '@operato/shell';
|
|
8
|
+
import { navigate, PageView, DraftController } from '@operato/shell';
|
|
10
9
|
import { ButtonStyles, DocumentPageStyles, FormFieldStyles } from '@operato/styles';
|
|
11
10
|
import { currencyChoices, documentAmount, DocumentFormStyles } from '@things-factory/invoice/dist-client';
|
|
12
11
|
import { opportunitySection } from '../opportunity-attach';
|
|
@@ -17,6 +16,12 @@ const FIELDS = `
|
|
|
17
16
|
counterpartyId endCustomerId ownerId counterparty { id name } endCustomer { id name } owner { id name }
|
|
18
17
|
quotations { id number version status issueDate validUntil totalAmount currency }
|
|
19
18
|
`;
|
|
19
|
+
const READ = gql(`
|
|
20
|
+
query ($id: String!) {
|
|
21
|
+
opportunity(id: $id) { ${FIELDS} }
|
|
22
|
+
opportunityEvents(opportunityId: $id) { id action fromStage stage probability reason createdAt actor { id name } }
|
|
23
|
+
}
|
|
24
|
+
`);
|
|
20
25
|
const thisMonth = () => new Date().toLocaleDateString('en-CA').slice(0, 7);
|
|
21
26
|
/**
|
|
22
27
|
* 영업 기회 편집 — with whom, for how much, by when and how likely.
|
|
@@ -28,18 +33,29 @@ const thisMonth = () => new Date().toLocaleDateString('en-CA').slice(0, 7);
|
|
|
28
33
|
let OpportunityEditor = class OpportunityEditor extends localize(i18next)(PageView) {
|
|
29
34
|
constructor() {
|
|
30
35
|
super(...arguments);
|
|
31
|
-
|
|
32
|
-
this.
|
|
36
|
+
/** The opportunity the address names; none for a new one. */
|
|
37
|
+
this.opportunityId = '';
|
|
33
38
|
this.pickers = { counterparty: { search: '', found: [] }, endCustomer: { search: '', found: [] } };
|
|
34
|
-
this.stages = [];
|
|
35
|
-
this.owners = [];
|
|
36
39
|
/** Whether the application's section is this person's to read - asked once, before it is shown. */
|
|
37
40
|
this.sectionReadable = false;
|
|
38
|
-
|
|
39
|
-
this.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
/* The stages with their probabilities and who may own one, and the opportunity the address names (ADR-0190). */
|
|
42
|
+
this.choices = new QueryController(this, gql `
|
|
43
|
+
query {
|
|
44
|
+
opportunityStages { stage probability open }
|
|
45
|
+
opportunityOwners { id name email }
|
|
46
|
+
}
|
|
47
|
+
`);
|
|
48
|
+
this.record = new QueryController(this, READ, { variables: () => (this.opportunityId ? { id: this.opportunityId } : undefined) });
|
|
49
|
+
/* What is typed over the opportunity read - a new one starts blank, at the first stage. */
|
|
50
|
+
this.draft = new DraftController(this, { read: () => (this.opportunityId ? this.opportunity && formOf(this.opportunity) : blank(this.stages)), key: () => this.opportunityId });
|
|
51
|
+
/* The reason a lost one is closed with. */
|
|
52
|
+
this.loss = new DraftController(this, { read: () => ({ reason: '' }), key: () => this.opportunityId });
|
|
53
|
+
/* Every change reads the opportunity again once it is done. */
|
|
54
|
+
this.saves = {
|
|
55
|
+
create: new MutationController(this, gql `mutation ($opportunity: NewOpportunity!) { opportunity: createOpportunity(opportunity: $opportunity) { id } }`),
|
|
56
|
+
update: this.change(gql `mutation ($id: String!, $opportunity: NewOpportunity!) { opportunity: updateOpportunity(id: $id, opportunity: $opportunity) { id } }`),
|
|
57
|
+
lose: this.change(gql `mutation ($id: String!, $reason: String!) { loseOpportunity(id: $id, reason: $reason) { id } }`)
|
|
58
|
+
};
|
|
43
59
|
}
|
|
44
60
|
static { this.styles = [
|
|
45
61
|
FormFieldStyles,
|
|
@@ -77,6 +93,40 @@ let OpportunityEditor = class OpportunityEditor extends localize(i18next)(PageVi
|
|
|
77
93
|
}
|
|
78
94
|
`
|
|
79
95
|
]; }
|
|
96
|
+
change(mutation) {
|
|
97
|
+
return new MutationController(this, mutation, { refetch: () => [this.record] });
|
|
98
|
+
}
|
|
99
|
+
get opportunity() {
|
|
100
|
+
return (this.opportunityId && this.record.data?.opportunity) || null;
|
|
101
|
+
}
|
|
102
|
+
get events() {
|
|
103
|
+
return (this.opportunityId && this.record.data?.opportunityEvents) || [];
|
|
104
|
+
}
|
|
105
|
+
get stages() {
|
|
106
|
+
return this.choices.data?.opportunityStages ?? [];
|
|
107
|
+
}
|
|
108
|
+
get owners() {
|
|
109
|
+
return this.choices.data?.opportunityOwners ?? [];
|
|
110
|
+
}
|
|
111
|
+
/** The form as it shows: each field's edit, else what was read (blank while it is read). */
|
|
112
|
+
get form() {
|
|
113
|
+
return Object.fromEntries(Object.entries(blank(this.stages)).map(([name, given]) => [name, this.draft.value(name) ?? given]));
|
|
114
|
+
}
|
|
115
|
+
get reason() {
|
|
116
|
+
return this.loss.value('reason') ?? '';
|
|
117
|
+
}
|
|
118
|
+
/** A failed read, or an opportunity the address names that is not there. */
|
|
119
|
+
get error() {
|
|
120
|
+
if (this.choices.error || this.record.error)
|
|
121
|
+
return this.choices.error ?? this.record.error;
|
|
122
|
+
return this.opportunityId && this.record.data && !this.record.data.opportunity ? new Error(i18next.t('text.opportunity-not-found')) : null;
|
|
123
|
+
}
|
|
124
|
+
get loading() {
|
|
125
|
+
return this.choices.loading || this.record.loading;
|
|
126
|
+
}
|
|
127
|
+
get busy() {
|
|
128
|
+
return Object.values(this.saves).some(save => save.pending);
|
|
129
|
+
}
|
|
80
130
|
isOpen(stage) {
|
|
81
131
|
return this.stages.some(one => one.stage === stage && one.open);
|
|
82
132
|
}
|
|
@@ -100,28 +150,17 @@ let OpportunityEditor = class OpportunityEditor extends localize(i18next)(PageVi
|
|
|
100
150
|
async pageUpdated(changes, lifecycle) {
|
|
101
151
|
if (!this.active || !(changes.active || changes.params))
|
|
102
152
|
return;
|
|
103
|
-
if (!this.stages.length)
|
|
104
|
-
await this.readChoices();
|
|
105
153
|
const section = opportunitySection();
|
|
106
154
|
if (section && !this.sectionReadable)
|
|
107
155
|
(section.readable?.() ?? Promise.resolve(true)).then(may => (this.sectionReadable = !!may));
|
|
108
|
-
|
|
109
|
-
if (!id) {
|
|
110
|
-
this.opportunity = null;
|
|
111
|
-
this.form = blank(this.stages);
|
|
112
|
-
this.events = [];
|
|
113
|
-
this.updateContext();
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
if (id !== this.opportunity?.id)
|
|
117
|
-
await this.load(id);
|
|
156
|
+
this.opportunityId = lifecycle?.params?.id ?? '';
|
|
118
157
|
}
|
|
119
158
|
render() {
|
|
120
159
|
const attached = this.opportunity && this.sectionReadable ? opportunitySection() : undefined;
|
|
121
160
|
const editable = this.editable;
|
|
122
|
-
const set = (patch) =>
|
|
161
|
+
const set = (patch) => this.edit(patch);
|
|
123
162
|
const stageChoices = this.stages.filter(one => one.open || one.stage === this.opportunity?.stage);
|
|
124
|
-
return html `<ox-page-state .loading=${this.loading} .error=${this.error} @retry=${() => this.
|
|
163
|
+
return html `<ox-page-state .loading=${this.loading} .error=${this.error} @retry=${() => (this.choices.refetch(), this.record.refetch())}>
|
|
125
164
|
<div class="page">
|
|
126
165
|
<section class="page-section">
|
|
127
166
|
<header class="page-section-header"><h3>${i18next.t('text.opportunity-section-head')}</h3></header>
|
|
@@ -186,7 +225,7 @@ let OpportunityEditor = class OpportunityEditor extends localize(i18next)(PageVi
|
|
|
186
225
|
: nothing}
|
|
187
226
|
${this.opportunity ? this.renderQuotations() : nothing}
|
|
188
227
|
${attached && this.opportunity
|
|
189
|
-
? html `<section class="page-section" @opportunity-changed=${() => this.
|
|
228
|
+
? html `<section class="page-section" @opportunity-changed=${() => this.record.refetch()}>
|
|
190
229
|
<header class="page-section-header"><h3>${attached.heading()}</h3></header>
|
|
191
230
|
${attached.render(this.opportunity)}
|
|
192
231
|
</section>`
|
|
@@ -210,7 +249,7 @@ let OpportunityEditor = class OpportunityEditor extends localize(i18next)(PageVi
|
|
|
210
249
|
@keydown=${(e) => e.key === 'Enter' && this.findParties(which)}
|
|
211
250
|
/>
|
|
212
251
|
<button ?disabled=${!editable} @click=${() => this.findParties(which)}>${i18next.t('button.search')}</button>
|
|
213
|
-
${which === 'endCustomer' && chosen ? html `<button ?disabled=${!editable} @click=${() =>
|
|
252
|
+
${which === 'endCustomer' && chosen ? html `<button ?disabled=${!editable} @click=${() => this.edit({ endCustomer: null })}>${i18next.t('button.opportunity-clear')}</button>` : nothing}
|
|
214
253
|
</span>
|
|
215
254
|
${picker.found.length
|
|
216
255
|
? html `<select ?disabled=${!editable} @change=${(e) => this.chooseParty(which, picker.found.find(one => one.id === e.target.value) ?? null)}>
|
|
@@ -231,7 +270,7 @@ let OpportunityEditor = class OpportunityEditor extends localize(i18next)(PageVi
|
|
|
231
270
|
<div class="form">
|
|
232
271
|
<label class="wide"
|
|
233
272
|
>${i18next.t('field.opportunity-lost-reason')}
|
|
234
|
-
<input .value=${this.reason} ?disabled=${this.busy} @input=${(e) =>
|
|
273
|
+
<input .value=${this.reason} ?disabled=${this.busy} @input=${(e) => this.loss.set('reason', e.target.value)}
|
|
235
274
|
/></label>
|
|
236
275
|
</div>
|
|
237
276
|
</section>`;
|
|
@@ -297,42 +336,30 @@ let OpportunityEditor = class OpportunityEditor extends localize(i18next)(PageVi
|
|
|
297
336
|
</table>
|
|
298
337
|
</section>`;
|
|
299
338
|
}
|
|
339
|
+
edit(patch) {
|
|
340
|
+
for (const [name, value] of Object.entries(patch))
|
|
341
|
+
this.draft.set(name, value);
|
|
342
|
+
}
|
|
300
343
|
setPicker(which, patch) {
|
|
301
344
|
this.pickers = { ...this.pickers, [which]: { ...this.pickers[which], ...patch } };
|
|
302
345
|
}
|
|
303
346
|
chooseParty(which, party) {
|
|
304
347
|
if (!party)
|
|
305
348
|
return;
|
|
306
|
-
this.
|
|
349
|
+
this.edit({ [which]: party });
|
|
307
350
|
this.setPicker(which, { search: '', found: [] });
|
|
308
351
|
}
|
|
309
352
|
/** The stage's own probability comes in with it; the person may change it afterwards. */
|
|
310
353
|
chooseStage(stage) {
|
|
311
354
|
const given = this.stages.find(one => one.stage === stage);
|
|
312
|
-
this.
|
|
313
|
-
}
|
|
314
|
-
async readChoices() {
|
|
315
|
-
try {
|
|
316
|
-
const response = await client.query({
|
|
317
|
-
query: gql `
|
|
318
|
-
query {
|
|
319
|
-
opportunityStages { stage probability open }
|
|
320
|
-
opportunityOwners { id name email }
|
|
321
|
-
}
|
|
322
|
-
`,
|
|
323
|
-
fetchPolicy: 'no-cache'
|
|
324
|
-
});
|
|
325
|
-
if (response.errors?.length)
|
|
326
|
-
throw response;
|
|
327
|
-
this.stages = response.data.opportunityStages;
|
|
328
|
-
this.owners = response.data.opportunityOwners;
|
|
329
|
-
}
|
|
330
|
-
catch (error) {
|
|
331
|
-
this.error = error;
|
|
332
|
-
}
|
|
355
|
+
this.edit({ stage, probability: given ? String(given.probability) : this.form.probability });
|
|
333
356
|
}
|
|
357
|
+
/**
|
|
358
|
+
* The counterparties the words find - a look-up on asking, not a read the page keeps: one match is taken at once,
|
|
359
|
+
* more are listed to choose from. A failure is told by the GraphQL link.
|
|
360
|
+
*/
|
|
334
361
|
async findParties(which) {
|
|
335
|
-
|
|
362
|
+
try {
|
|
336
363
|
const response = await client.query({
|
|
337
364
|
query: gql `
|
|
338
365
|
query ($search: String, $pagination: Pagination) {
|
|
@@ -344,68 +371,14 @@ let OpportunityEditor = class OpportunityEditor extends localize(i18next)(PageVi
|
|
|
344
371
|
variables: { search: this.pickers[which].search.trim() || undefined, pagination: { page: 1, limit: 30 } },
|
|
345
372
|
fetchPolicy: 'no-cache'
|
|
346
373
|
});
|
|
347
|
-
|
|
348
|
-
throw response;
|
|
349
|
-
const found = response.data.counterparties.items ?? [];
|
|
374
|
+
const found = response.data?.counterparties?.items ?? [];
|
|
350
375
|
if (found.length === 1)
|
|
351
376
|
this.chooseParty(which, found[0]);
|
|
352
377
|
else
|
|
353
378
|
this.setPicker(which, { found });
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
async load(id) {
|
|
357
|
-
this.loading = true;
|
|
358
|
-
this.error = null;
|
|
359
|
-
try {
|
|
360
|
-
const response = await client.query({
|
|
361
|
-
query: gql(`query ($id: String!) { opportunity(id: $id) { ${FIELDS} } opportunityEvents(opportunityId: $id) { id action fromStage stage probability reason createdAt actor { id name } } }`),
|
|
362
|
-
variables: { id },
|
|
363
|
-
fetchPolicy: 'no-cache'
|
|
364
|
-
});
|
|
365
|
-
if (response.errors?.length)
|
|
366
|
-
throw response;
|
|
367
|
-
if (!response.data.opportunity)
|
|
368
|
-
throw new Error(i18next.t('text.opportunity-not-found'));
|
|
369
|
-
this.show(response.data.opportunity, response.data.opportunityEvents);
|
|
370
|
-
}
|
|
371
|
-
catch (error) {
|
|
372
|
-
this.error = error;
|
|
373
379
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
this.updateContext();
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
show(opportunity, events) {
|
|
380
|
-
this.opportunity = opportunity;
|
|
381
|
-
this.events = events;
|
|
382
|
-
this.form = {
|
|
383
|
-
name: opportunity.name,
|
|
384
|
-
counterparty: opportunity.counterparty,
|
|
385
|
-
endCustomer: opportunity.endCustomer,
|
|
386
|
-
amount: String(opportunity.amount ?? 0),
|
|
387
|
-
currency: opportunity.currency,
|
|
388
|
-
expectedCloseMonth: opportunity.expectedCloseMonth,
|
|
389
|
-
stage: opportunity.stage,
|
|
390
|
-
probability: String(opportunity.probability),
|
|
391
|
-
ownerId: opportunity.ownerId ?? '',
|
|
392
|
-
description: opportunity.description ?? ''
|
|
393
|
-
};
|
|
394
|
-
this.reason = '';
|
|
395
|
-
}
|
|
396
|
-
async run(work) {
|
|
397
|
-
if (this.busy)
|
|
398
|
-
return undefined;
|
|
399
|
-
this.busy = true;
|
|
400
|
-
try {
|
|
401
|
-
return await work();
|
|
402
|
-
}
|
|
403
|
-
catch (error) {
|
|
404
|
-
notify({ level: 'error', message: error?.graphQLErrors?.[0]?.message ?? error?.errors?.[0]?.message ?? error?.message ?? String(error) });
|
|
405
|
-
return undefined;
|
|
406
|
-
}
|
|
407
|
-
finally {
|
|
408
|
-
this.busy = false;
|
|
380
|
+
catch {
|
|
381
|
+
/* Told by the GraphQL link. */
|
|
409
382
|
}
|
|
410
383
|
}
|
|
411
384
|
input() {
|
|
@@ -423,89 +396,34 @@ let OpportunityEditor = class OpportunityEditor extends localize(i18next)(PageVi
|
|
|
423
396
|
description
|
|
424
397
|
};
|
|
425
398
|
}
|
|
399
|
+
/** A new one is opened once it is made; the edits sent are cleared once the save answered. */
|
|
426
400
|
async save() {
|
|
427
|
-
|
|
428
|
-
const saved = await this.run(async () => {
|
|
429
|
-
const response = await client.mutate({
|
|
430
|
-
mutation: gql(id
|
|
431
|
-
? `mutation ($id: String!, $opportunity: NewOpportunity!) { opportunity: updateOpportunity(id: $id, opportunity: $opportunity) { id } }`
|
|
432
|
-
: `mutation ($opportunity: NewOpportunity!) { opportunity: createOpportunity(opportunity: $opportunity) { id } }`),
|
|
433
|
-
variables: { ...(id ? { id } : {}), opportunity: this.input() }
|
|
434
|
-
});
|
|
435
|
-
if (response.errors?.length)
|
|
436
|
-
throw response;
|
|
437
|
-
return response.data.opportunity;
|
|
438
|
-
});
|
|
439
|
-
if (!saved)
|
|
401
|
+
if (this.busy)
|
|
440
402
|
return;
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
403
|
+
const id = this.opportunityId;
|
|
404
|
+
const saved = await this.draft.submit(() => id ? this.saves.update.run({ id, opportunity: this.input() }) : this.saves.create.run({ opportunity: this.input() }));
|
|
405
|
+
if (saved && !id)
|
|
406
|
+
navigate(`opportunity-editor?id=${saved.opportunity.id}`);
|
|
445
407
|
}
|
|
446
408
|
async lose() {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
mutation ($id: String!, $reason: String!) {
|
|
452
|
-
loseOpportunity(id: $id, reason: $reason) { id }
|
|
453
|
-
}
|
|
454
|
-
`,
|
|
455
|
-
variables: { id, reason: this.reason.trim() }
|
|
456
|
-
});
|
|
457
|
-
if (response.errors?.length)
|
|
458
|
-
throw response;
|
|
459
|
-
return response.data.loseOpportunity;
|
|
460
|
-
});
|
|
461
|
-
if (lost)
|
|
462
|
-
await this.load(id);
|
|
409
|
+
if (this.busy)
|
|
410
|
+
return;
|
|
411
|
+
const id = this.opportunityId;
|
|
412
|
+
await this.loss.submit(sent => this.saves.lose.run({ id, reason: (sent.reason ?? '').trim() }));
|
|
463
413
|
}
|
|
464
414
|
};
|
|
465
415
|
__decorate([
|
|
466
416
|
state(),
|
|
467
417
|
__metadata("design:type", Object)
|
|
468
|
-
], OpportunityEditor.prototype, "
|
|
469
|
-
__decorate([
|
|
470
|
-
state(),
|
|
471
|
-
__metadata("design:type", Object)
|
|
472
|
-
], OpportunityEditor.prototype, "form", void 0);
|
|
418
|
+
], OpportunityEditor.prototype, "opportunityId", void 0);
|
|
473
419
|
__decorate([
|
|
474
420
|
state(),
|
|
475
421
|
__metadata("design:type", Object)
|
|
476
422
|
], OpportunityEditor.prototype, "pickers", void 0);
|
|
477
|
-
__decorate([
|
|
478
|
-
state(),
|
|
479
|
-
__metadata("design:type", Array)
|
|
480
|
-
], OpportunityEditor.prototype, "stages", void 0);
|
|
481
|
-
__decorate([
|
|
482
|
-
state(),
|
|
483
|
-
__metadata("design:type", Array)
|
|
484
|
-
], OpportunityEditor.prototype, "owners", void 0);
|
|
485
423
|
__decorate([
|
|
486
424
|
state(),
|
|
487
425
|
__metadata("design:type", Object)
|
|
488
426
|
], OpportunityEditor.prototype, "sectionReadable", void 0);
|
|
489
|
-
__decorate([
|
|
490
|
-
state(),
|
|
491
|
-
__metadata("design:type", Array)
|
|
492
|
-
], OpportunityEditor.prototype, "events", void 0);
|
|
493
|
-
__decorate([
|
|
494
|
-
state(),
|
|
495
|
-
__metadata("design:type", Object)
|
|
496
|
-
], OpportunityEditor.prototype, "reason", void 0);
|
|
497
|
-
__decorate([
|
|
498
|
-
state(),
|
|
499
|
-
__metadata("design:type", Object)
|
|
500
|
-
], OpportunityEditor.prototype, "loading", void 0);
|
|
501
|
-
__decorate([
|
|
502
|
-
state(),
|
|
503
|
-
__metadata("design:type", Object)
|
|
504
|
-
], OpportunityEditor.prototype, "error", void 0);
|
|
505
|
-
__decorate([
|
|
506
|
-
state(),
|
|
507
|
-
__metadata("design:type", Object)
|
|
508
|
-
], OpportunityEditor.prototype, "busy", void 0);
|
|
509
427
|
OpportunityEditor = __decorate([
|
|
510
428
|
customElement('opportunity-editor')
|
|
511
429
|
], OpportunityEditor);
|
|
@@ -521,6 +439,21 @@ const eventWord = (action) => action === 'CREATED'
|
|
|
521
439
|
: action === 'REOPENED'
|
|
522
440
|
? i18next.t('text.opportunity-event-reopened')
|
|
523
441
|
: i18next.t('text.opportunity-event-updated');
|
|
442
|
+
/** The form of an opportunity as read. */
|
|
443
|
+
function formOf(opportunity) {
|
|
444
|
+
return {
|
|
445
|
+
name: opportunity.name,
|
|
446
|
+
counterparty: opportunity.counterparty,
|
|
447
|
+
endCustomer: opportunity.endCustomer,
|
|
448
|
+
amount: String(opportunity.amount ?? 0),
|
|
449
|
+
currency: opportunity.currency,
|
|
450
|
+
expectedCloseMonth: opportunity.expectedCloseMonth,
|
|
451
|
+
stage: opportunity.stage,
|
|
452
|
+
probability: String(opportunity.probability),
|
|
453
|
+
ownerId: opportunity.ownerId ?? '',
|
|
454
|
+
description: opportunity.description ?? ''
|
|
455
|
+
};
|
|
456
|
+
}
|
|
524
457
|
function blank(stages = []) {
|
|
525
458
|
const lead = stages.find(one => one.stage === 'LEAD');
|
|
526
459
|
return {
|