@things-factory/saas 0.0.0-stage → 10.1.234

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 (83) hide show
  1. package/README.md +19 -2
  2. package/dist-client/bootstrap.d.ts +1 -0
  3. package/dist-client/bootstrap.js +5 -0
  4. package/dist-client/bootstrap.js.map +1 -0
  5. package/dist-client/components/saas-bill-editor.d.ts +59 -0
  6. package/dist-client/components/saas-bill-editor.js +211 -0
  7. package/dist-client/components/saas-bill-editor.js.map +1 -0
  8. package/dist-client/entity360/saas-account-360.d.ts +12 -0
  9. package/dist-client/entity360/saas-account-360.js +231 -0
  10. package/dist-client/entity360/saas-account-360.js.map +1 -0
  11. package/dist-client/index.d.ts +3 -0
  12. package/dist-client/index.js +4 -0
  13. package/dist-client/index.js.map +1 -0
  14. package/dist-client/pages/saas-account-list.d.ts +57 -0
  15. package/dist-client/pages/saas-account-list.js +175 -0
  16. package/dist-client/pages/saas-account-list.js.map +1 -0
  17. package/dist-client/pages/saas-account-page.d.ts +54 -0
  18. package/dist-client/pages/saas-account-page.js +352 -0
  19. package/dist-client/pages/saas-account-page.js.map +1 -0
  20. package/dist-client/pages/saas-cost-trend.d.ts +23 -0
  21. package/dist-client/pages/saas-cost-trend.js +219 -0
  22. package/dist-client/pages/saas-cost-trend.js.map +1 -0
  23. package/dist-client/route.d.ts +1 -0
  24. package/dist-client/route.js +16 -0
  25. package/dist-client/route.js.map +1 -0
  26. package/dist-client/tsconfig.tsbuildinfo +1 -0
  27. package/dist-client/words.d.ts +47 -0
  28. package/dist-client/words.js +39 -0
  29. package/dist-client/words.js.map +1 -0
  30. package/dist-server/index.d.ts +1 -0
  31. package/dist-server/index.js +9 -0
  32. package/dist-server/index.js.map +1 -0
  33. package/dist-server/role-tasks.d.ts +2 -0
  34. package/dist-server/role-tasks.js +16 -0
  35. package/dist-server/role-tasks.js.map +1 -0
  36. package/dist-server/service/checks.d.ts +32 -0
  37. package/dist-server/service/checks.js +83 -0
  38. package/dist-server/service/checks.js.map +1 -0
  39. package/dist-server/service/index.d.ts +20 -0
  40. package/dist-server/service/index.js +38 -0
  41. package/dist-server/service/index.js.map +1 -0
  42. package/dist-server/service/saas-access.d.ts +20 -0
  43. package/dist-server/service/saas-access.js +36 -0
  44. package/dist-server/service/saas-access.js.map +1 -0
  45. package/dist-server/service/saas-account.d.ts +43 -0
  46. package/dist-server/service/saas-account.js +166 -0
  47. package/dist-server/service/saas-account.js.map +1 -0
  48. package/dist-server/service/saas-bill.d.ts +41 -0
  49. package/dist-server/service/saas-bill.js +160 -0
  50. package/dist-server/service/saas-bill.js.map +1 -0
  51. package/dist-server/service/saas-mutation.d.ts +12 -0
  52. package/dist-server/service/saas-mutation.js +103 -0
  53. package/dist-server/service/saas-mutation.js.map +1 -0
  54. package/dist-server/service/saas-query.d.ts +69 -0
  55. package/dist-server/service/saas-query.js +215 -0
  56. package/dist-server/service/saas-query.js.map +1 -0
  57. package/dist-server/service/saas-rules.d.ts +138 -0
  58. package/dist-server/service/saas-rules.js +217 -0
  59. package/dist-server/service/saas-rules.js.map +1 -0
  60. package/dist-server/service/saas-seat.d.ts +25 -0
  61. package/dist-server/service/saas-seat.js +105 -0
  62. package/dist-server/service/saas-seat.js.map +1 -0
  63. package/dist-server/service/saas-service.d.ts +119 -0
  64. package/dist-server/service/saas-service.js +492 -0
  65. package/dist-server/service/saas-service.js.map +1 -0
  66. package/dist-server/service/saas-subscription.d.ts +44 -0
  67. package/dist-server/service/saas-subscription.js +174 -0
  68. package/dist-server/service/saas-subscription.js.map +1 -0
  69. package/dist-server/service/saas-trend-type.d.ts +9 -0
  70. package/dist-server/service/saas-trend-type.js +37 -0
  71. package/dist-server/service/saas-trend-type.js.map +1 -0
  72. package/dist-server/service/saas-type.d.ts +113 -0
  73. package/dist-server/service/saas-type.js +430 -0
  74. package/dist-server/service/saas-type.js.map +1 -0
  75. package/dist-server/tsconfig.tsbuildinfo +1 -0
  76. package/package.json +49 -4
  77. package/things-factory.config.js +13 -0
  78. package/translations/en.json +185 -0
  79. package/translations/es.json +185 -0
  80. package/translations/ja.json +185 -0
  81. package/translations/ko.json +185 -0
  82. package/translations/ms.json +185 -0
  83. package/translations/zh.json +185 -0
@@ -0,0 +1,219 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import gql from 'graphql-tag';
3
+ import { css, html, nothing } from 'lit';
4
+ import { customElement, state } from 'lit/decorators.js';
5
+ import { client } from '@operato/graphql';
6
+ import { i18next, localize } from '@operato/i18n';
7
+ import { navigate, PageView } from '@operato/shell';
8
+ import { ScrollbarStyles } from '@operato/styles';
9
+ import { amountText, changeText, shiftMonth, thisMonth } from '../words.js';
10
+ const SERIES = 'key accountId accountName subscriptionId name currency amounts total monthOverMonth yearOverYear';
11
+ /**
12
+ * 비용 추이 — what the subscriptions cost month by month, from their bills (ADR-0177 decision 9): everything, each account
13
+ * with its subscriptions under it, each department, in each bill's own currency. What the latest bills are worth telling
14
+ * comes first. Nothing here is stored; it is read from the bills each time.
15
+ */
16
+ let SaasCostTrend = class SaasCostTrend extends localize(i18next)(PageView) {
17
+ constructor() {
18
+ super(...arguments);
19
+ this.from = shiftMonth(thisMonth(), -12);
20
+ this.to = shiftMonth(thisMonth(), -1);
21
+ }
22
+ static { this.styles = [
23
+ ScrollbarStyles,
24
+ css `
25
+ :host {
26
+ display: flex;
27
+ flex-direction: column;
28
+ overflow: auto;
29
+ padding: var(--spacing-large, 12px);
30
+ gap: var(--spacing-large, 12px);
31
+ font-size: var(--ui-t-body, 14px);
32
+ }
33
+ .range {
34
+ display: flex;
35
+ flex-wrap: wrap;
36
+ align-items: center;
37
+ gap: var(--spacing-medium, 8px);
38
+ color: var(--md-sys-color-on-surface-variant);
39
+ }
40
+ .range input {
41
+ height: 32px;
42
+ padding: 0 8px;
43
+ border: 1px solid var(--ui-line-soft, var(--md-sys-color-outline-variant));
44
+ border-radius: var(--md-sys-shape-corner-small, 4px);
45
+ background: var(--md-sys-color-surface);
46
+ color: var(--md-sys-color-on-surface);
47
+ font: inherit;
48
+ }
49
+ h3 {
50
+ margin: 0;
51
+ font-size: var(--ui-t-title, 16px);
52
+ font-weight: 600;
53
+ }
54
+ ul.alerts {
55
+ margin: 0;
56
+ padding-left: 20px;
57
+ color: var(--md-sys-color-error);
58
+ }
59
+ .muted {
60
+ color: var(--md-sys-color-on-surface-variant);
61
+ }
62
+ .scroll {
63
+ overflow-x: auto;
64
+ }
65
+ table {
66
+ border-collapse: collapse;
67
+ white-space: nowrap;
68
+ }
69
+ th,
70
+ td {
71
+ padding: 6px 8px;
72
+ border-bottom: 1px solid var(--ui-line-soft, var(--md-sys-color-outline-variant));
73
+ text-align: right;
74
+ }
75
+ th:first-child,
76
+ td:first-child {
77
+ position: sticky;
78
+ left: 0;
79
+ text-align: left;
80
+ background: var(--md-sys-color-surface);
81
+ }
82
+ th {
83
+ color: var(--md-sys-color-on-surface-variant);
84
+ font-weight: 500;
85
+ }
86
+ tr.account td:first-child button {
87
+ border: none;
88
+ background: none;
89
+ color: var(--md-sys-color-primary);
90
+ font: inherit;
91
+ cursor: pointer;
92
+ padding: 0;
93
+ }
94
+ tr.sub td:first-child {
95
+ padding-left: 24px;
96
+ color: var(--md-sys-color-on-surface-variant);
97
+ }
98
+ .up {
99
+ color: var(--md-sys-color-error);
100
+ }
101
+ `
102
+ ]; }
103
+ get context() {
104
+ return { title: i18next.t('title.saas-cost-trend') };
105
+ }
106
+ async pageUpdated(changes) {
107
+ if (this.active && (changes.active || !this.trend))
108
+ await this.read();
109
+ }
110
+ async read() {
111
+ const response = await client.query({
112
+ query: gql `
113
+ query ($from: String!, $to: String!) {
114
+ saasCostTrend(from: $from, to: $to) {
115
+ months
116
+ all { ${SERIES} }
117
+ accounts { ${SERIES} }
118
+ subscriptions { ${SERIES} }
119
+ departments { key departmentId name currency amounts total }
120
+ alerts { kind accountId accountName subscriptionName month amount limit }
121
+ }
122
+ }
123
+ `,
124
+ variables: { from: this.from, to: this.to },
125
+ fetchPolicy: 'network-only'
126
+ });
127
+ this.trend = response.data?.saasCostTrend;
128
+ }
129
+ change(ratio) {
130
+ return html `<span class=${ratio && ratio > 0 ? 'up' : ''}>${changeText(ratio)}</span>`;
131
+ }
132
+ row(label, one, kind = '') {
133
+ return html `<tr class=${kind}>
134
+ <td>${label}</td>
135
+ <td>${one.currency}</td>
136
+ ${one.amounts.map((amount) => html `<td>${amount ? amountText(amount) : ''}</td>`)}
137
+ <td>${amountText(one.total)}</td>
138
+ ${one.monthOverMonth !== undefined ? html `<td>${this.change(one.monthOverMonth)}</td><td>${this.change(one.yearOverYear)}</td>` : html `<td></td><td></td>`}
139
+ </tr>`;
140
+ }
141
+ head(first) {
142
+ return html `<tr>
143
+ <th>${first}</th>
144
+ <th>${i18next.t('field.saas-trend-currency')}</th>
145
+ ${this.trend.months.map((month) => html `<th>${month}</th>`)}
146
+ <th>${i18next.t('field.saas-trend-total')}</th>
147
+ <th>${i18next.t('field.saas-trend-month-over-month')}</th>
148
+ <th>${i18next.t('field.saas-trend-year-over-year')}</th>
149
+ </tr>`;
150
+ }
151
+ render() {
152
+ const trend = this.trend;
153
+ return html `
154
+ <div class="range">
155
+ <input type="month" .value=${this.from} @change=${(e) => ((this.from = e.target.value), this.read())} />
156
+ ~
157
+ <input type="month" .value=${this.to} @change=${(e) => ((this.to = e.target.value), this.read())} />
158
+ </div>
159
+ ${trend
160
+ ? html `
161
+ ${trend.alerts.length
162
+ ? html `<h3>${i18next.t('text.saas-trend-alerts', { month: trend.alerts[0].month })}</h3>
163
+ <ul class="alerts">
164
+ ${trend.alerts.map((alert) => html `<li>
165
+ ${i18next.t(`text.saas-alert-${alert.kind}`, {
166
+ account: alert.accountName,
167
+ subscription: alert.subscriptionName ?? '',
168
+ amount: amountText(alert.amount),
169
+ limit: amountText(alert.limit)
170
+ })}
171
+ </li>`)}
172
+ </ul>`
173
+ : nothing}
174
+ ${trend.all.length
175
+ ? html `
176
+ <h3>${i18next.t('text.saas-trend-accounts')}</h3>
177
+ <div class="scroll">
178
+ <table>
179
+ ${this.head(i18next.t('field.saas-trend-account'))}
180
+ ${trend.all.map((one) => this.row(i18next.t('text.saas-trend-all'), one))}
181
+ ${trend.accounts.map((account) => html `
182
+ ${this.row(html `<button @click=${() => navigate(`saas-account-page?id=${account.accountId}&tab=bills`)}>${account.name}</button>`, account, 'account')}
183
+ ${trend.subscriptions
184
+ .filter((sub) => sub.accountId === account.accountId && sub.currency === account.currency)
185
+ .map((sub) => this.row(sub.name ?? i18next.t('text.saas-trend-rest'), sub, 'sub'))}
186
+ `)}
187
+ </table>
188
+ </div>
189
+ <h3>${i18next.t('text.saas-trend-departments')}</h3>
190
+ <div class="scroll">
191
+ <table>
192
+ ${this.head(i18next.t('field.saas-trend-department'))}
193
+ ${trend.departments.map((one) => this.row(one.name ?? i18next.t('text.saas-trend-no-department'), one))}
194
+ </table>
195
+ </div>
196
+ `
197
+ : html `<div class="muted">${i18next.t('text.saas-trend-empty')}</div>`}
198
+ `
199
+ : nothing}
200
+ `;
201
+ }
202
+ };
203
+ __decorate([
204
+ state(),
205
+ __metadata("design:type", Object)
206
+ ], SaasCostTrend.prototype, "from", void 0);
207
+ __decorate([
208
+ state(),
209
+ __metadata("design:type", Object)
210
+ ], SaasCostTrend.prototype, "to", void 0);
211
+ __decorate([
212
+ state(),
213
+ __metadata("design:type", Object)
214
+ ], SaasCostTrend.prototype, "trend", void 0);
215
+ SaasCostTrend = __decorate([
216
+ customElement('saas-cost-trend')
217
+ ], SaasCostTrend);
218
+ export { SaasCostTrend };
219
+ //# sourceMappingURL=saas-cost-trend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"saas-cost-trend.js","sourceRoot":"","sources":["../../client/pages/saas-cost-trend.ts"],"names":[],"mappings":";AAAA,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,QAAQ,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAEjD,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAE3E,MAAM,MAAM,GAAG,kGAAkG,CAAA;AAEjH;;;;GAIG;AAEI,IAAM,aAAa,GAAnB,MAAM,aAAc,SAAQ,QAAQ,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC;IAAvD;;QAmFI,SAAI,GAAG,UAAU,CAAC,SAAS,EAAE,EAAE,CAAC,EAAE,CAAC,CAAA;QACnC,OAAE,GAAG,UAAU,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,CAAC,CAAA;IAgH3C,CAAC;aAnMQ,WAAM,GAAG;QACd,eAAe;QACf,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA6EF;KACF,AAhFY,CAgFZ;IAMD,IAAI,OAAO;QACT,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,uBAAuB,CAAC,EAAE,CAAA;IACtD,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,OAAY;QAC5B,IAAI,IAAI,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;YAAE,MAAM,IAAI,CAAC,IAAI,EAAE,CAAA;IACvE,CAAC;IAEO,KAAK,CAAC,IAAI;QAChB,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;YAClC,KAAK,EAAE,GAAG,CAAA;;;;oBAII,MAAM;yBACD,MAAM;8BACD,MAAM;;;;;OAK7B;YACD,SAAS,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE;YAC3C,WAAW,EAAE,cAAc;SAC5B,CAAC,CAAA;QACF,IAAI,CAAC,KAAK,GAAG,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAA;IAC3C,CAAC;IAEO,MAAM,CAAC,KAAqB;QAClC,OAAO,IAAI,CAAA,eAAe,KAAK,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,UAAU,CAAC,KAAK,CAAC,SAAS,CAAA;IACxF,CAAC;IAEO,GAAG,CAAC,KAAc,EAAE,GAAQ,EAAE,IAAI,GAAG,EAAE;QAC7C,OAAO,IAAI,CAAA,aAAa,IAAI;YACpB,KAAK;YACL,GAAG,CAAC,QAAQ;QAChB,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAc,EAAE,EAAE,CAAC,IAAI,CAAA,OAAO,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC;YACnF,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC;QACzB,GAAG,CAAC,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAA,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,YAAY,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAA,oBAAoB;UACtJ,CAAA;IACR,CAAC;IAEO,IAAI,CAAC,KAAa;QACxB,OAAO,IAAI,CAAA;YACH,KAAK;YACL,OAAO,CAAC,CAAC,CAAC,2BAA2B,CAAC;QAC1C,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAa,EAAE,EAAE,CAAC,IAAI,CAAA,OAAO,KAAK,OAAO,CAAC;YAC7D,OAAO,CAAC,CAAC,CAAC,wBAAwB,CAAC;YACnC,OAAO,CAAC,CAAC,CAAC,mCAAmC,CAAC;YAC9C,OAAO,CAAC,CAAC,CAAC,iCAAiC,CAAC;UAC9C,CAAA;IACR,CAAC;IAED,MAAM;QACJ,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAA;QACxB,OAAO,IAAI,CAAA;;qCAEsB,IAAI,CAAC,IAAI,YAAY,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC;;qCAE5E,IAAI,CAAC,EAAE,YAAY,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC;;QAErG,KAAK;YACL,CAAC,CAAC,IAAI,CAAA;cACA,KAAK,CAAC,MAAM,CAAC,MAAM;gBACnB,CAAC,CAAC,IAAI,CAAA,OAAO,OAAO,CAAC,CAAC,CAAC,wBAAwB,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC;;sBAE1E,KAAK,CAAC,MAAM,CAAC,GAAG,CAChB,CAAC,KAAU,EAAE,EAAE,CACb,IAAI,CAAA;4BACA,OAAO,CAAC,CAAC,CAAC,mBAAmB,KAAK,CAAC,IAAI,EAAE,EAAE;oBAC3C,OAAO,EAAE,KAAK,CAAC,WAAW;oBAC1B,YAAY,EAAE,KAAK,CAAC,gBAAgB,IAAI,EAAE;oBAC1C,MAAM,EAAE,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC;oBAChC,KAAK,EAAE,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC;iBAC/B,CAAC;8BACE,CACT;wBACG;gBACV,CAAC,CAAC,OAAO;cACT,KAAK,CAAC,GAAG,CAAC,MAAM;gBAChB,CAAC,CAAC,IAAI,CAAA;wBACI,OAAO,CAAC,CAAC,CAAC,0BAA0B,CAAC;;;wBAGrC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,0BAA0B,CAAC,CAAC;wBAChD,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,qBAAqB,CAAC,EAAE,GAAG,CAAC,CAAC;wBAC5E,KAAK,CAAC,QAAQ,CAAC,GAAG,CAClB,CAAC,OAAY,EAAE,EAAE,CAAC,IAAI,CAAA;4BAClB,IAAI,CAAC,GAAG,CAAC,IAAI,CAAA,kBAAkB,GAAG,EAAE,CAAC,QAAQ,CAAC,wBAAwB,OAAO,CAAC,SAAS,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,WAAW,EAAE,OAAO,EAAE,SAAS,CAAC;4BACpJ,KAAK,CAAC,aAAa;qBAClB,MAAM,CAAC,CAAC,GAAQ,EAAE,EAAE,CAAC,GAAG,CAAC,SAAS,KAAK,OAAO,CAAC,SAAS,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO,CAAC,QAAQ,CAAC;qBAC9F,GAAG,CAAC,CAAC,GAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,OAAO,CAAC,CAAC,CAAC,sBAAsB,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;yBAC1F,CACF;;;wBAGC,OAAO,CAAC,CAAC,CAAC,6BAA6B,CAAC;;;wBAGxC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,6BAA6B,CAAC,CAAC;wBACnD,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,GAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,OAAO,CAAC,CAAC,CAAC,+BAA+B,CAAC,EAAE,GAAG,CAAC,CAAC;;;iBAGjH;gBACH,CAAC,CAAC,IAAI,CAAA,sBAAsB,OAAO,CAAC,CAAC,CAAC,uBAAuB,CAAC,QAAQ;WACzE;YACH,CAAC,CAAC,OAAO;KACZ,CAAA;IACH,CAAC;;AAhHQ;IAAR,KAAK,EAAE;;2CAAoC;AACnC;IAAR,KAAK,EAAE;;yCAAiC;AAChC;IAAR,KAAK,EAAE;;4CAAY;AArFT,aAAa;IADzB,aAAa,CAAC,iBAAiB,CAAC;GACpB,aAAa,CAoMzB","sourcesContent":["import 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 { navigate, PageView } from '@operato/shell'\nimport { ScrollbarStyles } from '@operato/styles'\n\nimport { amountText, changeText, shiftMonth, thisMonth } from '../words.js'\n\nconst SERIES = 'key accountId accountName subscriptionId name currency amounts total monthOverMonth yearOverYear'\n\n/**\n * 비용 추이 — what the subscriptions cost month by month, from their bills (ADR-0177 decision 9): everything, each account\n * with its subscriptions under it, each department, in each bill's own currency. What the latest bills are worth telling\n * comes first. Nothing here is stored; it is read from the bills each time.\n */\n@customElement('saas-cost-trend')\nexport class SaasCostTrend extends localize(i18next)(PageView) {\n static styles = [\n ScrollbarStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n overflow: auto;\n padding: var(--spacing-large, 12px);\n gap: var(--spacing-large, 12px);\n font-size: var(--ui-t-body, 14px);\n }\n .range {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--spacing-medium, 8px);\n color: var(--md-sys-color-on-surface-variant);\n }\n .range input {\n height: 32px;\n padding: 0 8px;\n border: 1px solid var(--ui-line-soft, var(--md-sys-color-outline-variant));\n border-radius: var(--md-sys-shape-corner-small, 4px);\n background: var(--md-sys-color-surface);\n color: var(--md-sys-color-on-surface);\n font: inherit;\n }\n h3 {\n margin: 0;\n font-size: var(--ui-t-title, 16px);\n font-weight: 600;\n }\n ul.alerts {\n margin: 0;\n padding-left: 20px;\n color: var(--md-sys-color-error);\n }\n .muted {\n color: var(--md-sys-color-on-surface-variant);\n }\n .scroll {\n overflow-x: auto;\n }\n table {\n border-collapse: collapse;\n white-space: nowrap;\n }\n th,\n td {\n padding: 6px 8px;\n border-bottom: 1px solid var(--ui-line-soft, var(--md-sys-color-outline-variant));\n text-align: right;\n }\n th:first-child,\n td:first-child {\n position: sticky;\n left: 0;\n text-align: left;\n background: var(--md-sys-color-surface);\n }\n th {\n color: var(--md-sys-color-on-surface-variant);\n font-weight: 500;\n }\n tr.account td:first-child button {\n border: none;\n background: none;\n color: var(--md-sys-color-primary);\n font: inherit;\n cursor: pointer;\n padding: 0;\n }\n tr.sub td:first-child {\n padding-left: 24px;\n color: var(--md-sys-color-on-surface-variant);\n }\n .up {\n color: var(--md-sys-color-error);\n }\n `\n ]\n\n @state() from = shiftMonth(thisMonth(), -12)\n @state() to = shiftMonth(thisMonth(), -1)\n @state() trend?: any\n\n get context() {\n return { title: i18next.t('title.saas-cost-trend') }\n }\n\n async pageUpdated(changes: any) {\n if (this.active && (changes.active || !this.trend)) await this.read()\n }\n\n private async read() {\n const response = await client.query({\n query: gql`\n query ($from: String!, $to: String!) {\n saasCostTrend(from: $from, to: $to) {\n months\n all { ${SERIES} }\n accounts { ${SERIES} }\n subscriptions { ${SERIES} }\n departments { key departmentId name currency amounts total }\n alerts { kind accountId accountName subscriptionName month amount limit }\n }\n }\n `,\n variables: { from: this.from, to: this.to },\n fetchPolicy: 'network-only'\n })\n this.trend = response.data?.saasCostTrend\n }\n\n private change(ratio?: number | null) {\n return html`<span class=${ratio && ratio > 0 ? 'up' : ''}>${changeText(ratio)}</span>`\n }\n\n private row(label: unknown, one: any, kind = '') {\n return html`<tr class=${kind}>\n <td>${label}</td>\n <td>${one.currency}</td>\n ${one.amounts.map((amount: number) => html`<td>${amount ? amountText(amount) : ''}</td>`)}\n <td>${amountText(one.total)}</td>\n ${one.monthOverMonth !== undefined ? html`<td>${this.change(one.monthOverMonth)}</td><td>${this.change(one.yearOverYear)}</td>` : html`<td></td><td></td>`}\n </tr>`\n }\n\n private head(first: string) {\n return html`<tr>\n <th>${first}</th>\n <th>${i18next.t('field.saas-trend-currency')}</th>\n ${this.trend.months.map((month: string) => html`<th>${month}</th>`)}\n <th>${i18next.t('field.saas-trend-total')}</th>\n <th>${i18next.t('field.saas-trend-month-over-month')}</th>\n <th>${i18next.t('field.saas-trend-year-over-year')}</th>\n </tr>`\n }\n\n render() {\n const trend = this.trend\n return html`\n <div class=\"range\">\n <input type=\"month\" .value=${this.from} @change=${(e: any) => ((this.from = e.target.value), this.read())} />\n ~\n <input type=\"month\" .value=${this.to} @change=${(e: any) => ((this.to = e.target.value), this.read())} />\n </div>\n ${trend\n ? html`\n ${trend.alerts.length\n ? html`<h3>${i18next.t('text.saas-trend-alerts', { month: trend.alerts[0].month })}</h3>\n <ul class=\"alerts\">\n ${trend.alerts.map(\n (alert: any) =>\n html`<li>\n ${i18next.t(`text.saas-alert-${alert.kind}`, {\n account: alert.accountName,\n subscription: alert.subscriptionName ?? '',\n amount: amountText(alert.amount),\n limit: amountText(alert.limit)\n })}\n </li>`\n )}\n </ul>`\n : nothing}\n ${trend.all.length\n ? html`\n <h3>${i18next.t('text.saas-trend-accounts')}</h3>\n <div class=\"scroll\">\n <table>\n ${this.head(i18next.t('field.saas-trend-account'))}\n ${trend.all.map((one: any) => this.row(i18next.t('text.saas-trend-all'), one))}\n ${trend.accounts.map(\n (account: any) => html`\n ${this.row(html`<button @click=${() => navigate(`saas-account-page?id=${account.accountId}&tab=bills`)}>${account.name}</button>`, account, 'account')}\n ${trend.subscriptions\n .filter((sub: any) => sub.accountId === account.accountId && sub.currency === account.currency)\n .map((sub: any) => this.row(sub.name ?? i18next.t('text.saas-trend-rest'), sub, 'sub'))}\n `\n )}\n </table>\n </div>\n <h3>${i18next.t('text.saas-trend-departments')}</h3>\n <div class=\"scroll\">\n <table>\n ${this.head(i18next.t('field.saas-trend-department'))}\n ${trend.departments.map((one: any) => this.row(one.name ?? i18next.t('text.saas-trend-no-department'), one))}\n </table>\n </div>\n `\n : html`<div class=\"muted\">${i18next.t('text.saas-trend-empty')}</div>`}\n `\n : nothing}\n `\n }\n}\n"]}
@@ -0,0 +1 @@
1
+ export default function route(page: string): "saas-account-list" | "saas-cost-trend" | "my-saas-account-list" | "saas-account-page" | undefined;
@@ -0,0 +1,16 @@
1
+ /* The pages this module brings to an application that puts them in its menu (ADR-0177). */
2
+ export default function route(page) {
3
+ switch (page) {
4
+ case 'saas-account-list':
5
+ case 'my-saas-account-list':
6
+ import('./pages/saas-account-list.js');
7
+ return page;
8
+ case 'saas-account-page':
9
+ import('./pages/saas-account-page.js');
10
+ return page;
11
+ case 'saas-cost-trend':
12
+ import('./pages/saas-cost-trend.js');
13
+ return page;
14
+ }
15
+ }
16
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route.js","sourceRoot":"","sources":["../client/route.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,MAAM,CAAC,OAAO,UAAU,KAAK,CAAC,IAAY;IACxC,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,mBAAmB,CAAC;QACzB,KAAK,sBAAsB;YACzB,MAAM,CAAC,8BAA8B,CAAC,CAAA;YACtC,OAAO,IAAI,CAAA;QAEb,KAAK,mBAAmB;YACtB,MAAM,CAAC,8BAA8B,CAAC,CAAA;YACtC,OAAO,IAAI,CAAA;QAEb,KAAK,iBAAiB;YACpB,MAAM,CAAC,4BAA4B,CAAC,CAAA;YACpC,OAAO,IAAI,CAAA;IACf,CAAC;AACH,CAAC","sourcesContent":["/* The pages this module brings to an application that puts them in its menu (ADR-0177). */\nexport default function route(page: string) {\n switch (page) {\n case 'saas-account-list':\n case 'my-saas-account-list':\n import('./pages/saas-account-list.js')\n return page\n\n case 'saas-account-page':\n import('./pages/saas-account-page.js')\n return page\n\n case 'saas-cost-trend':\n import('./pages/saas-cost-trend.js')\n return page\n }\n}\n"]}