@things-factory/sales 10.1.258 → 10.1.260

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.
@@ -0,0 +1,23 @@
1
+ export type GridFilter = {
2
+ name: string;
3
+ operator: string;
4
+ value: any;
5
+ };
6
+ /** 상품: the 사용 chip (`active` / `inactive`) as the boolean condition the server reads. */
7
+ export declare function salesProductFilters(filters: readonly GridFilter[]): GridFilter[];
8
+ /** The opportunity stages a person still moves an opportunity through - the list opens on them, won and lost left out. */
9
+ export declare const OPEN_STAGES: string[];
10
+ export declare const STAGES: string[];
11
+ /**
12
+ * 영업 기회: the person in charge (the `owner` column, a user id) as `ownerId`, and the expected-close period (the
13
+ * `expectedCloseMonth` column, `this-month` … `this-year`) as the months it covers on `today` (`YYYY-MM-DD`). The stages
14
+ * pass as they are.
15
+ */
16
+ export declare function opportunityFilters(filters: readonly GridFilter[], today: string): GridFilter[];
17
+ /**
18
+ * 견적: the issue period (the `issueDate` column, `this-month` … `last-year`) as the days it covers, by the invoice
19
+ * module's `issuePeriodOf`. The statuses pass as they are.
20
+ */
21
+ export declare function quotationFilters(filters: readonly GridFilter[], periodOf: (period: string) => [string, string] | undefined): GridFilter[];
22
+ /** Every status a quotation version takes, in the order the chips show them. */
23
+ export declare const QUOTATION_STATUSES: string[];
@@ -0,0 +1,58 @@
1
+ import { isFilterCondition } from '@operato/data-grist/filter-condition.js';
2
+ import { closeMonthsOf } from './close-months.js';
3
+ /*
4
+ * A filter is a condition by the grid's own rule (`isFilterCondition`: an operator that takes no value always is, an
5
+ * empty choice is not). Every condition passes on untouched - the conversions below change only the filters they are
6
+ * for, by name, operator and value form together.
7
+ */
8
+ const said = isFilterCondition;
9
+ /** 상품: the 사용 chip (`active` / `inactive`) as the boolean condition the server reads. */
10
+ export function salesProductFilters(filters) {
11
+ return filters
12
+ .filter(said)
13
+ .map(one => one.name === 'active' && one.operator === 'eq' && (one.value === 'active' || one.value === 'inactive')
14
+ ? { name: 'active', operator: one.value === 'active' ? 'is_true' : 'is_not_true', value: null }
15
+ : one);
16
+ }
17
+ /** The opportunity stages a person still moves an opportunity through - the list opens on them, won and lost left out. */
18
+ export const OPEN_STAGES = ['LEAD', 'PROPOSAL', 'QUOTED', 'NEGOTIATION'];
19
+ export const STAGES = [...OPEN_STAGES, 'WON', 'LOST'];
20
+ /**
21
+ * 영업 기회: the person in charge (the `owner` column, a user id) as `ownerId`, and the expected-close period (the
22
+ * `expectedCloseMonth` column, `this-month` … `this-year`) as the months it covers on `today` (`YYYY-MM-DD`). The stages
23
+ * pass as they are.
24
+ */
25
+ export function opportunityFilters(filters, today) {
26
+ return filters.filter(said).flatMap(one => {
27
+ /* The person in charge is asked by id - with the operator the filter has (담당자 제외 stays an exclusion). */
28
+ if (one.name === 'owner') {
29
+ const id = (value) => (value && typeof value === 'object' ? value.id : value);
30
+ return [{ ...one, name: 'ownerId', value: Array.isArray(one.value) ? one.value.map(id) : id(one.value) }];
31
+ }
32
+ /* Only the chosen period (`this-quarter` …) becomes its months; a month or any other condition passes as it is. */
33
+ if (one.name === 'expectedCloseMonth' && one.operator === 'eq' && typeof one.value === 'string') {
34
+ const months = closeMonthsOf(one.value, today);
35
+ if (months)
36
+ return [{ name: 'expectedCloseMonth', operator: 'between', value: months }];
37
+ }
38
+ return [one];
39
+ });
40
+ }
41
+ /**
42
+ * 견적: the issue period (the `issueDate` column, `this-month` … `last-year`) as the days it covers, by the invoice
43
+ * module's `issuePeriodOf`. The statuses pass as they are.
44
+ */
45
+ export function quotationFilters(filters, periodOf) {
46
+ return filters.filter(said).flatMap(one => {
47
+ /* Only the chosen period (`this-month` …) becomes its days; a day itself or another operator passes as it is. */
48
+ if (one.name === 'issueDate' && one.operator === 'eq' && typeof one.value === 'string') {
49
+ const days = periodOf(one.value);
50
+ if (days)
51
+ return [{ name: 'issueDate', operator: 'between', value: days }];
52
+ }
53
+ return [one];
54
+ });
55
+ }
56
+ /** Every status a quotation version takes, in the order the chips show them. */
57
+ export const QUOTATION_STATUSES = ['DRAFT', 'SENT', 'ACCEPTED', 'REJECTED', 'EXPIRED', 'SUPERSEDED'];
58
+ //# sourceMappingURL=list-filters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-filters.js","sourceRoot":"","sources":["../client/list-filters.ts"],"names":[],"mappings":"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","sourcesContent":["import { isFilterCondition } from '@operato/data-grist/filter-condition.js'\n\nimport { closeMonthsOf } from './close-months.js'\n\n/*\n * The sales lists' conditions are the grid's own column filters, kept in the address with the search, sort and page\n * (FOOTER-BUTTON-REVIEW G09). These turn the grid's filter values into the conditions the server's list queries read.\n * Pure: no browser, so the module's tests read them as built.\n */\n\nexport type GridFilter = { name: string; operator: string; value: any }\n\n/*\n * A filter is a condition by the grid's own rule (`isFilterCondition`: an operator that takes no value always is, an\n * empty choice is not). Every condition passes on untouched - the conversions below change only the filters they are\n * for, by name, operator and value form together.\n */\nconst said = isFilterCondition\n\n/** 상품: the 사용 chip (`active` / `inactive`) as the boolean condition the server reads. */\nexport function salesProductFilters(filters: readonly GridFilter[]): GridFilter[] {\n return filters\n .filter(said)\n .map(one =>\n one.name === 'active' && one.operator === 'eq' && (one.value === 'active' || one.value === 'inactive')\n ? { name: 'active', operator: one.value === 'active' ? 'is_true' : 'is_not_true', value: null }\n : one\n )\n}\n\n/** The opportunity stages a person still moves an opportunity through - the list opens on them, won and lost left out. */\nexport const OPEN_STAGES = ['LEAD', 'PROPOSAL', 'QUOTED', 'NEGOTIATION']\nexport const STAGES = [...OPEN_STAGES, 'WON', 'LOST']\n\n/**\n * 영업 기회: the person in charge (the `owner` column, a user id) as `ownerId`, and the expected-close period (the\n * `expectedCloseMonth` column, `this-month` … `this-year`) as the months it covers on `today` (`YYYY-MM-DD`). The stages\n * pass as they are.\n */\nexport function opportunityFilters(filters: readonly GridFilter[], today: string): GridFilter[] {\n return filters.filter(said).flatMap(one => {\n /* The person in charge is asked by id - with the operator the filter has (담당자 제외 stays an exclusion). */\n if (one.name === 'owner') {\n const id = (value: any) => (value && typeof value === 'object' ? value.id : value)\n return [{ ...one, name: 'ownerId', value: Array.isArray(one.value) ? one.value.map(id) : id(one.value) }]\n }\n /* Only the chosen period (`this-quarter` …) becomes its months; a month or any other condition passes as it is. */\n if (one.name === 'expectedCloseMonth' && one.operator === 'eq' && typeof one.value === 'string') {\n const months = closeMonthsOf(one.value, today)\n if (months) return [{ name: 'expectedCloseMonth', operator: 'between', value: months }]\n }\n return [one]\n })\n}\n\n/**\n * 견적: the issue period (the `issueDate` column, `this-month` … `last-year`) as the days it covers, by the invoice\n * module's `issuePeriodOf`. The statuses pass as they are.\n */\nexport function quotationFilters(filters: readonly GridFilter[], periodOf: (period: string) => [string, string] | undefined): GridFilter[] {\n return filters.filter(said).flatMap(one => {\n /* Only the chosen period (`this-month` …) becomes its days; a day itself or another operator passes as it is. */\n if (one.name === 'issueDate' && one.operator === 'eq' && typeof one.value === 'string') {\n const days = periodOf(one.value)\n if (days) return [{ name: 'issueDate', operator: 'between', value: days }]\n }\n return [one]\n })\n}\n\n/** Every status a quotation version takes, in the order the chips show them. */\nexport const QUOTATION_STATUSES = ['DRAFT', 'SENT', 'ACCEPTED', 'REJECTED', 'EXPIRED', 'SUPERSEDED']\n"]}
@@ -1,5 +1,5 @@
1
1
  import '@operato/data-grist/ox-grist.js';
2
- import '@operato/input/ox-input-select-buttons.js';
2
+ import '@operato/data-grist/ox-filters-form.js';
3
3
  import { FetchOption } from '@operato/data-grist';
4
4
  import { PageView } from '@operato/shell';
5
5
  declare const OpportunityList_base: (new (...args: any[]) => import("lit").LitElement) & typeof PageView;
@@ -10,18 +10,14 @@ declare const OpportunityList_base: (new (...args: any[]) => import("lit").LitEl
10
10
  * The person in charge and the expected-close period narrow it from the headroom. The stages are chips that narrow
11
11
  * the list, each with how many opportunities it holds under the other conditions — counted by the server, so the
12
12
  * numbers agree with one another whichever chips are pressed. The list opens on the open stages, won and lost left
13
- * out; with none pressed every stage shows.
13
+ * out; with none pressed every stage shows. All three are the grid's own column filters, so the address keeps them
14
+ * with the search, sort and page, and a change reads page 1 (FOOTER-BUTTON-REVIEW G09).
14
15
  */
15
16
  export declare class OpportunityList extends OpportunityList_base {
16
17
  static styles: import("lit").CSSResult[];
17
18
  gristConfig: any;
18
- private stageCounts;
19
- private stages?;
20
- private owner;
21
- private closePeriod;
22
- private ownerChoices;
23
19
  private grist;
24
- private get owners();
20
+ private owners;
25
21
  get context(): {
26
22
  title: string;
27
23
  search: {
@@ -50,8 +46,11 @@ export declare class OpportunityList extends OpportunityList_base {
50
46
  total: number;
51
47
  records: any[];
52
48
  }>;
53
- private headroomFilters;
54
- private fetchStageCounts;
49
+ countHandler({ filters, search, signal }: FetchOption): Promise<{
50
+ stage: {
51
+ [k: string]: any;
52
+ };
53
+ }>;
55
54
  private fetchPage;
56
55
  }
57
56
  export {};
@@ -1,11 +1,11 @@
1
1
  import { __decorate, __metadata } from "tslib";
2
2
  import '@operato/data-grist/ox-grist.js';
3
- import '@operato/input/ox-input-select-buttons.js';
3
+ import '@operato/data-grist/ox-filters-form.js';
4
4
  import gql from 'graphql-tag';
5
5
  import { html } from 'lit';
6
6
  import { customElement, query, state } from 'lit/decorators.js';
7
7
  import { DataGrist } from '@operato/data-grist';
8
- import { client, QueryController } from '@operato/graphql';
8
+ import { client } from '@operato/graphql';
9
9
  import { i18next, localize } from '@operato/i18n';
10
10
  import { entity360Name } from '@operato/layout';
11
11
  import { navigate, PageView } from '@operato/shell';
@@ -13,8 +13,7 @@ import { CommonGristStyles, CommonHeaderStyles, ScrollbarStyles, WorkSurfacePage
13
13
  import { isMobileDevice } from '@operato/utils';
14
14
  import { SELECTION_ROW_PICKER } from '@things-factory/ai-assistant/dist-client/index.js';
15
15
  import { documentAmount } from '@things-factory/invoice/dist-client';
16
- import { closeMonthsOf } from '../close-months';
17
- import { headroomChoice } from '../headroom-choice';
16
+ import { OPEN_STAGES, opportunityFilters, STAGES } from '../list-filters';
18
17
  import { opportunityStageWord } from '../opportunity-words';
19
18
  const today = () => new Date().toLocaleDateString('en-CA');
20
19
  /**
@@ -24,21 +23,15 @@ const today = () => new Date().toLocaleDateString('en-CA');
24
23
  * The person in charge and the expected-close period narrow it from the headroom. The stages are chips that narrow
25
24
  * the list, each with how many opportunities it holds under the other conditions — counted by the server, so the
26
25
  * numbers agree with one another whichever chips are pressed. The list opens on the open stages, won and lost left
27
- * out; with none pressed every stage shows.
26
+ * out; with none pressed every stage shows. All three are the grid's own column filters, so the address keeps them
27
+ * with the search, sort and page, and a change reads page 1 (FOOTER-BUTTON-REVIEW G09).
28
28
  */
29
29
  let OpportunityList = class OpportunityList extends localize(i18next)(PageView) {
30
- constructor() {
31
- super(...arguments);
32
- this.stageCounts = [];
33
- /* The person in charge narrowed to (a user id), and the expected-close period. */
34
- this.owner = '';
35
- this.closePeriod = '';
36
- /* The people an opportunity may be put in the charge of - the editor's choices (ADR-0190). */
37
- this.ownerChoices = new QueryController(this, gql `{ opportunityOwners { id name } }`);
38
- }
39
30
  static { this.styles = [WorkSurfacePageStyles, CommonGristStyles, CommonHeaderStyles, ScrollbarStyles]; }
40
- get owners() {
41
- return (this.ownerChoices.data?.opportunityOwners ?? []).map((one) => ({ value: one.id, display: one.name }));
31
+ /* The people an opportunity may be put in the charge of - the editor's choices, read when the filter opens. */
32
+ async owners() {
33
+ const response = await client.query({ query: gql `{ opportunityOwners { id name } }` });
34
+ return (response.data?.opportunityOwners ?? []).map((one) => ({ value: one.id, display: one.name }));
42
35
  }
43
36
  get context() {
44
37
  return {
@@ -68,28 +61,18 @@ let OpportunityList = class OpportunityList extends localize(i18next)(PageView)
68
61
  .labelOf=${(row) => [row.name, row.counterparty?.name].filter(Boolean).join(' · ')}
69
62
  ></ox-assistant-selection-bar>
70
63
  <ox-grist
71
- class="page-fill"
72
- search-mode="term"
73
- .mode=${isMobileDevice() ? 'CARD' : 'GRID'}
74
- .config=${this.gristConfig}
75
- .fetchHandler=${this.fetchHandler.bind(this)}
76
- >
77
- <div slot="headroom" class="header">
78
- <div class="filters">
79
- ${headroomChoice(i18next.t('field.opportunity-owner'), this.owner, [{ value: '', display: i18next.t('text.opportunity-owner-all') }, ...this.owners], value => ((this.owner = value), this.grist.fetch()))}
80
- ${headroomChoice(i18next.t('field.opportunity-expected-close-month'), this.closePeriod, ['', 'this-month', 'next-month', 'this-quarter', 'this-year'].map(value => ({ value, display: i18next.t(`text.opportunity-close-${value || 'all'}`) })), value => ((this.closePeriod = value), this.grist.fetch()))}
81
- <ox-input-select-buttons
82
- multiple
83
- .value=${this.stages ?? []}
84
- .options=${this.stageCounts.map(({ stage, count }) => ({ value: stage, display: opportunityStageWord(stage), count }))}
85
- @change=${(e) => {
86
- this.stages = e.currentTarget.value ?? [];
87
- this.grist.fetch();
88
- }}
89
- ></ox-input-select-buttons>
64
+ class="page-fill"
65
+ search-mode="term"
66
+ .mode=${isMobileDevice() ? 'CARD' : 'GRID'}
67
+ .config=${this.gristConfig}
68
+ .fetchHandler=${this.fetchHandler.bind(this)}
69
+ .countHandler=${this.countHandler.bind(this)}
70
+ ?url-params-sensitive=${this.active}
71
+ >
72
+ <div slot="headroom" class="header">
73
+ <div class="filters"><ox-filters-form without-search></ox-filters-form></div>
90
74
  </div>
91
- </div>
92
- </ox-grist>`;
75
+ </ox-grist>`;
93
76
  }
94
77
  async pageInitialized() {
95
78
  const open = (record) => record?.id && navigate(`opportunity-editor?id=${record.id}`);
@@ -117,52 +100,54 @@ let OpportunityList = class OpportunityList extends localize(i18next)(PageView)
117
100
  }),
118
101
  readonly('string', 'counterparty', i18next.t('field.opportunity-counterparty'), 180, { sortable: false, record: { renderer: (value) => party(value) } }),
119
102
  readonly('string', 'endCustomer', i18next.t('field.opportunity-end-customer'), 160, { sortable: false, record: { renderer: (value) => party(value) } }),
120
- readonly('string', 'stage', i18next.t('field.opportunity-stage'), 90, { record: { renderer: (value) => opportunityStageWord(value) } }),
103
+ readonly('string', 'stage', i18next.t('field.opportunity-stage'), 90, {
104
+ record: { renderer: (value) => opportunityStageWord(value) },
105
+ /* Chips with their counts; the list opens on the open stages, and none pressed is every stage. */
106
+ filter: { type: 'select-buttons', operator: 'in', value: OPEN_STAGES, options: STAGES.map(stage => ({ value: stage, display: opportunityStageWord(stage) })) }
107
+ }),
121
108
  readonly('integer', 'probability', i18next.t('field.opportunity-probability'), 70, { record: { align: 'right', renderer: (value) => `${value ?? 0}%` } }),
122
109
  readonly('float', 'amount', i18next.t('field.opportunity-amount'), 150, {
123
110
  record: { align: 'right', renderer: (value, _c, record) => documentAmount(value, record?.currency || 'KRW') }
124
111
  }),
125
- readonly('string', 'expectedCloseMonth', i18next.t('field.opportunity-expected-close-month'), 100),
126
- readonly('string', 'owner', i18next.t('field.opportunity-owner'), 110, { sortable: false, record: { renderer: (value) => value?.name ?? '' } })
112
+ readonly('string', 'expectedCloseMonth', i18next.t('field.opportunity-expected-close-month'), 100, {
113
+ /* A period named relative to today, so a shared address reads the same period on another day's terms. */
114
+ filter: {
115
+ type: 'select',
116
+ operator: 'eq',
117
+ options: ['', 'this-month', 'next-month', 'this-quarter', 'this-year'].map(value => ({ value, display: i18next.t(`text.opportunity-close-${value || 'all'}`) }))
118
+ }
119
+ }),
120
+ readonly('string', 'owner', i18next.t('field.opportunity-owner'), 110, {
121
+ sortable: false,
122
+ record: { renderer: (value) => value?.name ?? '' },
123
+ filter: { type: 'select', operator: 'eq', options: async () => [{ value: '', display: i18next.t('text.opportunity-owner-all') }, ...(await this.owners())] }
124
+ })
127
125
  ],
128
126
  sorters: [{ name: 'expectedCloseMonth' }]
129
127
  };
130
128
  }
131
129
  async fetchHandler({ page = 1, limit = 50, sortings = [], filters = [], search }) {
132
- search = search?.trim() || undefined;
133
- filters = [...filters, ...this.headroomFilters()];
134
- const counting = this.fetchStageCounts(filters, search);
135
- if (!this.stages)
136
- this.stages = (await counting).filter(one => one.open).map(one => one.stage);
137
- const stages = this.stages.length ? [{ name: 'stage', operator: 'in', value: this.stages }] : [];
138
- const [response] = await Promise.all([this.fetchPage([...filters, ...stages], { page, limit }, sortings, search), counting]);
130
+ const response = await this.fetchPage(opportunityFilters(filters, today()), { page, limit }, sortings, search?.trim() || undefined);
139
131
  return { total: response.total, records: response.items };
140
132
  }
141
- /* The owner and the close period as the list's filters - the stage counts are under them too. */
142
- headroomFilters() {
143
- const months = closeMonthsOf(this.closePeriod, today());
144
- return [
145
- ...(this.owner ? [{ name: 'ownerId', operator: 'eq', value: this.owner }] : []),
146
- ...(months ? [{ name: 'expectedCloseMonth', operator: 'between', value: months }] : [])
147
- ];
148
- }
149
- async fetchStageCounts(filters, search) {
133
+ /* The stage chips' numbers under the list's other conditions - the chips' own left out - counted by the server. */
134
+ async countHandler({ filters = [], search, signal }) {
150
135
  const response = await client.query({
151
136
  query: gql `
152
137
  query ($filters: [Filter!], $search: String) {
153
138
  opportunityStageCounts(filters: $filters, search: $search) {
154
139
  stage
155
140
  count
156
- open
157
141
  }
158
142
  }
159
143
  `,
160
- variables: { filters, search },
161
- fetchPolicy: 'network-only'
144
+ variables: { filters: opportunityFilters(filters, today()).filter(one => one.name !== 'stage'), search: search?.trim() || undefined },
145
+ fetchPolicy: 'no-cache',
146
+ context: { fetchOptions: { signal }, queryDeduplication: false }
162
147
  });
163
148
  if (response.errors?.length)
164
- throw response;
165
- return (this.stageCounts = response.data.opportunityStageCounts);
149
+ throw response.errors[0];
150
+ return { stage: Object.fromEntries(response.data.opportunityStageCounts.map((row) => [row.stage, row.count])) };
166
151
  }
167
152
  async fetchPage(filters, pagination, sortings, search) {
168
153
  const response = await client.query({
@@ -197,25 +182,11 @@ __decorate([
197
182
  state(),
198
183
  __metadata("design:type", Object)
199
184
  ], OpportunityList.prototype, "gristConfig", void 0);
200
- __decorate([
201
- state(),
202
- __metadata("design:type", Array)
203
- ], OpportunityList.prototype, "stageCounts", void 0);
204
- __decorate([
205
- state(),
206
- __metadata("design:type", Array)
207
- ], OpportunityList.prototype, "stages", void 0);
208
- __decorate([
209
- state(),
210
- __metadata("design:type", Object)
211
- ], OpportunityList.prototype, "owner", void 0);
212
- __decorate([
213
- state(),
214
- __metadata("design:type", Object)
215
- ], OpportunityList.prototype, "closePeriod", void 0);
216
185
  __decorate([
217
186
  query('ox-grist'),
218
- __metadata("design:type", DataGrist)
187
+ __metadata("design:type", DataGrist
188
+ /* The people an opportunity may be put in the charge of - the editor's choices, read when the filter opens. */
189
+ )
219
190
  ], OpportunityList.prototype, "grist", void 0);
220
191
  OpportunityList = __decorate([
221
192
  customElement('opportunity-list')
@@ -1 +1 @@
1
- {"version":3,"file":"opportunity-list.js","sourceRoot":"","sources":["../../client/pages/opportunity-list.ts"],"names":[],"mappings":"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","sourcesContent":["import '@operato/data-grist/ox-grist.js'\nimport '@operato/input/ox-input-select-buttons.js'\n\nimport gql from 'graphql-tag'\nimport { html } from 'lit'\nimport { customElement, query, state } from 'lit/decorators.js'\n\nimport { DataGrist, FetchOption } from '@operato/data-grist'\nimport { client, QueryController } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { entity360Name } from '@operato/layout'\nimport { navigate, PageView } from '@operato/shell'\nimport { CommonGristStyles, CommonHeaderStyles, ScrollbarStyles, WorkSurfacePageStyles } from '@operato/styles'\nimport { isMobileDevice } from '@operato/utils'\nimport { SELECTION_ROW_PICKER } from '@things-factory/ai-assistant/dist-client/index.js'\nimport { documentAmount } from '@things-factory/invoice/dist-client'\n\nimport { closeMonthsOf } from '../close-months'\nimport { headroomChoice } from '../headroom-choice'\nimport { opportunityStageWord } from '../opportunity-words'\n\nconst today = () => new Date().toLocaleDateString('en-CA')\n\ntype StageCount = { stage: string; count: number; open: boolean }\n\n/**\n * 영업 기회 — every opportunity of the domain, narrowed by stage. The name opens its 360; the edit gutter its editor. Rows\n * picked go to the assistant (「AI와 작업」) as `opportunity:<uuid>`, read by the opportunity list's read (sales-read.ts).\n *\n * The person in charge and the expected-close period narrow it from the headroom. The stages are chips that narrow\n * the list, each with how many opportunities it holds under the other conditions — counted by the server, so the\n * numbers agree with one another whichever chips are pressed. The list opens on the open stages, won and lost left\n * out; with none pressed every stage shows.\n */\n@customElement('opportunity-list')\nexport class OpportunityList extends localize(i18next)(PageView) {\n static styles = [WorkSurfacePageStyles, CommonGristStyles, CommonHeaderStyles, ScrollbarStyles]\n\n @state() gristConfig: any\n @state() private stageCounts: StageCount[] = []\n /* The stages chosen; none until the first counts say which stages are open. */\n @state() private stages?: string[]\n /* The person in charge narrowed to (a user id), and the expected-close period. */\n @state() private owner = ''\n @state() private closePeriod = ''\n /* The people an opportunity may be put in the charge of - the editor's choices (ADR-0190). */\n private ownerChoices = new QueryController(this, gql`{ opportunityOwners { id name } }`)\n @query('ox-grist') private grist!: DataGrist\n\n private get owners(): { value: string; display: string }[] {\n return (this.ownerChoices.data?.opportunityOwners ?? []).map((one: any) => ({ value: one.id, display: one.name }))\n }\n\n get context() {\n return {\n title: i18next.t('title.opportunity-list'),\n search: {\n handler: (search: string) => {\n this.grist.searchText = search\n },\n value: this.grist?.searchText || ''\n },\n /* Narrowing by stage stays in the headroom, and this folds it. */\n filter: {\n handler: () => {\n this.grist.toggleHeadroom()\n }\n },\n actions: [{ title: i18next.t('button.opportunity-new'), icon: 'add', emphasis: { raised: true }, action: () => navigate('opportunity-editor') }],\n exportable: { name: i18next.t('title.opportunity-list'), data: () => this.grist.exportRecords() }\n }\n }\n\n render() {\n return html`<ox-assistant-selection-bar\n .gridOf=${() => this.grist}\n name=${i18next.t('title.opportunity-list')}\n list=\"opportunities\"\n axis=\"opportunity\"\n .labelOf=${(row: any) => [row.name, row.counterparty?.name].filter(Boolean).join(' · ')}\n ></ox-assistant-selection-bar>\n <ox-grist\n class=\"page-fill\"\n search-mode=\"term\"\n .mode=${isMobileDevice() ? 'CARD' : 'GRID'}\n .config=${this.gristConfig}\n .fetchHandler=${this.fetchHandler.bind(this)}\n >\n <div slot=\"headroom\" class=\"header\">\n <div class=\"filters\">\n ${headroomChoice(\n i18next.t('field.opportunity-owner'),\n this.owner,\n [{ value: '', display: i18next.t('text.opportunity-owner-all') }, ...this.owners],\n value => ((this.owner = value), this.grist.fetch())\n )}\n ${headroomChoice(\n i18next.t('field.opportunity-expected-close-month'),\n this.closePeriod,\n ['', 'this-month', 'next-month', 'this-quarter', 'this-year'].map(value => ({ value, display: i18next.t(`text.opportunity-close-${value || 'all'}`) })),\n value => ((this.closePeriod = value), this.grist.fetch())\n )}\n <ox-input-select-buttons\n multiple\n .value=${this.stages ?? []}\n .options=${this.stageCounts.map(({ stage, count }) => ({ value: stage, display: opportunityStageWord(stage), count }))}\n @change=${(e: CustomEvent) => {\n this.stages = (e.currentTarget as any).value ?? []\n this.grist.fetch()\n }}\n ></ox-input-select-buttons>\n </div>\n </div>\n </ox-grist>`\n }\n\n async pageInitialized() {\n const open = (record: any) => record?.id && navigate(`opportunity-editor?id=${record.id}`)\n const party = (value?: { id: string; name: string }) => (value ? entity360Name({ __typename: 'Counterparty', ...value }, value.name) : '')\n const readonly = (type: string, name: string, header: string, width: number, extra: any = {}) => ({\n type,\n name,\n header,\n width,\n sortable: true,\n ...extra,\n record: { editable: false, ...extra.record }\n })\n this.gristConfig = {\n rows: { appendable: false, selectable: { multiple: true } },\n pagination: { limit: 50, pages: [50, 100, 500] },\n emptyNote: { title: i18next.t('text.opportunity-empty'), description: i18next.t('text.opportunity-empty-description') },\n list: { fields: ['name', 'counterparty'], details: ['stage', 'expectedCloseMonth', 'amount'] },\n columns: [\n { type: 'gutter', gutterName: 'sequence' },\n SELECTION_ROW_PICKER,\n { type: 'gutter', gutterName: 'button', icon: 'edit_note', handlers: { click: (_c: any, _d: any, _col: any, record: any) => open(record) } },\n readonly('string', 'name', i18next.t('field.opportunity-name'), 220, {\n record: { renderer: (_v: any, _c: any, record: any) => (record?.id ? entity360Name({ __typename: 'Opportunity', ...record }, record.name) : '') }\n }),\n readonly('string', 'counterparty', i18next.t('field.opportunity-counterparty'), 180, { sortable: false, record: { renderer: (value: any) => party(value) } }),\n readonly('string', 'endCustomer', i18next.t('field.opportunity-end-customer'), 160, { sortable: false, record: { renderer: (value: any) => party(value) } }),\n readonly('string', 'stage', i18next.t('field.opportunity-stage'), 90, { record: { renderer: (value: string) => opportunityStageWord(value) } }),\n readonly('integer', 'probability', i18next.t('field.opportunity-probability'), 70, { record: { align: 'right', renderer: (value: number) => `${value ?? 0}%` } }),\n readonly('float', 'amount', i18next.t('field.opportunity-amount'), 150, {\n record: { align: 'right', renderer: (value: number, _c: any, record: any) => documentAmount(value, record?.currency || 'KRW') }\n }),\n readonly('string', 'expectedCloseMonth', i18next.t('field.opportunity-expected-close-month'), 100),\n readonly('string', 'owner', i18next.t('field.opportunity-owner'), 110, { sortable: false, record: { renderer: (value: any) => value?.name ?? '' } })\n ],\n sorters: [{ name: 'expectedCloseMonth' }]\n }\n }\n\n async fetchHandler({ page = 1, limit = 50, sortings = [], filters = [], search }: FetchOption) {\n search = search?.trim() || undefined\n filters = [...filters, ...this.headroomFilters()]\n const counting = this.fetchStageCounts(filters, search)\n if (!this.stages) this.stages = (await counting).filter(one => one.open).map(one => one.stage)\n const stages = this.stages.length ? [{ name: 'stage', operator: 'in', value: this.stages }] : []\n const [response] = await Promise.all([this.fetchPage([...filters, ...stages], { page, limit }, sortings, search), counting])\n return { total: response.total, records: response.items }\n }\n\n /* The owner and the close period as the list's filters - the stage counts are under them too. */\n private headroomFilters(): any[] {\n const months = closeMonthsOf(this.closePeriod, today())\n return [\n ...(this.owner ? [{ name: 'ownerId', operator: 'eq', value: this.owner }] : []),\n ...(months ? [{ name: 'expectedCloseMonth', operator: 'between', value: months }] : [])\n ]\n }\n\n private async fetchStageCounts(filters: any[], search?: string) {\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $search: String) {\n opportunityStageCounts(filters: $filters, search: $search) {\n stage\n count\n open\n }\n }\n `,\n variables: { filters, search },\n fetchPolicy: 'network-only'\n })\n if (response.errors?.length) throw response\n return (this.stageCounts = response.data.opportunityStageCounts as StageCount[])\n }\n\n private async fetchPage(filters: any[], pagination: { page: number; limit: number }, sortings: any[], search?: string) {\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!], $search: String) {\n responses: opportunities(filters: $filters, pagination: $pagination, sortings: $sortings, search: $search) {\n items {\n id\n name\n stage\n probability\n amount\n currency\n expectedCloseMonth\n counterparty { id name }\n endCustomer { id name }\n owner { id name }\n }\n total\n }\n }\n `,\n variables: { filters, pagination, sortings, search },\n fetchPolicy: 'network-only'\n })\n if (response.errors?.length) throw response\n return response.data.responses as { items: any[]; total: number }\n }\n}\n"]}
1
+ {"version":3,"file":"opportunity-list.js","sourceRoot":"","sources":["../../client/pages/opportunity-list.ts"],"names":[],"mappings":"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","sourcesContent":["import '@operato/data-grist/ox-grist.js'\nimport '@operato/data-grist/ox-filters-form.js'\n\nimport gql from 'graphql-tag'\nimport { html } from 'lit'\nimport { customElement, query, state } from 'lit/decorators.js'\n\nimport { DataGrist, FetchOption } from '@operato/data-grist'\nimport { client } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { entity360Name } from '@operato/layout'\nimport { navigate, PageView } from '@operato/shell'\nimport { CommonGristStyles, CommonHeaderStyles, ScrollbarStyles, WorkSurfacePageStyles } from '@operato/styles'\nimport { isMobileDevice } from '@operato/utils'\nimport { SELECTION_ROW_PICKER } from '@things-factory/ai-assistant/dist-client/index.js'\nimport { documentAmount } from '@things-factory/invoice/dist-client'\n\nimport { OPEN_STAGES, opportunityFilters, STAGES } from '../list-filters'\nimport { opportunityStageWord } from '../opportunity-words'\n\nconst today = () => new Date().toLocaleDateString('en-CA')\n\n/**\n * 영업 기회 — every opportunity of the domain, narrowed by stage. The name opens its 360; the edit gutter its editor. Rows\n * picked go to the assistant (「AI와 작업」) as `opportunity:<uuid>`, read by the opportunity list's read (sales-read.ts).\n *\n * The person in charge and the expected-close period narrow it from the headroom. The stages are chips that narrow\n * the list, each with how many opportunities it holds under the other conditions — counted by the server, so the\n * numbers agree with one another whichever chips are pressed. The list opens on the open stages, won and lost left\n * out; with none pressed every stage shows. All three are the grid's own column filters, so the address keeps them\n * with the search, sort and page, and a change reads page 1 (FOOTER-BUTTON-REVIEW G09).\n */\n@customElement('opportunity-list')\nexport class OpportunityList extends localize(i18next)(PageView) {\n static styles = [WorkSurfacePageStyles, CommonGristStyles, CommonHeaderStyles, ScrollbarStyles]\n\n @state() gristConfig: any\n @query('ox-grist') private grist!: DataGrist\n\n /* The people an opportunity may be put in the charge of - the editor's choices, read when the filter opens. */\n private async owners(): Promise<{ value: string; display: string }[]> {\n const response = await client.query({ query: gql`{ opportunityOwners { id name } }` })\n return (response.data?.opportunityOwners ?? []).map((one: any) => ({ value: one.id, display: one.name }))\n }\n\n get context() {\n return {\n title: i18next.t('title.opportunity-list'),\n search: {\n handler: (search: string) => {\n this.grist.searchText = search\n },\n value: this.grist?.searchText || ''\n },\n /* Narrowing by stage stays in the headroom, and this folds it. */\n filter: {\n handler: () => {\n this.grist.toggleHeadroom()\n }\n },\n actions: [{ title: i18next.t('button.opportunity-new'), icon: 'add', emphasis: { raised: true }, action: () => navigate('opportunity-editor') }],\n exportable: { name: i18next.t('title.opportunity-list'), data: () => this.grist.exportRecords() }\n }\n }\n\n render() {\n return html`<ox-assistant-selection-bar\n .gridOf=${() => this.grist}\n name=${i18next.t('title.opportunity-list')}\n list=\"opportunities\"\n axis=\"opportunity\"\n .labelOf=${(row: any) => [row.name, row.counterparty?.name].filter(Boolean).join(' · ')}\n ></ox-assistant-selection-bar>\n <ox-grist\n class=\"page-fill\"\n search-mode=\"term\"\n .mode=${isMobileDevice() ? 'CARD' : 'GRID'}\n .config=${this.gristConfig}\n .fetchHandler=${this.fetchHandler.bind(this)}\n .countHandler=${this.countHandler.bind(this)}\n ?url-params-sensitive=${this.active}\n >\n <div slot=\"headroom\" class=\"header\">\n <div class=\"filters\"><ox-filters-form without-search></ox-filters-form></div>\n </div>\n </ox-grist>`\n }\n\n async pageInitialized() {\n const open = (record: any) => record?.id && navigate(`opportunity-editor?id=${record.id}`)\n const party = (value?: { id: string; name: string }) => (value ? entity360Name({ __typename: 'Counterparty', ...value }, value.name) : '')\n const readonly = (type: string, name: string, header: string, width: number, extra: any = {}) => ({\n type,\n name,\n header,\n width,\n sortable: true,\n ...extra,\n record: { editable: false, ...extra.record }\n })\n this.gristConfig = {\n rows: { appendable: false, selectable: { multiple: true } },\n pagination: { limit: 50, pages: [50, 100, 500] },\n emptyNote: { title: i18next.t('text.opportunity-empty'), description: i18next.t('text.opportunity-empty-description') },\n list: { fields: ['name', 'counterparty'], details: ['stage', 'expectedCloseMonth', 'amount'] },\n columns: [\n { type: 'gutter', gutterName: 'sequence' },\n SELECTION_ROW_PICKER,\n { type: 'gutter', gutterName: 'button', icon: 'edit_note', handlers: { click: (_c: any, _d: any, _col: any, record: any) => open(record) } },\n readonly('string', 'name', i18next.t('field.opportunity-name'), 220, {\n record: { renderer: (_v: any, _c: any, record: any) => (record?.id ? entity360Name({ __typename: 'Opportunity', ...record }, record.name) : '') }\n }),\n readonly('string', 'counterparty', i18next.t('field.opportunity-counterparty'), 180, { sortable: false, record: { renderer: (value: any) => party(value) } }),\n readonly('string', 'endCustomer', i18next.t('field.opportunity-end-customer'), 160, { sortable: false, record: { renderer: (value: any) => party(value) } }),\n readonly('string', 'stage', i18next.t('field.opportunity-stage'), 90, {\n record: { renderer: (value: string) => opportunityStageWord(value) },\n /* Chips with their counts; the list opens on the open stages, and none pressed is every stage. */\n filter: { type: 'select-buttons', operator: 'in', value: OPEN_STAGES, options: STAGES.map(stage => ({ value: stage, display: opportunityStageWord(stage) })) }\n }),\n readonly('integer', 'probability', i18next.t('field.opportunity-probability'), 70, { record: { align: 'right', renderer: (value: number) => `${value ?? 0}%` } }),\n readonly('float', 'amount', i18next.t('field.opportunity-amount'), 150, {\n record: { align: 'right', renderer: (value: number, _c: any, record: any) => documentAmount(value, record?.currency || 'KRW') }\n }),\n readonly('string', 'expectedCloseMonth', i18next.t('field.opportunity-expected-close-month'), 100, {\n /* A period named relative to today, so a shared address reads the same period on another day's terms. */\n filter: {\n type: 'select',\n operator: 'eq',\n options: ['', 'this-month', 'next-month', 'this-quarter', 'this-year'].map(value => ({ value, display: i18next.t(`text.opportunity-close-${value || 'all'}`) }))\n }\n }),\n readonly('string', 'owner', i18next.t('field.opportunity-owner'), 110, {\n sortable: false,\n record: { renderer: (value: any) => value?.name ?? '' },\n filter: { type: 'select', operator: 'eq', options: async () => [{ value: '', display: i18next.t('text.opportunity-owner-all') }, ...(await this.owners())] }\n })\n ],\n sorters: [{ name: 'expectedCloseMonth' }]\n }\n }\n\n async fetchHandler({ page = 1, limit = 50, sortings = [], filters = [], search }: FetchOption) {\n const response = await this.fetchPage(opportunityFilters(filters, today()), { page, limit }, sortings, search?.trim() || undefined)\n return { total: response.total, records: response.items }\n }\n\n /* The stage chips' numbers under the list's other conditions - the chips' own left out - counted by the server. */\n async countHandler({ filters = [], search, signal }: FetchOption) {\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $search: String) {\n opportunityStageCounts(filters: $filters, search: $search) {\n stage\n count\n }\n }\n `,\n variables: { filters: opportunityFilters(filters, today()).filter(one => one.name !== 'stage'), search: search?.trim() || undefined },\n fetchPolicy: 'no-cache',\n context: { fetchOptions: { signal }, queryDeduplication: false }\n })\n if (response.errors?.length) throw response.errors[0]\n return { stage: Object.fromEntries(response.data.opportunityStageCounts.map((row: { stage: string; count: number }) => [row.stage, row.count])) }\n }\n\n private async fetchPage(filters: any[], pagination: { page: number; limit: number }, sortings: any[], search?: string) {\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!], $search: String) {\n responses: opportunities(filters: $filters, pagination: $pagination, sortings: $sortings, search: $search) {\n items {\n id\n name\n stage\n probability\n amount\n currency\n expectedCloseMonth\n counterparty { id name }\n endCustomer { id name }\n owner { id name }\n }\n total\n }\n }\n `,\n variables: { filters, pagination, sortings, search },\n fetchPolicy: 'network-only'\n })\n if (response.errors?.length) throw response\n return response.data.responses as { items: any[]; total: number }\n }\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import '@operato/data-grist/ox-grist.js';
2
- import '@operato/input/ox-input-select-buttons.js';
2
+ import '@operato/data-grist/ox-filters-form.js';
3
3
  import { FetchOption } from '@operato/data-grist';
4
4
  import { PageView } from '@operato/shell';
5
5
  declare const QuotationList_base: (new (...args: any[]) => import("lit").LitElement) & typeof PageView;
@@ -8,13 +8,12 @@ declare const QuotationList_base: (new (...args: any[]) => import("lit").LitElem
8
8
  * assistant (「AI와 작업」) as `quotation:<uuid>`, read by the quotation list's read (sales-read.ts).
9
9
  *
10
10
  * The headroom narrows it by issue period, as the invoice list does, and by status chips, each with how many quotations
11
- * it holds under the other conditions - counted by the server. With no status pressed every status shows.
11
+ * it holds under the other conditions - counted by the server. With no status pressed every status shows. Both are the
12
+ * grid's own column filters, kept in the address with the search, sort and page (FOOTER-BUTTON-REVIEW G09).
12
13
  */
13
14
  export declare class QuotationList extends QuotationList_base {
14
15
  static styles: import("lit").CSSResult[];
15
16
  gristConfig: any;
16
- private statuses;
17
- private period;
18
17
  private grist;
19
18
  get context(): {
20
19
  title: string;
@@ -1,6 +1,6 @@
1
1
  import { __decorate, __metadata } from "tslib";
2
2
  import '@operato/data-grist/ox-grist.js';
3
- import '@operato/input/ox-input-select-buttons.js';
3
+ import '@operato/data-grist/ox-filters-form.js';
4
4
  import gql from 'graphql-tag';
5
5
  import { html } from 'lit';
6
6
  import { customElement, query, state } from 'lit/decorators.js';
@@ -12,7 +12,7 @@ import { CommonGristStyles, CommonHeaderStyles, ScrollbarStyles, WorkSurfacePage
12
12
  import { isMobileDevice } from '@operato/utils';
13
13
  import { SELECTION_ROW_PICKER } from '@things-factory/ai-assistant/dist-client/index.js';
14
14
  import { counterpartOf, documentAmount, issuePeriodOf } from '@things-factory/invoice/dist-client';
15
- import { headroomChoice } from '../headroom-choice';
15
+ import { QUOTATION_STATUSES, quotationFilters } from '../list-filters';
16
16
  import { quotationStatusWord } from '../quotation-words';
17
17
  const today = () => new Date().toLocaleDateString('en-CA');
18
18
  /**
@@ -20,14 +20,10 @@ const today = () => new Date().toLocaleDateString('en-CA');
20
20
  * assistant (「AI와 작업」) as `quotation:<uuid>`, read by the quotation list's read (sales-read.ts).
21
21
  *
22
22
  * The headroom narrows it by issue period, as the invoice list does, and by status chips, each with how many quotations
23
- * it holds under the other conditions - counted by the server. With no status pressed every status shows.
23
+ * it holds under the other conditions - counted by the server. With no status pressed every status shows. Both are the
24
+ * grid's own column filters, kept in the address with the search, sort and page (FOOTER-BUTTON-REVIEW G09).
24
25
  */
25
26
  let QuotationList = class QuotationList extends localize(i18next)(PageView) {
26
- constructor() {
27
- super(...arguments);
28
- this.statuses = [];
29
- this.period = '';
30
- }
31
27
  static { this.styles = [WorkSurfacePageStyles, CommonGristStyles, CommonHeaderStyles, ScrollbarStyles]; }
32
28
  get context() {
33
29
  return {
@@ -57,29 +53,18 @@ let QuotationList = class QuotationList extends localize(i18next)(PageView) {
57
53
  .labelOf=${(row) => [row.number && `${row.number} v${row.version}`, counterpartOf(row)].filter(Boolean).join(' · ')}
58
54
  ></ox-assistant-selection-bar>
59
55
  <ox-grist
60
- class="page-fill"
61
- search-mode="term"
62
- .mode=${isMobileDevice() ? 'CARD' : 'GRID'}
63
- .config=${this.gristConfig}
64
- .fetchHandler=${this.fetchHandler.bind(this)}
65
- .countHandler=${this.countHandler.bind(this)}
66
- @counts-change=${() => this.requestUpdate()}
67
- >
68
- <div slot="headroom" class="header">
69
- <div class="filters">
70
- ${headroomChoice(i18next.t('field.quotation-issue-date'), this.period, ['', 'this-month', 'last-month', 'this-year', 'last-year'].map(value => ({ value, display: i18next.t(`text.invoice-period-${value || 'all'}`) })), value => ((this.period = value), this.grist.fetch()))}
71
- <ox-input-select-buttons
72
- multiple
73
- .value=${this.statuses}
74
- .options=${Object.entries(this.grist?.counts?.status ?? {}).map(([value, count]) => ({ value, display: quotationStatusWord(value), count }))}
75
- @change=${(e) => {
76
- this.statuses = e.currentTarget.value ?? [];
77
- this.grist.fetch();
78
- }}
79
- ></ox-input-select-buttons>
56
+ class="page-fill"
57
+ search-mode="term"
58
+ .mode=${isMobileDevice() ? 'CARD' : 'GRID'}
59
+ .config=${this.gristConfig}
60
+ .fetchHandler=${this.fetchHandler.bind(this)}
61
+ .countHandler=${this.countHandler.bind(this)}
62
+ ?url-params-sensitive=${this.active}
63
+ >
64
+ <div slot="headroom" class="header">
65
+ <div class="filters"><ox-filters-form without-search></ox-filters-form></div>
80
66
  </div>
81
- </div>
82
- </ox-grist>`;
67
+ </ox-grist>`;
83
68
  }
84
69
  async pageInitialized() {
85
70
  const open = (record) => record?.id && navigate(`quotation-editor?id=${record.id}`);
@@ -104,8 +89,19 @@ let QuotationList = class QuotationList extends localize(i18next)(PageView) {
104
89
  readonly('string', 'counterpart', i18next.t('field.quotation-counterpart'), 200, { sortable: false, record: { renderer: (_v, _c, record) => counterpartOf(record) } }),
105
90
  readonly('string', 'number', i18next.t('field.quotation-number'), 140),
106
91
  readonly('integer', 'version', i18next.t('field.quotation-version'), 60),
107
- readonly('string', 'status', i18next.t('field.quotation-status'), 90, { record: { renderer: (value) => quotationStatusWord(value) } }),
108
- readonly('string', 'issueDate', i18next.t('field.quotation-issue-date'), 110),
92
+ readonly('string', 'status', i18next.t('field.quotation-status'), 90, {
93
+ record: { renderer: (value) => quotationStatusWord(value) },
94
+ /* Chips with their counts; none pressed is every status. */
95
+ filter: { type: 'select-buttons', operator: 'in', options: QUOTATION_STATUSES.map(value => ({ value, display: quotationStatusWord(value) })) }
96
+ }),
97
+ readonly('string', 'issueDate', i18next.t('field.quotation-issue-date'), 110, {
98
+ /* A period named relative to today, as the invoice list narrows by it. */
99
+ filter: {
100
+ type: 'select',
101
+ operator: 'eq',
102
+ options: ['', 'this-month', 'last-month', 'this-year', 'last-year'].map(value => ({ value, display: i18next.t(`text.invoice-period-${value || 'all'}`) }))
103
+ }
104
+ }),
109
105
  readonly('string', 'validUntil', i18next.t('field.quotation-valid-until'), 110),
110
106
  readonly('float', 'totalAmount', i18next.t('field.quotation-total-amount'), 160, {
111
107
  record: { align: 'right', renderer: (value, _c, record) => documentAmount(value, record?.currency || 'KRW') }
@@ -114,12 +110,11 @@ let QuotationList = class QuotationList extends localize(i18next)(PageView) {
114
110
  sorters: [{ name: 'issueDate', desc: true }]
115
111
  };
116
112
  }
117
- /* The list's conditions before the statuses are added: the grid's, the issue period and the search. */
113
+ /* The grid's filters as the server reads them - the issue period as its days - and the search. */
118
114
  conditions(filters = [], search) {
119
- const period = issuePeriodOf(this.period, today());
120
- return { filters: [...filters, ...(period ? [{ name: 'issueDate', operator: 'between', value: period }] : [])], search: search?.trim() || undefined };
115
+ return { filters: quotationFilters(filters, period => issuePeriodOf(period, today())), search: search?.trim() || undefined };
121
116
  }
122
- /* The status chips and their numbers, under the list's other conditions; a chip keeps its number until the next land. */
117
+ /* The status chips' numbers, under the list's other conditions - the chips' own left out; a chip keeps its number until the next land. */
123
118
  async countHandler({ filters, search, signal }) {
124
119
  const response = await client.query({
125
120
  query: gql `
@@ -127,7 +122,7 @@ let QuotationList = class QuotationList extends localize(i18next)(PageView) {
127
122
  quotationStatusCounts(filters: $filters, search: $search) { value count }
128
123
  }
129
124
  `,
130
- variables: this.conditions(filters, search),
125
+ variables: (({ filters: all, search: term }) => ({ filters: all.filter(one => one.name !== 'status'), search: term }))(this.conditions(filters, search)),
131
126
  fetchPolicy: 'no-cache',
132
127
  context: { fetchOptions: { signal }, queryDeduplication: false }
133
128
  });
@@ -136,8 +131,7 @@ let QuotationList = class QuotationList extends localize(i18next)(PageView) {
136
131
  return { status: Object.fromEntries(response.data.quotationStatusCounts.map((row) => [row.value, row.count])) };
137
132
  }
138
133
  async fetchHandler({ page = 1, limit = 50, sortings = [], ...asked }) {
139
- const { filters: others, search } = this.conditions(asked.filters, asked.search);
140
- const filters = this.statuses.length ? [...others, { name: 'status', operator: 'in', value: this.statuses }] : others;
134
+ const { filters, search } = this.conditions(asked.filters, asked.search);
141
135
  const response = await client.query({
142
136
  query: gql `
143
137
  query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!], $search: String) {
@@ -171,14 +165,6 @@ __decorate([
171
165
  state(),
172
166
  __metadata("design:type", Object)
173
167
  ], QuotationList.prototype, "gristConfig", void 0);
174
- __decorate([
175
- state(),
176
- __metadata("design:type", Array)
177
- ], QuotationList.prototype, "statuses", void 0);
178
- __decorate([
179
- state(),
180
- __metadata("design:type", Object)
181
- ], QuotationList.prototype, "period", void 0);
182
168
  __decorate([
183
169
  query('ox-grist'),
184
170
  __metadata("design:type", DataGrist)