@things-factory/sales 10.1.256 → 10.1.258

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.
@@ -1,12 +1,11 @@
1
1
  import { LitElement } from 'lit';
2
- import { type Entity360DoorAnswer, type Entity360Subject } from '@operato/layout';
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
- answer?: Entity360DoorAnswer<any>;
9
- updated(changed: Map<string, unknown>): void;
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, state } from 'lit/decorators.js';
5
- import { client } from '@operato/graphql';
4
+ import { customElement, property } from 'lit/decorators.js';
5
+ import { QueryController } from '@operato/graphql';
6
6
  import { i18next } from '@operato/i18n';
7
- import { entity360Name, readEntity360Door, registerEntity360Kind, registerEntity360Section } from '@operato/layout';
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
- updated(changed) {
47
- if (changed.has('subject'))
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":"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","sourcesContent":["import gql from 'graphql-tag'\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { client } from '@operato/graphql'\nimport { i18next } from '@operato/i18n'\nimport { entity360Name, readEntity360Door, registerEntity360Kind, registerEntity360Section, type Entity360DoorAnswer, 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 @state() answer?: Entity360DoorAnswer<any>\n\n updated(changed: Map<string, unknown>) {\n if (changed.has('subject')) this.read()\n }\n\n private async read() {\n const id = this.subject?.id\n this.answer = undefined\n if (!id) return\n let response: any\n try {\n response = await client.query({\n query: 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: { id },\n fetchPolicy: 'no-cache',\n errorPolicy: 'all'\n } as any)\n } catch (e: any) {\n response = { data: null, errors: e?.graphQLErrors?.length ? e.graphQLErrors : [{ message: e?.message ?? String(e) }] }\n }\n if (this.subject?.id !== id) return\n this.answer = readEntity360Door(response, 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"]}
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: any;
35
- form: Form;
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
- events: any[];
50
- reason: string;
51
- loading: boolean;
52
- error: unknown;
53
- busy: boolean;
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
- private readChoices;
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 { notify } from '@operato/layout';
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
- this.opportunity = null;
32
- this.form = blank();
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
- this.events = [];
39
- this.reason = '';
40
- this.loading = false;
41
- this.error = null;
42
- this.busy = false;
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
- const id = lifecycle?.params?.id;
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) => (this.form = { ...this.form, ...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.opportunity && this.load(this.opportunity.id)}>
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.load(this.opportunity.id)}>
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=${() => (this.form = { ...this.form, endCustomer: null })}>${i18next.t('button.opportunity-clear')}</button>` : nothing}
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) => (this.reason = e.target.value)}
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.form = { ...this.form, [which]: party };
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.form = { ...this.form, stage, probability: given ? String(given.probability) : this.form.probability };
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
- await this.run(async () => {
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
- if (response.errors?.length)
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
- finally {
375
- this.loading = false;
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
- const id = this.opportunity?.id;
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
- if (!id)
442
- navigate(`opportunity-editor?id=${saved.id}`);
443
- else
444
- await this.load(id);
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
- const id = this.opportunity.id;
448
- const lost = await this.run(async () => {
449
- const response = await client.mutate({
450
- mutation: gql `
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, "opportunity", void 0);
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 {