@things-factory/sales 0.0.0-stage → 10.1.176

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.
Files changed (131) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/assets/images/hatiolab-logo.png +0 -0
  3. package/dist-client/bootstrap.d.ts +1 -0
  4. package/dist-client/bootstrap.js +5 -0
  5. package/dist-client/bootstrap.js.map +1 -0
  6. package/dist-client/entity360/opportunity-360.d.ts +12 -0
  7. package/dist-client/entity360/opportunity-360.js +114 -0
  8. package/dist-client/entity360/opportunity-360.js.map +1 -0
  9. package/dist-client/index.d.ts +4 -0
  10. package/dist-client/index.js +5 -0
  11. package/dist-client/index.js.map +1 -0
  12. package/dist-client/opportunity-attach.d.ts +17 -0
  13. package/dist-client/opportunity-attach.js +8 -0
  14. package/dist-client/opportunity-attach.js.map +1 -0
  15. package/dist-client/opportunity-words.d.ts +1 -0
  16. package/dist-client/opportunity-words.js +13 -0
  17. package/dist-client/opportunity-words.js.map +1 -0
  18. package/dist-client/pages/opportunity-editor.d.ts +81 -0
  19. package/dist-client/pages/opportunity-editor.js +482 -0
  20. package/dist-client/pages/opportunity-editor.js.map +1 -0
  21. package/dist-client/pages/opportunity-list.d.ts +40 -0
  22. package/dist-client/pages/opportunity-list.js +134 -0
  23. package/dist-client/pages/opportunity-list.js.map +1 -0
  24. package/dist-client/pages/quotation-editor.d.ts +44 -0
  25. package/dist-client/pages/quotation-editor.js +315 -0
  26. package/dist-client/pages/quotation-editor.js.map +1 -0
  27. package/dist-client/pages/quotation-list.d.ts +36 -0
  28. package/dist-client/pages/quotation-list.js +113 -0
  29. package/dist-client/pages/quotation-list.js.map +1 -0
  30. package/dist-client/quotation-attach.d.ts +16 -0
  31. package/dist-client/quotation-attach.js +8 -0
  32. package/dist-client/quotation-attach.js.map +1 -0
  33. package/dist-client/quotation-words.d.ts +1 -0
  34. package/dist-client/quotation-words.js +13 -0
  35. package/dist-client/quotation-words.js.map +1 -0
  36. package/dist-client/route.d.ts +1 -0
  37. package/dist-client/route.js +17 -0
  38. package/dist-client/route.js.map +1 -0
  39. package/dist-client/tsconfig.tsbuildinfo +1 -0
  40. package/dist-server/controllers/index.d.ts +0 -0
  41. package/dist-server/controllers/index.js +1 -0
  42. package/dist-server/controllers/index.js.map +1 -0
  43. package/dist-server/index.d.ts +4 -0
  44. package/dist-server/index.js +8 -0
  45. package/dist-server/index.js.map +1 -0
  46. package/dist-server/middlewares/index.d.ts +2 -0
  47. package/dist-server/middlewares/index.js +7 -0
  48. package/dist-server/middlewares/index.js.map +1 -0
  49. package/dist-server/migrations/index.d.ts +1 -0
  50. package/dist-server/migrations/index.js +12 -0
  51. package/dist-server/migrations/index.js.map +1 -0
  52. package/dist-server/routes.d.ts +0 -0
  53. package/dist-server/routes.js +24 -0
  54. package/dist-server/routes.js.map +1 -0
  55. package/dist-server/service/index.d.ts +16 -0
  56. package/dist-server/service/index.js +21 -0
  57. package/dist-server/service/index.js.map +1 -0
  58. package/dist-server/service/opportunity/index.d.ts +8 -0
  59. package/dist-server/service/opportunity/index.js +12 -0
  60. package/dist-server/service/opportunity/index.js.map +1 -0
  61. package/dist-server/service/opportunity/opportunity-event-query.d.ts +9 -0
  62. package/dist-server/service/opportunity/opportunity-event-query.js +55 -0
  63. package/dist-server/service/opportunity/opportunity-event-query.js.map +1 -0
  64. package/dist-server/service/opportunity/opportunity-event.d.ts +27 -0
  65. package/dist-server/service/opportunity/opportunity-event.js +115 -0
  66. package/dist-server/service/opportunity/opportunity-event.js.map +1 -0
  67. package/dist-server/service/opportunity/opportunity-mutation.d.ts +7 -0
  68. package/dist-server/service/opportunity/opportunity-mutation.js +64 -0
  69. package/dist-server/service/opportunity/opportunity-mutation.js.map +1 -0
  70. package/dist-server/service/opportunity/opportunity-query.d.ts +19 -0
  71. package/dist-server/service/opportunity/opportunity-query.js +144 -0
  72. package/dist-server/service/opportunity/opportunity-query.js.map +1 -0
  73. package/dist-server/service/opportunity/opportunity-rules.d.ts +22 -0
  74. package/dist-server/service/opportunity/opportunity-rules.js +59 -0
  75. package/dist-server/service/opportunity/opportunity-rules.js.map +1 -0
  76. package/dist-server/service/opportunity/opportunity-service.d.ts +19 -0
  77. package/dist-server/service/opportunity/opportunity-service.js +107 -0
  78. package/dist-server/service/opportunity/opportunity-service.js.map +1 -0
  79. package/dist-server/service/opportunity/opportunity-type.d.ts +23 -0
  80. package/dist-server/service/opportunity/opportunity-type.js +86 -0
  81. package/dist-server/service/opportunity/opportunity-type.js.map +1 -0
  82. package/dist-server/service/opportunity/opportunity.d.ts +35 -0
  83. package/dist-server/service/opportunity/opportunity.js +147 -0
  84. package/dist-server/service/opportunity/opportunity.js.map +1 -0
  85. package/dist-server/service/quotation/index.d.ts +10 -0
  86. package/dist-server/service/quotation/index.js +14 -0
  87. package/dist-server/service/quotation/index.js.map +1 -0
  88. package/dist-server/service/quotation/quotation-event-query.d.ts +9 -0
  89. package/dist-server/service/quotation/quotation-event-query.js +55 -0
  90. package/dist-server/service/quotation/quotation-event-query.js.map +1 -0
  91. package/dist-server/service/quotation/quotation-event.d.ts +22 -0
  92. package/dist-server/service/quotation/quotation-event.js +98 -0
  93. package/dist-server/service/quotation/quotation-event.js.map +1 -0
  94. package/dist-server/service/quotation/quotation-item-query.d.ts +8 -0
  95. package/dist-server/service/quotation/quotation-item-query.js +45 -0
  96. package/dist-server/service/quotation/quotation-item-query.js.map +1 -0
  97. package/dist-server/service/quotation/quotation-item.d.ts +18 -0
  98. package/dist-server/service/quotation/quotation-item.js +75 -0
  99. package/dist-server/service/quotation/quotation-item.js.map +1 -0
  100. package/dist-server/service/quotation/quotation-mutation.d.ts +11 -0
  101. package/dist-server/service/quotation/quotation-mutation.js +117 -0
  102. package/dist-server/service/quotation/quotation-mutation.js.map +1 -0
  103. package/dist-server/service/quotation/quotation-query.d.ts +17 -0
  104. package/dist-server/service/quotation/quotation-query.js +125 -0
  105. package/dist-server/service/quotation/quotation-query.js.map +1 -0
  106. package/dist-server/service/quotation/quotation-rules.d.ts +6 -0
  107. package/dist-server/service/quotation/quotation-rules.js +22 -0
  108. package/dist-server/service/quotation/quotation-rules.js.map +1 -0
  109. package/dist-server/service/quotation/quotation-service.d.ts +23 -0
  110. package/dist-server/service/quotation/quotation-service.js +115 -0
  111. package/dist-server/service/quotation/quotation-service.js.map +1 -0
  112. package/dist-server/service/quotation/quotation-type.d.ts +9 -0
  113. package/dist-server/service/quotation/quotation-type.js +32 -0
  114. package/dist-server/service/quotation/quotation-type.js.map +1 -0
  115. package/dist-server/service/quotation/quotation.d.ts +33 -0
  116. package/dist-server/service/quotation/quotation.js +109 -0
  117. package/dist-server/service/quotation/quotation.js.map +1 -0
  118. package/dist-server/service/sales-event.d.ts +15 -0
  119. package/dist-server/service/sales-event.js +12 -0
  120. package/dist-server/service/sales-event.js.map +1 -0
  121. package/dist-server/tsconfig.tsbuildinfo +1 -0
  122. package/helps/quotation/quotation.md +160 -0
  123. package/package.json +46 -4
  124. package/things-factory.config.js +13 -0
  125. package/translations/en.json +98 -0
  126. package/translations/es.json +98 -0
  127. package/translations/ja.json +98 -0
  128. package/translations/ko.json +98 -0
  129. package/translations/ms.json +98 -0
  130. package/translations/zh.json +98 -0
  131. package/README.md +0 -3
@@ -0,0 +1,482 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import '@operato/list-state/ox-page-state.js';
3
+ import gql from 'graphql-tag';
4
+ import { css, html, nothing } from 'lit';
5
+ import { customElement, state } from 'lit/decorators.js';
6
+ import { client } from '@operato/graphql';
7
+ import { i18next, localize } from '@operato/i18n';
8
+ import { notify } from '@operato/layout';
9
+ import { navigate, PageView } from '@operato/shell';
10
+ import { ButtonStyles, DocumentPageStyles, FormFieldStyles } from '@operato/styles';
11
+ import { currencyChoices } from '@things-factory/invoice/dist-client';
12
+ import { opportunitySection } from '../opportunity-attach';
13
+ import { opportunityStageWord } from '../opportunity-words';
14
+ const FIELDS = `
15
+ id name stage probability amount currency expectedCloseMonth closedOn lostReason description
16
+ counterpartyId endCustomerId ownerId counterparty { id name } endCustomer { id name } owner { id name }
17
+ `;
18
+ const thisMonth = () => new Date().toLocaleDateString('en-CA').slice(0, 7);
19
+ /**
20
+ * 영업 기회 편집 — with whom, for how much, by when and how likely.
21
+ *
22
+ * A person moves an open opportunity through the stages; changing the stage puts that stage's probability in, and the
23
+ * person may change it. An open one is closed as lost with the reason; a lost one is opened again by choosing an open
24
+ * stage. Winning is the application's: it closes the opportunity when the opportunity becomes its contract.
25
+ */
26
+ let OpportunityEditor = class OpportunityEditor extends localize(i18next)(PageView) {
27
+ constructor() {
28
+ super(...arguments);
29
+ this.opportunity = null;
30
+ this.form = blank();
31
+ this.pickers = { counterparty: { search: '', found: [] }, endCustomer: { search: '', found: [] } };
32
+ this.stages = [];
33
+ this.owners = [];
34
+ this.events = [];
35
+ this.reason = '';
36
+ this.loading = false;
37
+ this.error = null;
38
+ this.busy = false;
39
+ }
40
+ static { this.styles = [
41
+ FormFieldStyles,
42
+ ButtonStyles,
43
+ DocumentPageStyles,
44
+ css `
45
+ .party {
46
+ display: flex;
47
+ gap: var(--spacing-small);
48
+ align-items: center;
49
+ }
50
+ .party input {
51
+ flex: 1;
52
+ }
53
+ table {
54
+ width: 100%;
55
+ border-collapse: collapse;
56
+ font-variant-numeric: tabular-nums;
57
+ }
58
+ th,
59
+ td {
60
+ text-align: left;
61
+ padding: var(--spacing-small);
62
+ border-bottom: 1px solid var(--ui-border, rgba(0, 0, 0, 0.1));
63
+ }
64
+ td.number {
65
+ text-align: right;
66
+ }
67
+ `
68
+ ]; }
69
+ isOpen(stage) {
70
+ return this.stages.some(one => one.stage === stage && one.open);
71
+ }
72
+ /** Won is final; a lost one is changed only by being opened again. */
73
+ get editable() {
74
+ return !this.busy && this.opportunity?.stage !== 'WON' && this.isOpen(this.form.stage);
75
+ }
76
+ get context() {
77
+ const actions = [];
78
+ if (this.editable)
79
+ actions.push({ title: i18next.t('button.save'), icon: 'save', emphasis: { raised: true }, action: () => this.save() });
80
+ return {
81
+ title: this.opportunity ? `${i18next.t('title.opportunity')} ${this.opportunity.name}` : i18next.t('title.opportunity-new'),
82
+ ...(this.opportunity ? { description: opportunityStageWord(this.opportunity.stage) } : {}),
83
+ frame: 'document',
84
+ /* Opened from the opportunity list; the menu marks it. */
85
+ menu: 'opportunity-list',
86
+ actions: this.error ? [] : actions
87
+ };
88
+ }
89
+ async pageUpdated(changes, lifecycle) {
90
+ if (!this.active || !(changes.active || changes.params))
91
+ return;
92
+ if (!this.stages.length)
93
+ await this.readChoices();
94
+ const id = lifecycle?.params?.id;
95
+ if (!id) {
96
+ this.opportunity = null;
97
+ this.form = blank(this.stages);
98
+ this.events = [];
99
+ this.updateContext();
100
+ return;
101
+ }
102
+ if (id !== this.opportunity?.id)
103
+ await this.load(id);
104
+ }
105
+ render() {
106
+ const attached = this.opportunity ? opportunitySection() : undefined;
107
+ const editable = this.editable;
108
+ const set = (patch) => (this.form = { ...this.form, ...patch });
109
+ const stageChoices = this.stages.filter(one => one.open || one.stage === this.opportunity?.stage);
110
+ return html `<ox-page-state .loading=${this.loading} .error=${this.error} @retry=${() => this.opportunity && this.load(this.opportunity.id)}>
111
+ <div class="page">
112
+ <section class="page-section">
113
+ <header class="page-section-header"><h3>${i18next.t('text.opportunity-section-head')}</h3></header>
114
+ <div class="form">
115
+ <label class="wide"
116
+ >${i18next.t('field.opportunity-name')}
117
+ <input .value=${this.form.name} ?disabled=${!editable} @input=${(e) => set({ name: e.target.value })}
118
+ /></label>
119
+ ${this.renderParty('counterparty', i18next.t('field.opportunity-counterparty'), editable)}
120
+ ${this.renderParty('endCustomer', i18next.t('field.opportunity-end-customer'), editable)}
121
+ <label
122
+ >${i18next.t('field.opportunity-amount')}
123
+ <input type="number" min="0" .value=${this.form.amount} ?disabled=${!editable} @input=${(e) => set({ amount: e.target.value })}
124
+ /></label>
125
+ <label
126
+ >${i18next.t('field.opportunity-currency')}
127
+ <select .value=${this.form.currency} ?disabled=${!editable} @change=${(e) => set({ currency: e.target.value })}>
128
+ ${currencyChoices().map((code) => html `<option value=${code} ?selected=${code === this.form.currency}>${code}</option>`)}
129
+ </select>
130
+ </label>
131
+ <label
132
+ >${i18next.t('field.opportunity-expected-close-month')}
133
+ <input type="month" .value=${this.form.expectedCloseMonth} ?disabled=${!editable} @input=${(e) => set({ expectedCloseMonth: e.target.value })}
134
+ /></label>
135
+ <label
136
+ >${i18next.t('field.opportunity-owner')}
137
+ <select .value=${this.form.ownerId} ?disabled=${!editable} @change=${(e) => set({ ownerId: e.target.value })}>
138
+ ${this.form.ownerId ? nothing : html `<option value="" selected>${i18next.t('text.opportunity-owner-me')}</option>`}
139
+ ${this.owners.map(one => html `<option value=${one.id} ?selected=${one.id === this.form.ownerId}>${one.name}</option>`)}
140
+ </select>
141
+ </label>
142
+ <label class="wide"
143
+ >${i18next.t('field.opportunity-description')}
144
+ <textarea .value=${this.form.description} ?disabled=${!editable} @input=${(e) => set({ description: e.target.value })}></textarea>
145
+ </label>
146
+ </div>
147
+ </section>
148
+ <section class="page-section">
149
+ <header class="page-section-header"><h3>${i18next.t('text.opportunity-section-stage')}</h3></header>
150
+ <div class="form">
151
+ <label
152
+ >${i18next.t('field.opportunity-stage')}
153
+ <select .value=${this.form.stage} ?disabled=${this.busy || this.opportunity?.stage === 'WON'} @change=${(e) => this.chooseStage(e.target.value)}>
154
+ ${stageChoices.map(one => html `<option value=${one.stage} ?selected=${one.stage === this.form.stage} ?disabled=${!one.open}>${opportunityStageWord(one.stage)}</option>`)}
155
+ </select>
156
+ </label>
157
+ <label
158
+ >${i18next.t('field.opportunity-probability')}
159
+ <input type="number" min="0" max="100" step="1" .value=${this.form.probability} ?disabled=${!editable} @input=${(e) => set({ probability: e.target.value })}
160
+ /></label>
161
+ ${this.opportunity?.closedOn
162
+ ? html `<label>${i18next.t('field.opportunity-closed-on')} <strong>${this.opportunity.closedOn}</strong></label>`
163
+ : nothing}
164
+ </div>
165
+ </section>
166
+ ${this.opportunity && this.isOpen(this.opportunity.stage) ? this.renderLose() : nothing}
167
+ ${this.opportunity?.lostReason
168
+ ? html `<section class="page-section">
169
+ <header class="page-section-header"><h3>${i18next.t('field.opportunity-lost-reason')}</h3></header>
170
+ <p>${this.opportunity.lostReason}</p>
171
+ </section>`
172
+ : nothing}
173
+ ${attached && this.opportunity
174
+ ? html `<section class="page-section">
175
+ <header class="page-section-header"><h3>${attached.heading()}</h3></header>
176
+ ${attached.render(this.opportunity)}
177
+ </section>`
178
+ : nothing}
179
+ ${this.events.length ? this.renderEvents() : nothing}
180
+ </div>
181
+ </ox-page-state>`;
182
+ }
183
+ /** A counterparty of the domain, found by name or business number. */
184
+ renderParty(which, label, editable) {
185
+ const picker = this.pickers[which];
186
+ const chosen = this.form[which];
187
+ return html `<label
188
+ >${label}
189
+ <span class="party">
190
+ <input
191
+ .value=${picker.search}
192
+ placeholder=${chosen?.name ?? i18next.t('text.opportunity-counterparty-search')}
193
+ ?disabled=${!editable}
194
+ @input=${(e) => this.setPicker(which, { search: e.target.value })}
195
+ @keydown=${(e) => e.key === 'Enter' && this.findParties(which)}
196
+ />
197
+ <button ?disabled=${!editable} @click=${() => this.findParties(which)}>${i18next.t('button.search')}</button>
198
+ ${which === 'endCustomer' && chosen ? html `<button ?disabled=${!editable} @click=${() => (this.form = { ...this.form, endCustomer: null })}>${i18next.t('button.opportunity-clear')}</button>` : nothing}
199
+ </span>
200
+ ${picker.found.length
201
+ ? html `<select ?disabled=${!editable} @change=${(e) => this.chooseParty(which, picker.found.find(one => one.id === e.target.value) ?? null)}>
202
+ <option value="">${chosen?.name ?? ''}</option>
203
+ ${picker.found.map(one => html `<option value=${one.id}>${one.name}${one.businessNumber ? ` (${one.businessNumber})` : ''}</option>`)}
204
+ </select>`
205
+ : nothing}
206
+ </label>`;
207
+ }
208
+ renderLose() {
209
+ return html `<section class="page-section">
210
+ <header class="page-section-header">
211
+ <h3>${i18next.t('text.opportunity-section-lose')}</h3>
212
+ <div class="page-section-actions">
213
+ <button ?disabled=${this.busy || !this.reason.trim()} @click=${() => this.lose()}>${i18next.t('button.opportunity-lose')}</button>
214
+ </div>
215
+ </header>
216
+ <div class="form">
217
+ <label class="wide"
218
+ >${i18next.t('field.opportunity-lost-reason')}
219
+ <input .value=${this.reason} ?disabled=${this.busy} @input=${(e) => (this.reason = e.target.value)}
220
+ /></label>
221
+ </div>
222
+ </section>`;
223
+ }
224
+ renderEvents() {
225
+ return html `<section class="page-section">
226
+ <header class="page-section-header"><h3>${i18next.t('text.opportunity-section-history')}</h3></header>
227
+ <table>
228
+ <thead>
229
+ <tr>
230
+ <th>${i18next.t('field.opportunity-event-at')}</th>
231
+ <th>${i18next.t('field.opportunity-event-actor')}</th>
232
+ <th>${i18next.t('field.opportunity-event-action')}</th>
233
+ <th>${i18next.t('field.opportunity-stage')}</th>
234
+ <th>${i18next.t('field.opportunity-probability')}</th>
235
+ </tr>
236
+ </thead>
237
+ <tbody>
238
+ ${this.events.map(one => html `<tr>
239
+ <td>${one.createdAt ? new Date(one.createdAt).toLocaleString() : ''}</td>
240
+ <td>${one.actor?.name ?? ''}</td>
241
+ <td>${eventWord(one.action)}${one.reason ? ` · ${one.reason}` : ''}</td>
242
+ <td>${one.fromStage ? `${opportunityStageWord(one.fromStage)} → ` : ''}${opportunityStageWord(one.stage)}</td>
243
+ <td class="number">${one.probability}%</td>
244
+ </tr>`)}
245
+ </tbody>
246
+ </table>
247
+ </section>`;
248
+ }
249
+ setPicker(which, patch) {
250
+ this.pickers = { ...this.pickers, [which]: { ...this.pickers[which], ...patch } };
251
+ }
252
+ chooseParty(which, party) {
253
+ if (!party)
254
+ return;
255
+ this.form = { ...this.form, [which]: party };
256
+ this.setPicker(which, { search: '', found: [] });
257
+ }
258
+ /** The stage's own probability comes in with it; the person may change it afterwards. */
259
+ chooseStage(stage) {
260
+ const given = this.stages.find(one => one.stage === stage);
261
+ this.form = { ...this.form, stage, probability: given ? String(given.probability) : this.form.probability };
262
+ }
263
+ async readChoices() {
264
+ try {
265
+ const response = await client.query({
266
+ query: gql `
267
+ query {
268
+ opportunityStages { stage probability open }
269
+ opportunityOwners { id name email }
270
+ }
271
+ `,
272
+ fetchPolicy: 'no-cache'
273
+ });
274
+ if (response.errors?.length)
275
+ throw response;
276
+ this.stages = response.data.opportunityStages;
277
+ this.owners = response.data.opportunityOwners;
278
+ }
279
+ catch (error) {
280
+ this.error = error;
281
+ }
282
+ }
283
+ async findParties(which) {
284
+ await this.run(async () => {
285
+ const response = await client.query({
286
+ query: gql `
287
+ query ($search: String, $pagination: Pagination) {
288
+ counterparties(search: $search, pagination: $pagination) {
289
+ items { id name businessNumber }
290
+ }
291
+ }
292
+ `,
293
+ variables: { search: this.pickers[which].search.trim() || undefined, pagination: { page: 1, limit: 30 } },
294
+ fetchPolicy: 'no-cache'
295
+ });
296
+ if (response.errors?.length)
297
+ throw response;
298
+ const found = response.data.counterparties.items ?? [];
299
+ if (found.length === 1)
300
+ this.chooseParty(which, found[0]);
301
+ else
302
+ this.setPicker(which, { found });
303
+ });
304
+ }
305
+ async load(id) {
306
+ this.loading = true;
307
+ this.error = null;
308
+ try {
309
+ const response = await client.query({
310
+ query: gql(`query ($id: String!) { opportunity(id: $id) { ${FIELDS} } opportunityEvents(opportunityId: $id) { id action fromStage stage probability reason createdAt actor { id name } } }`),
311
+ variables: { id },
312
+ fetchPolicy: 'no-cache'
313
+ });
314
+ if (response.errors?.length)
315
+ throw response;
316
+ if (!response.data.opportunity)
317
+ throw new Error(i18next.t('text.opportunity-not-found'));
318
+ this.show(response.data.opportunity, response.data.opportunityEvents);
319
+ }
320
+ catch (error) {
321
+ this.error = error;
322
+ }
323
+ finally {
324
+ this.loading = false;
325
+ this.updateContext();
326
+ }
327
+ }
328
+ show(opportunity, events) {
329
+ this.opportunity = opportunity;
330
+ this.events = events;
331
+ this.form = {
332
+ name: opportunity.name,
333
+ counterparty: opportunity.counterparty,
334
+ endCustomer: opportunity.endCustomer,
335
+ amount: String(opportunity.amount ?? 0),
336
+ currency: opportunity.currency,
337
+ expectedCloseMonth: opportunity.expectedCloseMonth,
338
+ stage: opportunity.stage,
339
+ probability: String(opportunity.probability),
340
+ ownerId: opportunity.ownerId ?? '',
341
+ description: opportunity.description ?? ''
342
+ };
343
+ this.reason = '';
344
+ }
345
+ async run(work) {
346
+ if (this.busy)
347
+ return undefined;
348
+ this.busy = true;
349
+ try {
350
+ return await work();
351
+ }
352
+ catch (error) {
353
+ notify({ level: 'error', message: error?.graphQLErrors?.[0]?.message ?? error?.errors?.[0]?.message ?? error?.message ?? String(error) });
354
+ return undefined;
355
+ }
356
+ finally {
357
+ this.busy = false;
358
+ }
359
+ }
360
+ input() {
361
+ const { name, counterparty, endCustomer, amount, currency, expectedCloseMonth, stage, probability, ownerId, description } = this.form;
362
+ return {
363
+ name,
364
+ counterpartyId: counterparty?.id ?? '',
365
+ endCustomerId: endCustomer?.id ?? null,
366
+ amount: Number(amount || 0),
367
+ currency,
368
+ expectedCloseMonth,
369
+ stage,
370
+ probability: probability === '' ? null : Number(probability),
371
+ ownerId: ownerId || null,
372
+ description
373
+ };
374
+ }
375
+ async save() {
376
+ const id = this.opportunity?.id;
377
+ const saved = await this.run(async () => {
378
+ const response = await client.mutate({
379
+ mutation: gql(id
380
+ ? `mutation ($id: String!, $opportunity: NewOpportunity!) { opportunity: updateOpportunity(id: $id, opportunity: $opportunity) { id } }`
381
+ : `mutation ($opportunity: NewOpportunity!) { opportunity: createOpportunity(opportunity: $opportunity) { id } }`),
382
+ variables: { ...(id ? { id } : {}), opportunity: this.input() }
383
+ });
384
+ if (response.errors?.length)
385
+ throw response;
386
+ return response.data.opportunity;
387
+ });
388
+ if (!saved)
389
+ return;
390
+ if (!id)
391
+ navigate(`opportunity-editor?id=${saved.id}`);
392
+ else
393
+ await this.load(id);
394
+ }
395
+ async lose() {
396
+ const id = this.opportunity.id;
397
+ const lost = await this.run(async () => {
398
+ const response = await client.mutate({
399
+ mutation: gql `
400
+ mutation ($id: String!, $reason: String!) {
401
+ loseOpportunity(id: $id, reason: $reason) { id }
402
+ }
403
+ `,
404
+ variables: { id, reason: this.reason.trim() }
405
+ });
406
+ if (response.errors?.length)
407
+ throw response;
408
+ return response.data.loseOpportunity;
409
+ });
410
+ if (lost)
411
+ await this.load(id);
412
+ }
413
+ };
414
+ __decorate([
415
+ state(),
416
+ __metadata("design:type", Object)
417
+ ], OpportunityEditor.prototype, "opportunity", void 0);
418
+ __decorate([
419
+ state(),
420
+ __metadata("design:type", Object)
421
+ ], OpportunityEditor.prototype, "form", void 0);
422
+ __decorate([
423
+ state(),
424
+ __metadata("design:type", Object)
425
+ ], OpportunityEditor.prototype, "pickers", void 0);
426
+ __decorate([
427
+ state(),
428
+ __metadata("design:type", Array)
429
+ ], OpportunityEditor.prototype, "stages", void 0);
430
+ __decorate([
431
+ state(),
432
+ __metadata("design:type", Array)
433
+ ], OpportunityEditor.prototype, "owners", void 0);
434
+ __decorate([
435
+ state(),
436
+ __metadata("design:type", Array)
437
+ ], OpportunityEditor.prototype, "events", void 0);
438
+ __decorate([
439
+ state(),
440
+ __metadata("design:type", Object)
441
+ ], OpportunityEditor.prototype, "reason", void 0);
442
+ __decorate([
443
+ state(),
444
+ __metadata("design:type", Object)
445
+ ], OpportunityEditor.prototype, "loading", void 0);
446
+ __decorate([
447
+ state(),
448
+ __metadata("design:type", Object)
449
+ ], OpportunityEditor.prototype, "error", void 0);
450
+ __decorate([
451
+ state(),
452
+ __metadata("design:type", Object)
453
+ ], OpportunityEditor.prototype, "busy", void 0);
454
+ OpportunityEditor = __decorate([
455
+ customElement('opportunity-editor')
456
+ ], OpportunityEditor);
457
+ export { OpportunityEditor };
458
+ const eventWord = (action) => action === 'CREATED'
459
+ ? i18next.t('text.opportunity-event-created')
460
+ : action === 'STAGED'
461
+ ? i18next.t('text.opportunity-event-staged')
462
+ : action === 'WON'
463
+ ? i18next.t('text.opportunity-event-won')
464
+ : action === 'LOST'
465
+ ? i18next.t('text.opportunity-event-lost')
466
+ : i18next.t('text.opportunity-event-updated');
467
+ function blank(stages = []) {
468
+ const lead = stages.find(one => one.stage === 'LEAD');
469
+ return {
470
+ name: '',
471
+ counterparty: null,
472
+ endCustomer: null,
473
+ amount: '0',
474
+ currency: 'KRW',
475
+ expectedCloseMonth: thisMonth(),
476
+ stage: 'LEAD',
477
+ probability: lead ? String(lead.probability) : '',
478
+ ownerId: '',
479
+ description: ''
480
+ };
481
+ }
482
+ //# sourceMappingURL=opportunity-editor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opportunity-editor.js","sourceRoot":"","sources":["../../client/pages/opportunity-editor.ts"],"names":[],"mappings":";AAAA,OAAO,sCAAsC,CAAA;AAE7C,OAAO,GAAG,MAAM,aAAa,CAAA;AAC7B,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,KAAK,CAAA;AACxC,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAExD,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AACxC,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AACnD,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACnF,OAAO,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAA;AAErE,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAC1D,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAE3D,MAAM,MAAM,GAAG;;;CAGd,CAAA;AAiBD,MAAM,SAAS,GAAG,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;AAE1E;;;;;;GAMG;AAEI,IAAM,iBAAiB,GAAvB,MAAM,iBAAkB,SAAQ,QAAQ,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC;IAA3D;;QA+BI,gBAAW,GAAQ,IAAI,CAAA;QACvB,SAAI,GAAS,KAAK,EAAE,CAAA;QACpB,YAAO,GAAmD,EAAE,YAAY,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,CAAA;QAC7I,WAAM,GAA4D,EAAE,CAAA;QACpE,WAAM,GAAmD,EAAE,CAAA;QAC3D,WAAM,GAAU,EAAE,CAAA;QAClB,WAAM,GAAG,EAAE,CAAA;QACX,YAAO,GAAG,KAAK,CAAA;QACf,UAAK,GAAY,IAAI,CAAA;QACrB,SAAI,GAAG,KAAK,CAAA;IAyVvB,CAAC;aAhYQ,WAAM,GAAG;QACd,eAAe;QACf,YAAY;QACZ,kBAAkB;QAClB,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;KAuBF;KACF,AA5BY,CA4BZ;IAaO,MAAM,CAAC,KAAa;QAC1B,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,KAAK,KAAK,KAAK,IAAI,GAAG,CAAC,IAAI,CAAC,CAAA;IACjE,CAAC;IAED,sEAAsE;IACtE,IAAI,QAAQ;QACV,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,WAAW,EAAE,KAAK,KAAK,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACxF,CAAC;IAED,IAAI,OAAO;QACT,MAAM,OAAO,GAAU,EAAE,CAAA;QACzB,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;QACzI,OAAO;YACL,KAAK,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,mBAAmB,CAAC,IAAI,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,uBAAuB,CAAC;YAC3H,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,oBAAoB,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC1F,KAAK,EAAE,UAAU;YACjB,0DAA0D;YAC1D,IAAI,EAAE,kBAAkB;YACxB,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO;SACnC,CAAA;IACH,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,OAAY,EAAE,SAAc;QAC5C,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC;YAAE,OAAM;QAC/D,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM;YAAE,MAAM,IAAI,CAAC,WAAW,EAAE,CAAA;QACjD,MAAM,EAAE,GAAG,SAAS,EAAE,MAAM,EAAE,EAAE,CAAA;QAChC,IAAI,CAAC,EAAE,EAAE,CAAC;YACR,IAAI,CAAC,WAAW,GAAG,IAAI,CAAA;YACvB,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;YAC9B,IAAI,CAAC,MAAM,GAAG,EAAE,CAAA;YAChB,IAAI,CAAC,aAAa,EAAE,CAAA;YACpB,OAAM;QACR,CAAC;QACD,IAAI,EAAE,KAAK,IAAI,CAAC,WAAW,EAAE,EAAE;YAAE,MAAM,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACtD,CAAC;IAED,MAAM;QACJ,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,kBAAkB,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;QACpE,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAA;QAC9B,MAAM,GAAG,GAAG,CAAC,KAAoB,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,GAAG,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,KAAK,EAAE,CAAC,CAAA;QAC9E,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,KAAK,KAAK,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,CAAA;QACjG,OAAO,IAAI,CAAA,2BAA2B,IAAI,CAAC,OAAO,WAAW,IAAI,CAAC,KAAK,WAAW,GAAG,EAAE,CAAC,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;;;oDAG1F,OAAO,CAAC,CAAC,CAAC,+BAA+B,CAAC;;;iBAG7E,OAAO,CAAC,CAAC,CAAC,wBAAwB,CAAC;8BACtB,IAAI,CAAC,IAAI,CAAC,IAAI,cAAc,CAAC,QAAQ,WAAW,CAAC,CAAM,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;;cAEzG,IAAI,CAAC,WAAW,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC,CAAC,gCAAgC,CAAC,EAAE,QAAQ,CAAC;cACvF,IAAI,CAAC,WAAW,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC,CAAC,gCAAgC,CAAC,EAAE,QAAQ,CAAC;;iBAEnF,OAAO,CAAC,CAAC,CAAC,0BAA0B,CAAC;oDACF,IAAI,CAAC,IAAI,CAAC,MAAM,cAAc,CAAC,QAAQ,WAAW,CAAC,CAAM,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;;;iBAGhI,OAAO,CAAC,CAAC,CAAC,4BAA4B,CAAC;+BACzB,IAAI,CAAC,IAAI,CAAC,QAAQ,cAAc,CAAC,QAAQ,YAAY,CAAC,CAAM,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;kBAC/G,eAAe,EAAE,CAAC,GAAG,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,IAAI,CAAA,iBAAiB,IAAI,cAAc,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,WAAW,CAAC;;;;iBAI/H,OAAO,CAAC,CAAC,CAAC,wCAAwC,CAAC;2CACzB,IAAI,CAAC,IAAI,CAAC,kBAAkB,cAAc,CAAC,QAAQ,WAAW,CAAC,CAAM,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,kBAAkB,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;;;iBAG/I,OAAO,CAAC,CAAC,CAAC,yBAAyB,CAAC;+BACtB,IAAI,CAAC,IAAI,CAAC,OAAO,cAAc,CAAC,QAAQ,YAAY,CAAC,CAAM,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;kBAC7G,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAA,6BAA6B,OAAO,CAAC,CAAC,CAAC,2BAA2B,CAAC,WAAW;kBAChH,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAA,iBAAiB,GAAG,CAAC,EAAE,cAAc,GAAG,CAAC,EAAE,KAAK,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,GAAG,CAAC,IAAI,WAAW,CAAC;;;;iBAIrH,OAAO,CAAC,CAAC,CAAC,+BAA+B,CAAC;iCAC1B,IAAI,CAAC,IAAI,CAAC,WAAW,cAAc,CAAC,QAAQ,WAAW,CAAC,CAAM,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;;;;;oDAKpF,OAAO,CAAC,CAAC,CAAC,gCAAgC,CAAC;;;iBAG9E,OAAO,CAAC,CAAC,CAAC,yBAAyB,CAAC;+BACtB,IAAI,CAAC,IAAI,CAAC,KAAK,cAAc,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,WAAW,EAAE,KAAK,KAAK,KAAK,YAAY,CAAC,CAAM,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;kBAChJ,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAA,iBAAiB,GAAG,CAAC,KAAK,cAAc,GAAG,CAAC,KAAK,KAAK,IAAI,CAAC,IAAI,CAAC,KAAK,cAAc,CAAC,GAAG,CAAC,IAAI,IAAI,oBAAoB,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC;;;;iBAIxK,OAAO,CAAC,CAAC,CAAC,+BAA+B,CAAC;uEACY,IAAI,CAAC,IAAI,CAAC,WAAW,cAAc,CAAC,QAAQ,WAAW,CAAC,CAAM,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;;cAEhK,IAAI,CAAC,WAAW,EAAE,QAAQ;YAC1B,CAAC,CAAC,IAAI,CAAA,UAAU,OAAO,CAAC,CAAC,CAAC,6BAA6B,CAAC,YAAY,IAAI,CAAC,WAAW,CAAC,QAAQ,mBAAmB;YAChH,CAAC,CAAC,OAAO;;;UAGb,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,OAAO;UACrF,IAAI,CAAC,WAAW,EAAE,UAAU;YAC5B,CAAC,CAAC,IAAI,CAAA;wDACwC,OAAO,CAAC,CAAC,CAAC,+BAA+B,CAAC;mBAC/E,IAAI,CAAC,WAAW,CAAC,UAAU;uBACvB;YACb,CAAC,CAAC,OAAO;UACT,QAAQ,IAAI,IAAI,CAAC,WAAW;YAC5B,CAAC,CAAC,IAAI,CAAA;wDACwC,QAAQ,CAAC,OAAO,EAAE;gBAC1D,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC;uBAC1B;YACb,CAAC,CAAC,OAAO;UACT,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,OAAO;;qBAEvC,CAAA;IACnB,CAAC;IAED,sEAAsE;IAC9D,WAAW,CAAC,KAAqC,EAAE,KAAa,EAAE,QAAiB;QACzF,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;QAClC,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC/B,OAAO,IAAI,CAAA;SACN,KAAK;;;mBAGK,MAAM,CAAC,MAAM;wBACR,MAAM,EAAE,IAAI,IAAI,OAAO,CAAC,CAAC,CAAC,sCAAsC,CAAC;sBACnE,CAAC,QAAQ;mBACZ,CAAC,CAAM,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;qBAC3D,CAAC,CAAgB,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,OAAO,IAAI,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC;;4BAE3D,CAAC,QAAQ,WAAW,GAAG,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC;UACjG,KAAK,KAAK,aAAa,IAAI,MAAM,CAAC,CAAC,CAAC,IAAI,CAAA,qBAAqB,CAAC,QAAQ,WAAW,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,GAAG,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,0BAA0B,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO;;QAExM,MAAM,CAAC,KAAK,CAAC,MAAM;YACnB,CAAC,CAAC,IAAI,CAAA,qBAAqB,CAAC,QAAQ,YAAY,CAAC,CAAM,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC;+BACzH,MAAM,EAAE,IAAI,IAAI,EAAE;cACnC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAA,iBAAiB,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,cAAc,GAAG,CAAC,CAAC,CAAC,EAAE,WAAW,CAAC;oBAC5H;YACZ,CAAC,CAAC,OAAO;aACJ,CAAA;IACX,CAAC;IAEO,UAAU;QAChB,OAAO,IAAI,CAAA;;cAED,OAAO,CAAC,CAAC,CAAC,+BAA+B,CAAC;;8BAE1B,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,OAAO,CAAC,CAAC,CAAC,yBAAyB,CAAC;;;;;aAKrH,OAAO,CAAC,CAAC,CAAC,+BAA+B,CAAC;0BAC7B,IAAI,CAAC,MAAM,cAAc,IAAI,CAAC,IAAI,WAAW,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;;;eAGlG,CAAA;IACb,CAAC;IAEO,YAAY;QAClB,OAAO,IAAI,CAAA;gDACiC,OAAO,CAAC,CAAC,CAAC,kCAAkC,CAAC;;;;kBAI3E,OAAO,CAAC,CAAC,CAAC,4BAA4B,CAAC;kBACvC,OAAO,CAAC,CAAC,CAAC,+BAA+B,CAAC;kBAC1C,OAAO,CAAC,CAAC,CAAC,gCAAgC,CAAC;kBAC3C,OAAO,CAAC,CAAC,CAAC,yBAAyB,CAAC;kBACpC,OAAO,CAAC,CAAC,CAAC,+BAA+B,CAAC;;;;YAIhD,IAAI,CAAC,MAAM,CAAC,GAAG,CACf,GAAG,CAAC,EAAE,CAAC,IAAI,CAAA;oBACH,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE;oBAC7D,GAAG,CAAC,KAAK,EAAE,IAAI,IAAI,EAAE;oBACrB,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE;oBAC5D,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,oBAAoB,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,oBAAoB,CAAC,GAAG,CAAC,KAAK,CAAC;mCACnF,GAAG,CAAC,WAAW;kBAChC,CACP;;;eAGI,CAAA;IACb,CAAC;IAEO,SAAS,CAAC,KAAqC,EAAE,KAAsB;QAC7E,IAAI,CAAC,OAAO,GAAG,EAAE,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,GAAG,KAAK,EAAE,EAAE,CAAA;IACnF,CAAC;IAEO,WAAW,CAAC,KAAqC,EAAE,KAAmB;QAC5E,IAAI,CAAC,KAAK;YAAE,OAAM;QAClB,IAAI,CAAC,IAAI,GAAG,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,CAAA;QAC5C,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAA;IAClD,CAAC;IAED,yFAAyF;IACjF,WAAW,CAAC,KAAa;QAC/B,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,KAAK,KAAK,KAAK,CAAC,CAAA;QAC1D,IAAI,CAAC,IAAI,GAAG,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAA;IAC7G,CAAC;IAEO,KAAK,CAAC,WAAW;QACvB,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;gBAClC,KAAK,EAAE,GAAG,CAAA;;;;;SAKT;gBACD,WAAW,EAAE,UAAU;aACxB,CAAC,CAAA;YACF,IAAI,QAAQ,CAAC,MAAM,EAAE,MAAM;gBAAE,MAAM,QAAQ,CAAA;YAC3C,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,iBAAiB,CAAA;YAC7C,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,iBAAiB,CAAA;QAC/C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;QACpB,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,WAAW,CAAC,KAAqC;QAC7D,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE;YACxB,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;gBAClC,KAAK,EAAE,GAAG,CAAA;;;;;;SAMT;gBACD,SAAS,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,SAAS,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE;gBACzG,WAAW,EAAE,UAAU;aACxB,CAAC,CAAA;YACF,IAAI,QAAQ,CAAC,MAAM,EAAE,MAAM;gBAAE,MAAM,QAAQ,CAAA;YAC3C,MAAM,KAAK,GAAY,QAAQ,CAAC,IAAI,CAAC,cAAc,CAAC,KAAK,IAAI,EAAE,CAAA;YAC/D,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;gBAAE,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;;gBACpD,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,CAAC,CAAA;QACvC,CAAC,CAAC,CAAA;IACJ,CAAC;IAEO,KAAK,CAAC,IAAI,CAAC,EAAU;QAC3B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAA;QACnB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAA;QACjB,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;gBAClC,KAAK,EAAE,GAAG,CAAC,iDAAiD,MAAM,yHAAyH,CAAC;gBAC5L,SAAS,EAAE,EAAE,EAAE,EAAE;gBACjB,WAAW,EAAE,UAAU;aACxB,CAAC,CAAA;YACF,IAAI,QAAQ,CAAC,MAAM,EAAE,MAAM;gBAAE,MAAM,QAAQ,CAAA;YAC3C,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,WAAW;gBAAE,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,4BAA4B,CAAC,CAAC,CAAA;YACxF,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAA;QACvE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;QACpB,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,OAAO,GAAG,KAAK,CAAA;YACpB,IAAI,CAAC,aAAa,EAAE,CAAA;QACtB,CAAC;IACH,CAAC;IAEO,IAAI,CAAC,WAAgB,EAAE,MAAa;QAC1C,IAAI,CAAC,WAAW,GAAG,WAAW,CAAA;QAC9B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,IAAI,CAAC,IAAI,GAAG;YACV,IAAI,EAAE,WAAW,CAAC,IAAI;YACtB,YAAY,EAAE,WAAW,CAAC,YAAY;YACtC,WAAW,EAAE,WAAW,CAAC,WAAW;YACpC,MAAM,EAAE,MAAM,CAAC,WAAW,CAAC,MAAM,IAAI,CAAC,CAAC;YACvC,QAAQ,EAAE,WAAW,CAAC,QAAQ;YAC9B,kBAAkB,EAAE,WAAW,CAAC,kBAAkB;YAClD,KAAK,EAAE,WAAW,CAAC,KAAK;YACxB,WAAW,EAAE,MAAM,CAAC,WAAW,CAAC,WAAW,CAAC;YAC5C,OAAO,EAAE,WAAW,CAAC,OAAO,IAAI,EAAE;YAClC,WAAW,EAAE,WAAW,CAAC,WAAW,IAAI,EAAE;SAC3C,CAAA;QACD,IAAI,CAAC,MAAM,GAAG,EAAE,CAAA;IAClB,CAAC;IAEO,KAAK,CAAC,GAAG,CAAI,IAAsB;QACzC,IAAI,IAAI,CAAC,IAAI;YAAE,OAAO,SAAS,CAAA;QAC/B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,EAAE,CAAA;QACrB,CAAC;QAAC,OAAO,KAAU,EAAE,CAAC;YACpB,MAAM,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,IAAI,KAAK,EAAE,OAAO,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;YACzI,OAAO,SAAS,CAAA;QAClB,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,IAAI,GAAG,KAAK,CAAA;QACnB,CAAC;IACH,CAAC;IAEO,KAAK;QACX,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC,IAAI,CAAA;QACrI,OAAO;YACL,IAAI;YACJ,cAAc,EAAE,YAAY,EAAE,EAAE,IAAI,EAAE;YACtC,aAAa,EAAE,WAAW,EAAE,EAAE,IAAI,IAAI;YACtC,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;YAC3B,QAAQ;YACR,kBAAkB;YAClB,KAAK;YACL,WAAW,EAAE,WAAW,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC;YAC5D,OAAO,EAAE,OAAO,IAAI,IAAI;YACxB,WAAW;SACZ,CAAA;IACH,CAAC;IAEO,KAAK,CAAC,IAAI;QAChB,MAAM,EAAE,GAAG,IAAI,CAAC,WAAW,EAAE,EAAE,CAAA;QAC/B,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE;YACtC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;gBACnC,QAAQ,EAAE,GAAG,CACX,EAAE;oBACA,CAAC,CAAC,sIAAsI;oBACxI,CAAC,CAAC,+GAA+G,CACpH;gBACD,SAAS,EAAE,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,KAAK,EAAE,EAAE;aAChE,CAAC,CAAA;YACF,IAAI,QAAQ,CAAC,MAAM,EAAE,MAAM;gBAAE,MAAM,QAAQ,CAAA;YAC3C,OAAO,QAAQ,CAAC,IAAI,CAAC,WAA6B,CAAA;QACpD,CAAC,CAAC,CAAA;QACF,IAAI,CAAC,KAAK;YAAE,OAAM;QAClB,IAAI,CAAC,EAAE;YAAE,QAAQ,CAAC,yBAAyB,KAAK,CAAC,EAAE,EAAE,CAAC,CAAA;;YACjD,MAAM,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IAC1B,CAAC;IAEO,KAAK,CAAC,IAAI;QAChB,MAAM,EAAE,GAAG,IAAI,CAAC,WAAY,CAAC,EAAE,CAAA;QAC/B,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE;YACrC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;gBACnC,QAAQ,EAAE,GAAG,CAAA;;;;SAIZ;gBACD,SAAS,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE;aAC9C,CAAC,CAAA;YACF,IAAI,QAAQ,CAAC,MAAM,EAAE,MAAM;gBAAE,MAAM,QAAQ,CAAA;YAC3C,OAAO,QAAQ,CAAC,IAAI,CAAC,eAAe,CAAA;QACtC,CAAC,CAAC,CAAA;QACF,IAAI,IAAI;YAAE,MAAM,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IAC/B,CAAC;;AAjWQ;IAAR,KAAK,EAAE;;sDAAwB;AACvB;IAAR,KAAK,EAAE;;+CAAqB;AACpB;IAAR,KAAK,EAAE;;kDAA8I;AAC7I;IAAR,KAAK,EAAE;;iDAAqE;AACpE;IAAR,KAAK,EAAE;;iDAA4D;AAC3D;IAAR,KAAK,EAAE;;iDAAmB;AAClB;IAAR,KAAK,EAAE;;iDAAY;AACX;IAAR,KAAK,EAAE;;kDAAgB;AACf;IAAR,KAAK,EAAE;;gDAAsB;AACrB;IAAR,KAAK,EAAE;;+CAAa;AAxCV,iBAAiB;IAD7B,aAAa,CAAC,oBAAoB,CAAC;GACvB,iBAAiB,CAiY7B;;AAED,MAAM,SAAS,GAAG,CAAC,MAAe,EAAE,EAAE,CACpC,MAAM,KAAK,SAAS;IAClB,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,gCAAgC,CAAC;IAC7C,CAAC,CAAC,MAAM,KAAK,QAAQ;QACnB,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,+BAA+B,CAAC;QAC5C,CAAC,CAAC,MAAM,KAAK,KAAK;YAChB,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,4BAA4B,CAAC;YACzC,CAAC,CAAC,MAAM,KAAK,MAAM;gBACjB,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,6BAA6B,CAAC;gBAC1C,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,gCAAgC,CAAC,CAAA;AAEvD,SAAS,KAAK,CAAC,SAAmD,EAAE;IAClE,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,KAAK,KAAK,MAAM,CAAC,CAAA;IACrD,OAAO;QACL,IAAI,EAAE,EAAE;QACR,YAAY,EAAE,IAAI;QAClB,WAAW,EAAE,IAAI;QACjB,MAAM,EAAE,GAAG;QACX,QAAQ,EAAE,KAAK;QACf,kBAAkB,EAAE,SAAS,EAAE;QAC/B,KAAK,EAAE,MAAM;QACb,WAAW,EAAE,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,EAAE;QACjD,OAAO,EAAE,EAAE;QACX,WAAW,EAAE,EAAE;KAChB,CAAA;AACH,CAAC","sourcesContent":["import '@operato/list-state/ox-page-state.js'\n\nimport gql from 'graphql-tag'\nimport { css, html, nothing } from 'lit'\nimport { customElement, state } from 'lit/decorators.js'\n\nimport { client } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { notify } from '@operato/layout'\nimport { navigate, PageView } from '@operato/shell'\nimport { ButtonStyles, DocumentPageStyles, FormFieldStyles } from '@operato/styles'\nimport { currencyChoices } from '@things-factory/invoice/dist-client'\n\nimport { opportunitySection } from '../opportunity-attach'\nimport { opportunityStageWord } from '../opportunity-words'\n\nconst FIELDS = `\n id name stage probability amount currency expectedCloseMonth closedOn lostReason description\n counterpartyId endCustomerId ownerId counterparty { id name } endCustomer { id name } owner { id name }\n`\n\ntype Party = { id: string; name: string; businessNumber?: string }\ntype Picker = { search: string; found: Party[] }\ntype Form = {\n name: string\n counterparty: Party | null\n endCustomer: Party | null\n amount: string\n currency: string\n expectedCloseMonth: string\n stage: string\n probability: string\n ownerId: string\n description: string\n}\n\nconst thisMonth = () => new Date().toLocaleDateString('en-CA').slice(0, 7)\n\n/**\n * 영업 기회 편집 — with whom, for how much, by when and how likely.\n *\n * A person moves an open opportunity through the stages; changing the stage puts that stage's probability in, and the\n * person may change it. An open one is closed as lost with the reason; a lost one is opened again by choosing an open\n * stage. Winning is the application's: it closes the opportunity when the opportunity becomes its contract.\n */\n@customElement('opportunity-editor')\nexport class OpportunityEditor extends localize(i18next)(PageView) {\n static styles = [\n FormFieldStyles,\n ButtonStyles,\n DocumentPageStyles,\n css`\n .party {\n display: flex;\n gap: var(--spacing-small);\n align-items: center;\n }\n .party input {\n flex: 1;\n }\n table {\n width: 100%;\n border-collapse: collapse;\n font-variant-numeric: tabular-nums;\n }\n th,\n td {\n text-align: left;\n padding: var(--spacing-small);\n border-bottom: 1px solid var(--ui-border, rgba(0, 0, 0, 0.1));\n }\n td.number {\n text-align: right;\n }\n `\n ]\n\n @state() opportunity: any = null\n @state() form: Form = blank()\n @state() pickers: Record<'counterparty' | 'endCustomer', Picker> = { counterparty: { search: '', found: [] }, endCustomer: { search: '', found: [] } }\n @state() stages: { stage: string; probability: number; open: boolean }[] = []\n @state() owners: { id: string; name: string; email?: string }[] = []\n @state() events: any[] = []\n @state() reason = ''\n @state() loading = false\n @state() error: unknown = null\n @state() busy = false\n\n private isOpen(stage: string) {\n return this.stages.some(one => one.stage === stage && one.open)\n }\n\n /** Won is final; a lost one is changed only by being opened again. */\n get editable() {\n return !this.busy && this.opportunity?.stage !== 'WON' && this.isOpen(this.form.stage)\n }\n\n get context() {\n const actions: any[] = []\n if (this.editable) actions.push({ title: i18next.t('button.save'), icon: 'save', emphasis: { raised: true }, action: () => this.save() })\n return {\n title: this.opportunity ? `${i18next.t('title.opportunity')} ${this.opportunity.name}` : i18next.t('title.opportunity-new'),\n ...(this.opportunity ? { description: opportunityStageWord(this.opportunity.stage) } : {}),\n frame: 'document',\n /* Opened from the opportunity list; the menu marks it. */\n menu: 'opportunity-list',\n actions: this.error ? [] : actions\n }\n }\n\n async pageUpdated(changes: any, lifecycle: any) {\n if (!this.active || !(changes.active || changes.params)) return\n if (!this.stages.length) await this.readChoices()\n const id = lifecycle?.params?.id\n if (!id) {\n this.opportunity = null\n this.form = blank(this.stages)\n this.events = []\n this.updateContext()\n return\n }\n if (id !== this.opportunity?.id) await this.load(id)\n }\n\n render() {\n const attached = this.opportunity ? opportunitySection() : undefined\n const editable = this.editable\n const set = (patch: Partial<Form>) => (this.form = { ...this.form, ...patch })\n const stageChoices = this.stages.filter(one => one.open || one.stage === this.opportunity?.stage)\n return html`<ox-page-state .loading=${this.loading} .error=${this.error} @retry=${() => this.opportunity && this.load(this.opportunity.id)}>\n <div class=\"page\">\n <section class=\"page-section\">\n <header class=\"page-section-header\"><h3>${i18next.t('text.opportunity-section-head')}</h3></header>\n <div class=\"form\">\n <label class=\"wide\"\n >${i18next.t('field.opportunity-name')}\n <input .value=${this.form.name} ?disabled=${!editable} @input=${(e: any) => set({ name: e.target.value })}\n /></label>\n ${this.renderParty('counterparty', i18next.t('field.opportunity-counterparty'), editable)}\n ${this.renderParty('endCustomer', i18next.t('field.opportunity-end-customer'), editable)}\n <label\n >${i18next.t('field.opportunity-amount')}\n <input type=\"number\" min=\"0\" .value=${this.form.amount} ?disabled=${!editable} @input=${(e: any) => set({ amount: e.target.value })}\n /></label>\n <label\n >${i18next.t('field.opportunity-currency')}\n <select .value=${this.form.currency} ?disabled=${!editable} @change=${(e: any) => set({ currency: e.target.value })}>\n ${currencyChoices().map((code: string) => html`<option value=${code} ?selected=${code === this.form.currency}>${code}</option>`)}\n </select>\n </label>\n <label\n >${i18next.t('field.opportunity-expected-close-month')}\n <input type=\"month\" .value=${this.form.expectedCloseMonth} ?disabled=${!editable} @input=${(e: any) => set({ expectedCloseMonth: e.target.value })}\n /></label>\n <label\n >${i18next.t('field.opportunity-owner')}\n <select .value=${this.form.ownerId} ?disabled=${!editable} @change=${(e: any) => set({ ownerId: e.target.value })}>\n ${this.form.ownerId ? nothing : html`<option value=\"\" selected>${i18next.t('text.opportunity-owner-me')}</option>`}\n ${this.owners.map(one => html`<option value=${one.id} ?selected=${one.id === this.form.ownerId}>${one.name}</option>`)}\n </select>\n </label>\n <label class=\"wide\"\n >${i18next.t('field.opportunity-description')}\n <textarea .value=${this.form.description} ?disabled=${!editable} @input=${(e: any) => set({ description: e.target.value })}></textarea>\n </label>\n </div>\n </section>\n <section class=\"page-section\">\n <header class=\"page-section-header\"><h3>${i18next.t('text.opportunity-section-stage')}</h3></header>\n <div class=\"form\">\n <label\n >${i18next.t('field.opportunity-stage')}\n <select .value=${this.form.stage} ?disabled=${this.busy || this.opportunity?.stage === 'WON'} @change=${(e: any) => this.chooseStage(e.target.value)}>\n ${stageChoices.map(one => html`<option value=${one.stage} ?selected=${one.stage === this.form.stage} ?disabled=${!one.open}>${opportunityStageWord(one.stage)}</option>`)}\n </select>\n </label>\n <label\n >${i18next.t('field.opportunity-probability')}\n <input type=\"number\" min=\"0\" max=\"100\" step=\"1\" .value=${this.form.probability} ?disabled=${!editable} @input=${(e: any) => set({ probability: e.target.value })}\n /></label>\n ${this.opportunity?.closedOn\n ? html`<label>${i18next.t('field.opportunity-closed-on')} <strong>${this.opportunity.closedOn}</strong></label>`\n : nothing}\n </div>\n </section>\n ${this.opportunity && this.isOpen(this.opportunity.stage) ? this.renderLose() : nothing}\n ${this.opportunity?.lostReason\n ? html`<section class=\"page-section\">\n <header class=\"page-section-header\"><h3>${i18next.t('field.opportunity-lost-reason')}</h3></header>\n <p>${this.opportunity.lostReason}</p>\n </section>`\n : nothing}\n ${attached && this.opportunity\n ? html`<section class=\"page-section\">\n <header class=\"page-section-header\"><h3>${attached.heading()}</h3></header>\n ${attached.render(this.opportunity)}\n </section>`\n : nothing}\n ${this.events.length ? this.renderEvents() : nothing}\n </div>\n </ox-page-state>`\n }\n\n /** A counterparty of the domain, found by name or business number. */\n private renderParty(which: 'counterparty' | 'endCustomer', label: string, editable: boolean) {\n const picker = this.pickers[which]\n const chosen = this.form[which]\n return html`<label\n >${label}\n <span class=\"party\">\n <input\n .value=${picker.search}\n placeholder=${chosen?.name ?? i18next.t('text.opportunity-counterparty-search')}\n ?disabled=${!editable}\n @input=${(e: any) => this.setPicker(which, { search: e.target.value })}\n @keydown=${(e: KeyboardEvent) => e.key === 'Enter' && this.findParties(which)}\n />\n <button ?disabled=${!editable} @click=${() => this.findParties(which)}>${i18next.t('button.search')}</button>\n ${which === 'endCustomer' && chosen ? html`<button ?disabled=${!editable} @click=${() => (this.form = { ...this.form, endCustomer: null })}>${i18next.t('button.opportunity-clear')}</button>` : nothing}\n </span>\n ${picker.found.length\n ? html`<select ?disabled=${!editable} @change=${(e: any) => this.chooseParty(which, picker.found.find(one => one.id === e.target.value) ?? null)}>\n <option value=\"\">${chosen?.name ?? ''}</option>\n ${picker.found.map(one => html`<option value=${one.id}>${one.name}${one.businessNumber ? ` (${one.businessNumber})` : ''}</option>`)}\n </select>`\n : nothing}\n </label>`\n }\n\n private renderLose() {\n return html`<section class=\"page-section\">\n <header class=\"page-section-header\">\n <h3>${i18next.t('text.opportunity-section-lose')}</h3>\n <div class=\"page-section-actions\">\n <button ?disabled=${this.busy || !this.reason.trim()} @click=${() => this.lose()}>${i18next.t('button.opportunity-lose')}</button>\n </div>\n </header>\n <div class=\"form\">\n <label class=\"wide\"\n >${i18next.t('field.opportunity-lost-reason')}\n <input .value=${this.reason} ?disabled=${this.busy} @input=${(e: any) => (this.reason = e.target.value)}\n /></label>\n </div>\n </section>`\n }\n\n private renderEvents() {\n return html`<section class=\"page-section\">\n <header class=\"page-section-header\"><h3>${i18next.t('text.opportunity-section-history')}</h3></header>\n <table>\n <thead>\n <tr>\n <th>${i18next.t('field.opportunity-event-at')}</th>\n <th>${i18next.t('field.opportunity-event-actor')}</th>\n <th>${i18next.t('field.opportunity-event-action')}</th>\n <th>${i18next.t('field.opportunity-stage')}</th>\n <th>${i18next.t('field.opportunity-probability')}</th>\n </tr>\n </thead>\n <tbody>\n ${this.events.map(\n one => html`<tr>\n <td>${one.createdAt ? new Date(one.createdAt).toLocaleString() : ''}</td>\n <td>${one.actor?.name ?? ''}</td>\n <td>${eventWord(one.action)}${one.reason ? ` · ${one.reason}` : ''}</td>\n <td>${one.fromStage ? `${opportunityStageWord(one.fromStage)} → ` : ''}${opportunityStageWord(one.stage)}</td>\n <td class=\"number\">${one.probability}%</td>\n </tr>`\n )}\n </tbody>\n </table>\n </section>`\n }\n\n private setPicker(which: 'counterparty' | 'endCustomer', patch: Partial<Picker>) {\n this.pickers = { ...this.pickers, [which]: { ...this.pickers[which], ...patch } }\n }\n\n private chooseParty(which: 'counterparty' | 'endCustomer', party: Party | null) {\n if (!party) return\n this.form = { ...this.form, [which]: party }\n this.setPicker(which, { search: '', found: [] })\n }\n\n /** The stage's own probability comes in with it; the person may change it afterwards. */\n private chooseStage(stage: string) {\n const given = this.stages.find(one => one.stage === stage)\n this.form = { ...this.form, stage, probability: given ? String(given.probability) : this.form.probability }\n }\n\n private async readChoices() {\n try {\n const response = await client.query({\n query: gql`\n query {\n opportunityStages { stage probability open }\n opportunityOwners { id name email }\n }\n `,\n fetchPolicy: 'no-cache'\n })\n if (response.errors?.length) throw response\n this.stages = response.data.opportunityStages\n this.owners = response.data.opportunityOwners\n } catch (error) {\n this.error = error\n }\n }\n\n private async findParties(which: 'counterparty' | 'endCustomer') {\n await this.run(async () => {\n const response = await client.query({\n query: gql`\n query ($search: String, $pagination: Pagination) {\n counterparties(search: $search, pagination: $pagination) {\n items { id name businessNumber }\n }\n }\n `,\n variables: { search: this.pickers[which].search.trim() || undefined, pagination: { page: 1, limit: 30 } },\n fetchPolicy: 'no-cache'\n })\n if (response.errors?.length) throw response\n const found: Party[] = response.data.counterparties.items ?? []\n if (found.length === 1) this.chooseParty(which, found[0])\n else this.setPicker(which, { found })\n })\n }\n\n private async load(id: string) {\n this.loading = true\n this.error = null\n try {\n const response = await client.query({\n query: gql(`query ($id: String!) { opportunity(id: $id) { ${FIELDS} } opportunityEvents(opportunityId: $id) { id action fromStage stage probability reason createdAt actor { id name } } }`),\n variables: { id },\n fetchPolicy: 'no-cache'\n })\n if (response.errors?.length) throw response\n if (!response.data.opportunity) throw new Error(i18next.t('text.opportunity-not-found'))\n this.show(response.data.opportunity, response.data.opportunityEvents)\n } catch (error) {\n this.error = error\n } finally {\n this.loading = false\n this.updateContext()\n }\n }\n\n private show(opportunity: any, events: any[]) {\n this.opportunity = opportunity\n this.events = events\n this.form = {\n name: opportunity.name,\n counterparty: opportunity.counterparty,\n endCustomer: opportunity.endCustomer,\n amount: String(opportunity.amount ?? 0),\n currency: opportunity.currency,\n expectedCloseMonth: opportunity.expectedCloseMonth,\n stage: opportunity.stage,\n probability: String(opportunity.probability),\n ownerId: opportunity.ownerId ?? '',\n description: opportunity.description ?? ''\n }\n this.reason = ''\n }\n\n private async run<T>(work: () => Promise<T>): Promise<T | undefined> {\n if (this.busy) return undefined\n this.busy = true\n try {\n return await work()\n } catch (error: any) {\n notify({ level: 'error', message: error?.graphQLErrors?.[0]?.message ?? error?.errors?.[0]?.message ?? error?.message ?? String(error) })\n return undefined\n } finally {\n this.busy = false\n }\n }\n\n private input() {\n const { name, counterparty, endCustomer, amount, currency, expectedCloseMonth, stage, probability, ownerId, description } = this.form\n return {\n name,\n counterpartyId: counterparty?.id ?? '',\n endCustomerId: endCustomer?.id ?? null,\n amount: Number(amount || 0),\n currency,\n expectedCloseMonth,\n stage,\n probability: probability === '' ? null : Number(probability),\n ownerId: ownerId || null,\n description\n }\n }\n\n private async save() {\n const id = this.opportunity?.id\n const saved = await this.run(async () => {\n const response = await client.mutate({\n mutation: gql(\n id\n ? `mutation ($id: String!, $opportunity: NewOpportunity!) { opportunity: updateOpportunity(id: $id, opportunity: $opportunity) { id } }`\n : `mutation ($opportunity: NewOpportunity!) { opportunity: createOpportunity(opportunity: $opportunity) { id } }`\n ),\n variables: { ...(id ? { id } : {}), opportunity: this.input() }\n })\n if (response.errors?.length) throw response\n return response.data.opportunity as { id: string }\n })\n if (!saved) return\n if (!id) navigate(`opportunity-editor?id=${saved.id}`)\n else await this.load(id)\n }\n\n private async lose() {\n const id = this.opportunity!.id\n const lost = await this.run(async () => {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $reason: String!) {\n loseOpportunity(id: $id, reason: $reason) { id }\n }\n `,\n variables: { id, reason: this.reason.trim() }\n })\n if (response.errors?.length) throw response\n return response.data.loseOpportunity\n })\n if (lost) await this.load(id)\n }\n}\n\nconst eventWord = (action?: string) =>\n action === 'CREATED'\n ? i18next.t('text.opportunity-event-created')\n : action === 'STAGED'\n ? i18next.t('text.opportunity-event-staged')\n : action === 'WON'\n ? i18next.t('text.opportunity-event-won')\n : action === 'LOST'\n ? i18next.t('text.opportunity-event-lost')\n : i18next.t('text.opportunity-event-updated')\n\nfunction blank(stages: { stage: string; probability: number }[] = []): Form {\n const lead = stages.find(one => one.stage === 'LEAD')\n return {\n name: '',\n counterparty: null,\n endCustomer: null,\n amount: '0',\n currency: 'KRW',\n expectedCloseMonth: thisMonth(),\n stage: 'LEAD',\n probability: lead ? String(lead.probability) : '',\n ownerId: '',\n description: ''\n }\n}\n"]}
@@ -0,0 +1,40 @@
1
+ import '@operato/data-grist/ox-grist.js';
2
+ import '@operato/data-grist/ox-filters-form.js';
3
+ import { FetchOption } from '@operato/data-grist';
4
+ import { PageView } from '@operato/shell';
5
+ declare const OpportunityList_base: (new (...args: any[]) => import("lit").LitElement) & typeof PageView;
6
+ /** 영업 기회 — every opportunity of the domain, narrowed by stage. The name opens its 360; the edit gutter its editor. */
7
+ export declare class OpportunityList extends OpportunityList_base {
8
+ static styles: import("lit").CSSResult[];
9
+ gristConfig: any;
10
+ private grist;
11
+ get context(): {
12
+ title: string;
13
+ search: {
14
+ handler: (search: string) => void;
15
+ value: string;
16
+ };
17
+ filter: {
18
+ handler: () => void;
19
+ };
20
+ actions: {
21
+ title: string;
22
+ icon: string;
23
+ emphasis: {
24
+ raised: boolean;
25
+ };
26
+ action: () => void;
27
+ }[];
28
+ exportable: {
29
+ name: string;
30
+ data: () => any[];
31
+ };
32
+ };
33
+ render(): import("lit-html").TemplateResult<1>;
34
+ pageInitialized(): Promise<void>;
35
+ fetchHandler({ page, limit, sortings, filters, search }: FetchOption): Promise<{
36
+ total: any;
37
+ records: any;
38
+ }>;
39
+ }
40
+ export {};