@trismegisto/spine 3.3.0 → 3.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -11
- package/fesm2022/trismegisto-spine-audit.mjs +16 -12
- package/fesm2022/trismegisto-spine-audit.mjs.map +1 -1
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-audit.component-tfGcVR3i.mjs → trismegisto-spine-auto-service-auto-service-audit.component-D8HUpZtA.mjs} +4 -4
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-audit.component-tfGcVR3i.mjs.map → trismegisto-spine-auto-service-auto-service-audit.component-D8HUpZtA.mjs.map} +1 -1
- package/fesm2022/trismegisto-spine-auto-service-auto-service-client-picker.component-NawbfdLW.mjs +65 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-client-picker.component-NawbfdLW.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-client.component-IATeedbr.mjs → trismegisto-spine-auto-service-auto-service-client.component-BAfeLyTB.mjs} +27 -19
- package/fesm2022/trismegisto-spine-auto-service-auto-service-client.component-BAfeLyTB.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-BYjIQ9ID.mjs +635 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-BYjIQ9ID.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-login-attempts.component-CRvxfSWD.mjs → trismegisto-spine-auto-service-auto-service-login-attempts.component-DoyhkSYc.mjs} +4 -4
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-login-attempts.component-CRvxfSWD.mjs.map → trismegisto-spine-auto-service-auto-service-login-attempts.component-DoyhkSYc.mjs.map} +1 -1
- package/fesm2022/trismegisto-spine-auto-service-auto-service-news-page.component-CBemH4hr.mjs +432 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-news-page.component-CBemH4hr.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-ratings-page.component-C_THGTkw.mjs +769 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-ratings-page.component-C_THGTkw.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-DIeunpQ9.mjs +2123 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-DIeunpQ9.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-auto-service.component-DGrs_Jvw.mjs → trismegisto-spine-auto-service-auto-service.component-QsIQ61w2.mjs} +31 -23
- package/fesm2022/trismegisto-spine-auto-service-auto-service.component-QsIQ61w2.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-client.usage-C_2zBCRN.mjs → trismegisto-spine-auto-service-client.usage-CJxn9Ysz.mjs} +5 -3
- package/fesm2022/trismegisto-spine-auto-service-client.usage-CJxn9Ysz.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-groups.technical-9aPPulhy.mjs → trismegisto-spine-auto-service-groups.technical-_jQrg1Qh.mjs} +7 -5
- package/fesm2022/trismegisto-spine-auto-service-groups.technical-_jQrg1Qh.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-groups.usage-CYh61LFL.mjs → trismegisto-spine-auto-service-groups.usage-OvWjNeeT.mjs} +7 -6
- package/fesm2022/{trismegisto-spine-auto-service-groups.usage-CYh61LFL.mjs.map → trismegisto-spine-auto-service-groups.usage-OvWjNeeT.mjs.map} +1 -1
- package/fesm2022/trismegisto-spine-auto-service-home.component-rQ3dIGIE.mjs +315 -0
- package/fesm2022/trismegisto-spine-auto-service-home.component-rQ3dIGIE.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-home.technical-Ce7oYRdg.mjs +46 -0
- package/fesm2022/trismegisto-spine-auto-service-home.technical-Ce7oYRdg.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-home.usage-D9R_ZMuU.mjs +48 -0
- package/fesm2022/trismegisto-spine-auto-service-home.usage-D9R_ZMuU.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-news.technical-DV9UpobG.mjs +38 -0
- package/fesm2022/trismegisto-spine-auto-service-news.technical-DV9UpobG.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-news.usage-CeuZnG0V.mjs +48 -0
- package/fesm2022/trismegisto-spine-auto-service-news.usage-CeuZnG0V.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-ratings.technical-QIVj6HBU.mjs +52 -0
- package/fesm2022/trismegisto-spine-auto-service-ratings.technical-QIVj6HBU.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-ratings.usage-BkteY8lz.mjs +45 -0
- package/fesm2022/trismegisto-spine-auto-service-ratings.usage-BkteY8lz.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-suggestions.technical-BtUK6sKD.mjs +69 -0
- package/fesm2022/trismegisto-spine-auto-service-suggestions.technical-BtUK6sKD.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-suggestions.usage-BwO9Hjsx.mjs +72 -0
- package/fesm2022/trismegisto-spine-auto-service-suggestions.usage-BwO9Hjsx.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service.mjs +49 -19
- package/fesm2022/trismegisto-spine-auto-service.mjs.map +1 -1
- package/fesm2022/trismegisto-spine-graphql.mjs +11 -2
- package/fesm2022/trismegisto-spine-graphql.mjs.map +1 -1
- package/fesm2022/trismegisto-spine-layout.mjs +3113 -1221
- package/fesm2022/trismegisto-spine-layout.mjs.map +1 -1
- package/fesm2022/trismegisto-spine-modal.mjs +7 -5
- package/fesm2022/trismegisto-spine-modal.mjs.map +1 -1
- package/fesm2022/trismegisto-spine-routing.mjs +12 -20
- package/fesm2022/trismegisto-spine-routing.mjs.map +1 -1
- package/fesm2022/trismegisto-spine-shell.mjs +15 -39
- package/fesm2022/trismegisto-spine-shell.mjs.map +1 -1
- package/fesm2022/trismegisto-spine-side-panel.mjs +7 -5
- package/fesm2022/trismegisto-spine-side-panel.mjs.map +1 -1
- package/fesm2022/trismegisto-spine.mjs +1969 -279
- package/fesm2022/trismegisto-spine.mjs.map +1 -1
- package/package.json +6 -6
- package/types/trismegisto-spine-audit.d.ts +2 -12
- package/types/trismegisto-spine-audit.d.ts.map +1 -1
- package/types/trismegisto-spine-auto-service.d.ts +22 -5
- package/types/trismegisto-spine-auto-service.d.ts.map +1 -1
- package/types/trismegisto-spine-layout.d.ts +1083 -283
- package/types/trismegisto-spine-layout.d.ts.map +1 -1
- package/types/trismegisto-spine-modal.d.ts +1 -0
- package/types/trismegisto-spine-modal.d.ts.map +1 -1
- package/types/trismegisto-spine-routing.d.ts +3 -12
- package/types/trismegisto-spine-routing.d.ts.map +1 -1
- package/types/trismegisto-spine-shell.d.ts +2 -15
- package/types/trismegisto-spine-shell.d.ts.map +1 -1
- package/types/trismegisto-spine-side-panel.d.ts +1 -0
- package/types/trismegisto-spine-side-panel.d.ts.map +1 -1
- package/types/trismegisto-spine.d.ts +739 -165
- package/types/trismegisto-spine.d.ts.map +1 -1
- package/fesm2022/trismegisto-spine-auto-service-auto-service-client-picker.component-Cl7ahisu.mjs +0 -65
- package/fesm2022/trismegisto-spine-auto-service-auto-service-client-picker.component-Cl7ahisu.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-auto-service-client.component-IATeedbr.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-D6L0gTmd.mjs +0 -605
- package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-D6L0gTmd.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-auto-service.component-DGrs_Jvw.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-client.usage-C_2zBCRN.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-groups.technical-9aPPulhy.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-home.component-5_aji1hf.mjs +0 -239
- package/fesm2022/trismegisto-spine-auto-service-home.component-5_aji1hf.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-home.technical-BrVhcgDO.mjs +0 -35
- package/fesm2022/trismegisto-spine-auto-service-home.technical-BrVhcgDO.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-home.usage-DWYGtngX.mjs +0 -43
- package/fesm2022/trismegisto-spine-auto-service-home.usage-DWYGtngX.mjs.map +0 -1
package/fesm2022/trismegisto-spine-auto-service-auto-service-ratings-page.component-C_THGTkw.mjs
ADDED
|
@@ -0,0 +1,769 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, Injectable, input, model, output, signal, computed, ChangeDetectionStrategy, Component, viewChild, effect, untracked } from '@angular/core';
|
|
3
|
+
import { A as AutoServiceClientPickerComponent } from './trismegisto-spine-auto-service-auto-service-client-picker.component-NawbfdLW.mjs';
|
|
4
|
+
import { HttpClient, HttpHeaders, HttpParams, HttpErrorResponse } from '@angular/common/http';
|
|
5
|
+
import { ComboboxComponent, IconComponent, PaginatorComponent, SearchFieldComponent, SegmentedControlComponent, TooltipDirective, HermesToastService, AccountAvatarComponent, AlertComponent, ButtonComponent, CardComponent, DrawerComponent, MenuButtonComponent, SkeletonComponent, StateScreenComponent, SwitchComponent, TabComponent, TabContentDirective, TabsComponent, TagComponent } from '@trismegisto/angular';
|
|
6
|
+
import { urlPath, formatMessage, pluralMessage } from '@trismegisto/primitives';
|
|
7
|
+
import { SPINE_MESSAGES } from '@trismegisto/spine';
|
|
8
|
+
import { SpineRatingStarsComponent, SPINE_RATING_SCORE_MAX, SpinePageActionsDirective } from '@trismegisto/spine/layout';
|
|
9
|
+
import { firstValueFrom } from 'rxjs';
|
|
10
|
+
import { AuthorizationService } from '@orfeu/angular/core';
|
|
11
|
+
import { AUTO_SERVICE_CONFIG } from './trismegisto-spine-auto-service.mjs';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Leitura das avaliações do app no auto-serviço (`{adminApi}/auto-service/:clientId/ratings*`): o servidor recorta
|
|
15
|
+
* pelo client da URL e exige o grupo de administração com o crachá Administrador. Próprio da aba (o
|
|
16
|
+
* `AutoServiceService` é o das outras telas): fornecido pelo componente.
|
|
17
|
+
*/
|
|
18
|
+
class AutoServiceRatingsService {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.http = inject(HttpClient);
|
|
21
|
+
this.auth = inject(AuthorizationService);
|
|
22
|
+
this.config = inject(AUTO_SERVICE_CONFIG);
|
|
23
|
+
}
|
|
24
|
+
url(clientId, rest) {
|
|
25
|
+
return `${this.config.adminApi}/auto-service${urlPath `/${clientId}`}${rest}`;
|
|
26
|
+
}
|
|
27
|
+
get headers() {
|
|
28
|
+
return new HttpHeaders({ 'x-access-token': this.auth.getToken() ?? '' });
|
|
29
|
+
}
|
|
30
|
+
summary(clientId, from) {
|
|
31
|
+
const params = from ? new HttpParams().set('from', from) : new HttpParams();
|
|
32
|
+
return firstValueFrom(this.http.get(this.url(clientId, '/ratings/summary'), { headers: this.headers, params }));
|
|
33
|
+
}
|
|
34
|
+
list(clientId, q) {
|
|
35
|
+
let params = new HttpParams();
|
|
36
|
+
if (q.from)
|
|
37
|
+
params = params.set('from', q.from);
|
|
38
|
+
if (q.scope)
|
|
39
|
+
params = params.set('scope', q.scope);
|
|
40
|
+
if (q.screen)
|
|
41
|
+
params = params.set('screen', q.screen);
|
|
42
|
+
if (q.score != null)
|
|
43
|
+
params = params.set('score', String(q.score));
|
|
44
|
+
if (q.comment)
|
|
45
|
+
params = params.set('comment', 'true');
|
|
46
|
+
if (q.page)
|
|
47
|
+
params = params.set('page', String(q.page));
|
|
48
|
+
if (q.limit)
|
|
49
|
+
params = params.set('limit', String(q.limit));
|
|
50
|
+
return firstValueFrom(this.http.get(this.url(clientId, '/ratings'), { headers: this.headers, params }));
|
|
51
|
+
}
|
|
52
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceRatingsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
53
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceRatingsService }); }
|
|
54
|
+
}
|
|
55
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceRatingsService, decorators: [{
|
|
56
|
+
type: Injectable
|
|
57
|
+
}] });
|
|
58
|
+
|
|
59
|
+
/** Ícone de uma tela que não está mais no menu (ou de um servidor que ainda não manda `screen_icon`). */
|
|
60
|
+
const RATINGS_SCREEN_FALLBACK_ICON = 'grid-layout';
|
|
61
|
+
/** Ícone de uma área sem ícone no menu. */
|
|
62
|
+
const RATINGS_AREA_FALLBACK_ICON = 'tree-view';
|
|
63
|
+
/** Quantas cores categóricas o tema tem (`--hm-data-categorical-1..8`): o quadrado do ícone de cada tela. */
|
|
64
|
+
const HUES = 8;
|
|
65
|
+
/** Faixa de cor de uma nota ou média de 0 a 10: abaixo de 5 ruim, abaixo de 7 regular, de 7 para cima boa. */
|
|
66
|
+
function ratingTone(average) {
|
|
67
|
+
if (average === null)
|
|
68
|
+
return 'neutral';
|
|
69
|
+
if (average < 5)
|
|
70
|
+
return 'danger';
|
|
71
|
+
if (average < 7)
|
|
72
|
+
return 'warning';
|
|
73
|
+
return 'success';
|
|
74
|
+
}
|
|
75
|
+
function ratingBand(average) {
|
|
76
|
+
const tone = ratingTone(average);
|
|
77
|
+
return tone === 'danger' ? 'bad' : tone === 'warning' ? 'mid' : tone === 'success' ? 'good' : null;
|
|
78
|
+
}
|
|
79
|
+
/** Na ordem pedida; empate pela contagem (mais avaliada primeiro) e pela chave. */
|
|
80
|
+
function sortRows(rows, sort) {
|
|
81
|
+
const avg = (r) => r.agg.average ?? 0;
|
|
82
|
+
const tie = (a, b) => b.agg.total - a.agg.total || a.key.localeCompare(b.key);
|
|
83
|
+
return [...rows].sort((a, b) => {
|
|
84
|
+
if (sort === 'worst')
|
|
85
|
+
return avg(a) - avg(b) || tie(a, b);
|
|
86
|
+
if (sort === 'best')
|
|
87
|
+
return avg(b) - avg(a) || tie(a, b);
|
|
88
|
+
return tie(a, b);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/** Telas na ordem pedida (mesma regra de `sortRows`, pela rota). */
|
|
92
|
+
function sortScreens(screens, sort) {
|
|
93
|
+
return sortRows(screens.map((s) => ({ key: s.screen_path, agg: s, s })), sort).map((r) => r.s);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Cor do quadrado do ícone (1..8, a paleta categórica do tema): sai da chave (caminho da tela, nome da área), então a
|
|
97
|
+
* mesma tela tem a mesma cor em qualquer ordem e filtro -- a cor segue a tela, nunca a posição na lista.
|
|
98
|
+
*/
|
|
99
|
+
function screenHue(key) {
|
|
100
|
+
let h = 0;
|
|
101
|
+
for (const ch of key ?? '')
|
|
102
|
+
h = (h * 31 + ch.charCodeAt(0)) >>> 0;
|
|
103
|
+
return (h % HUES) + 1;
|
|
104
|
+
}
|
|
105
|
+
/** Área da tela: a do menu (servidor) ou, sem ela, a trilha guardada com a avaliação menos o último degrau. */
|
|
106
|
+
function areaOf(s) {
|
|
107
|
+
if (s.screen_area)
|
|
108
|
+
return s.screen_area;
|
|
109
|
+
const parts = (s.screen_label ?? '').split('›').map((p) => p.trim()).filter(Boolean);
|
|
110
|
+
return parts.length > 1 ? parts.slice(0, -1).join(' › ') : null;
|
|
111
|
+
}
|
|
112
|
+
/** Soma agregados (a média sai das contagens somadas, ponderada; uma casa). */
|
|
113
|
+
function combineAggregates(parts) {
|
|
114
|
+
const distribution = Array.from({ length: 11 }, () => 0);
|
|
115
|
+
for (const p of parts)
|
|
116
|
+
p.distribution.forEach((n, i) => { distribution[i] = (distribution[i] ?? 0) + n; });
|
|
117
|
+
const total = distribution.reduce((a, n) => a + n, 0);
|
|
118
|
+
const sum = distribution.reduce((a, n, score) => a + n * score, 0);
|
|
119
|
+
return {
|
|
120
|
+
average: total ? Math.round((sum / total) * 10) / 10 : null,
|
|
121
|
+
total,
|
|
122
|
+
comments: parts.reduce((a, p) => a + p.comments, 0),
|
|
123
|
+
distribution,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/** Soma séries semanais (mesmas semanas): média ponderada pelo total de cada semana. */
|
|
127
|
+
function combineTrends(series) {
|
|
128
|
+
const by = new Map();
|
|
129
|
+
for (const s of series) {
|
|
130
|
+
for (const p of s ?? []) {
|
|
131
|
+
const cur = by.get(p.week) ?? { sum: 0, total: 0 };
|
|
132
|
+
by.set(p.week, { sum: cur.sum + (p.average ?? 0) * p.total, total: cur.total + p.total });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return [...by.keys()].sort().map((week) => {
|
|
136
|
+
const v = by.get(week);
|
|
137
|
+
return { week, total: v.total, average: v.total ? Math.round((v.sum / v.total) * 10) / 10 : null };
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
/** Centro da semana `i` de `n` numa largura `width` (cada semana é uma coluna do mesmo tamanho). */
|
|
141
|
+
const weekX = (i, n, width) => ((i + 0.5) * width) / n;
|
|
142
|
+
/**
|
|
143
|
+
* A linha (0..10 na vertical) num quadro `width` x `height`, ligando as semanas que têm nota. Semana sem avaliação não
|
|
144
|
+
* quebra a linha: com poucas avaliações por semana quase toda semana tem buraco, e a linha picotada não dizia nada. Só
|
|
145
|
+
* uma semana com nota vira um traço curto (`dot`), para não sumir. Devolve lista (vazia sem nota) para o template.
|
|
146
|
+
*/
|
|
147
|
+
function linePaths(points, width, height, dot = 1.5) {
|
|
148
|
+
const n = points.length;
|
|
149
|
+
const y = (v) => height - (v / 10) * height;
|
|
150
|
+
let line = points.flatMap((p, i) => (p.average == null ? [] : [[weekX(i, n, width), y(p.average)]]));
|
|
151
|
+
if (!line.length)
|
|
152
|
+
return [];
|
|
153
|
+
if (line.length === 1)
|
|
154
|
+
line = [[line[0][0] - dot, line[0][1]], [line[0][0] + dot, line[0][1]]];
|
|
155
|
+
return [line.map(([x, yy]) => `${round(x)},${round(yy)}`).join(' ')];
|
|
156
|
+
}
|
|
157
|
+
const round = (v) => Math.round(v * 100) / 100;
|
|
158
|
+
/** Primeira e última média com nota de uma série (a tendência resumida em texto); `null` sem nenhuma. */
|
|
159
|
+
function trendEnds(points) {
|
|
160
|
+
const valued = (points ?? []).filter((p) => p.average != null);
|
|
161
|
+
return valued.length ? { first: valued[0].average, last: valued.at(-1).average } : null;
|
|
162
|
+
}
|
|
163
|
+
function relativeWhen(date, now = new Date()) {
|
|
164
|
+
const diff = now.getTime() - date.getTime();
|
|
165
|
+
const sameDay = date.toDateString() === now.toDateString();
|
|
166
|
+
if (diff < 60_000)
|
|
167
|
+
return { kind: 'now' };
|
|
168
|
+
if (sameDay && diff < 3_600_000)
|
|
169
|
+
return { kind: 'minutes', value: Math.floor(diff / 60_000) };
|
|
170
|
+
if (sameDay)
|
|
171
|
+
return { kind: 'hours', value: Math.floor(diff / 3_600_000) };
|
|
172
|
+
const yesterday = new Date(now);
|
|
173
|
+
yesterday.setDate(now.getDate() - 1);
|
|
174
|
+
if (date.toDateString() === yesterday.toDateString())
|
|
175
|
+
return { kind: 'yesterday' };
|
|
176
|
+
return { kind: 'date' };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const RATINGS_TABLE_PAGE_SIZE = 10;
|
|
180
|
+
/** Valor do filtro de área para as telas sem área (o vazio já é "todas"). */
|
|
181
|
+
const NO_AREA$1 = '__none__';
|
|
182
|
+
const SPARK_W = 64;
|
|
183
|
+
const SPARK_H = 20;
|
|
184
|
+
/**
|
|
185
|
+
* Tabela compacta das telas (ou das áreas) na aba do resumo: busca, filtro de área (telas) e de faixa da nota,
|
|
186
|
+
* ordenação (piores primeiro por padrão: é onde agir) e paginação no navegador -- o resumo já traz todas as telas.
|
|
187
|
+
* Clicar numa linha emite `picked` (quem usa decide: abrir as avaliações da tela, filtrar pela área).
|
|
188
|
+
*/
|
|
189
|
+
class RatingsTableComponent {
|
|
190
|
+
constructor() {
|
|
191
|
+
this.mode = input('screens', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
|
|
192
|
+
this.rows = input.required(...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
|
|
193
|
+
/** Filtro de área (modo `screens`); quem usa pode preencher (clicar numa área). */
|
|
194
|
+
this.area = model('', ...(ngDevMode ? [{ debugName: "area" }] : /* istanbul ignore next */ []));
|
|
195
|
+
/** Linha marcada (a tela cujas avaliações estão abertas). */
|
|
196
|
+
this.selected = input(null, ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
|
|
197
|
+
this.picked = output();
|
|
198
|
+
this.messages = inject(SPINE_MESSAGES);
|
|
199
|
+
this.text = this.messages.ratings;
|
|
200
|
+
this.locale = this.messages.meta.locale;
|
|
201
|
+
this.areaFallback = RATINGS_AREA_FALLBACK_ICON;
|
|
202
|
+
this.search = signal('', ...(ngDevMode ? [{ debugName: "search" }] : /* istanbul ignore next */ []));
|
|
203
|
+
this.band = signal('', ...(ngDevMode ? [{ debugName: "band" }] : /* istanbul ignore next */ []));
|
|
204
|
+
this.sort = signal('worst', ...(ngDevMode ? [{ debugName: "sort" }] : /* istanbul ignore next */ []));
|
|
205
|
+
this.pageState = signal({ page: 1, size: RATINGS_TABLE_PAGE_SIZE }, ...(ngDevMode ? [{ debugName: "pageState" }] : /* istanbul ignore next */ []));
|
|
206
|
+
this.sortOptions = computed(() => ['worst', 'best', 'most'].map((value) => ({ value, label: this.text.sortsShort[value] })), ...(ngDevMode ? [{ debugName: "sortOptions" }] : /* istanbul ignore next */ []));
|
|
207
|
+
this.bandOptions = computed(() => [
|
|
208
|
+
{ value: '', label: this.text.bands.all },
|
|
209
|
+
...['bad', 'mid', 'good'].map((value) => ({ value, label: this.text.bands[value] })),
|
|
210
|
+
], ...(ngDevMode ? [{ debugName: "bandOptions" }] : /* istanbul ignore next */ []));
|
|
211
|
+
this.areaOptions = computed(() => {
|
|
212
|
+
const names = [...new Set(this.rows().map((r) => r.area ?? ''))].sort((a, b) => a.localeCompare(b));
|
|
213
|
+
return [
|
|
214
|
+
{ value: '', label: this.text.allAreas },
|
|
215
|
+
...names.map((n) => ({ value: n || NO_AREA$1, label: n || this.text.noArea })),
|
|
216
|
+
];
|
|
217
|
+
}, ...(ngDevMode ? [{ debugName: "areaOptions" }] : /* istanbul ignore next */ []));
|
|
218
|
+
this.filtered = computed(() => {
|
|
219
|
+
const q = this.search().trim().toLocaleLowerCase(this.locale);
|
|
220
|
+
const band = this.band();
|
|
221
|
+
const area = this.mode() === 'screens' ? this.area() : '';
|
|
222
|
+
const rows = this.rows().filter((r) => (!q || `${r.name} ${r.sub ?? ''} ${r.area ?? ''}`.toLocaleLowerCase(this.locale).includes(q)) &&
|
|
223
|
+
(!band || ratingBand(r.agg.average) === band) &&
|
|
224
|
+
(!area || (area === NO_AREA$1 ? !r.area : r.area === area)));
|
|
225
|
+
return sortRows(rows, this.sort());
|
|
226
|
+
}, ...(ngDevMode ? [{ debugName: "filtered" }] : /* istanbul ignore next */ []));
|
|
227
|
+
this.page = computed(() => {
|
|
228
|
+
const { page, size } = this.pageState();
|
|
229
|
+
return this.filtered().slice((page - 1) * size, page * size);
|
|
230
|
+
}, ...(ngDevMode ? [{ debugName: "page" }] : /* istanbul ignore next */ []));
|
|
231
|
+
this.averageFormat = new Intl.NumberFormat(this.locale, { minimumFractionDigits: 1, maximumFractionDigits: 1 });
|
|
232
|
+
this.numberFormat = new Intl.NumberFormat(this.locale, { maximumFractionDigits: 1 });
|
|
233
|
+
}
|
|
234
|
+
setSearch(v) { this.search.set(v); this.toFirstPage(); }
|
|
235
|
+
setBand(v) { this.band.set(v); this.toFirstPage(); }
|
|
236
|
+
setArea(v) { this.area.set(v); this.toFirstPage(); }
|
|
237
|
+
setSort(v) { this.sort.set(v); this.toFirstPage(); }
|
|
238
|
+
onPage(state) { this.pageState.set(state); }
|
|
239
|
+
toFirstPage() { this.pageState.update((s) => ({ ...s, page: 1 })); }
|
|
240
|
+
pick(row) { this.picked.emit(row); }
|
|
241
|
+
hue(row) { return screenHue(row.key); }
|
|
242
|
+
tone(row) { return ratingTone(row.agg.average); }
|
|
243
|
+
figure(a) { return a.average == null ? '—' : this.averageFormat.format(a.average); }
|
|
244
|
+
scoreOf(v) { return formatMessage(this.text.scoreOf, { value: this.numberFormat.format(v) }); }
|
|
245
|
+
rowLabel(row) {
|
|
246
|
+
return formatMessage(this.mode() === 'screens' ? this.text.filterByScreen : this.text.filterByArea, { screen: row.name, area: row.name });
|
|
247
|
+
}
|
|
248
|
+
screensCount(n) { return pluralMessage(this.text.screensCount, n, this.locale); }
|
|
249
|
+
/** Sparkline: a média semana a semana, na cor da faixa da média do período. */
|
|
250
|
+
spark(row) { return linePaths(row.trend ?? [], SPARK_W, SPARK_H); }
|
|
251
|
+
/** O que a sparkline diz, para o leitor: "Média por semana: de 6 para 7,5". */
|
|
252
|
+
sparkLabel(row) {
|
|
253
|
+
const ends = trendEnds(row.trend);
|
|
254
|
+
return ends
|
|
255
|
+
? formatMessage(this.text.trendOf, { first: this.numberFormat.format(ends.first), last: this.numberFormat.format(ends.last) })
|
|
256
|
+
: this.text.trendNone;
|
|
257
|
+
}
|
|
258
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RatingsTableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
259
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: RatingsTableComponent, isStandalone: true, selector: "spine-ratings-table", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: true, transformFunction: null }, area: { classPropertyName: "area", publicName: "area", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { area: "areaChange", picked: "picked" }, ngImport: i0, template: "<div class=\"rt\">\n <div class=\"rt__controls\">\n <hm-search-field\n class=\"rt__search\"\n size=\"small\"\n labelHidden\n [landmark]=\"false\"\n [label]=\"mode() === 'screens' ? text.searchScreensLabel : text.searchAreasLabel\"\n [placeholder]=\"mode() === 'screens' ? text.searchScreens : text.searchAreas\"\n [value]=\"search()\"\n (valueChange)=\"setSearch($event)\"\n />\n @if (mode() === 'screens') {\n <hm-combobox class=\"rt__filter\" size=\"small\" labelHidden [label]=\"text.filterArea\" [options]=\"areaOptions()\" [value]=\"area()\" (valueChange)=\"setArea($event)\" />\n }\n <hm-combobox class=\"rt__filter\" size=\"small\" labelHidden [label]=\"text.bandFilter\" [options]=\"bandOptions()\" [value]=\"band()\" (valueChange)=\"setBand($event)\" />\n <hm-segmented-control class=\"rt__sort\" size=\"small\" [label]=\"text.sort\" [options]=\"sortOptions()\" [value]=\"sort()\" (valueChange)=\"setSort($event)\" />\n </div>\n\n @if (!filtered().length) {\n <p class=\"rt__empty\">{{ rows().length ? (mode() === 'screens' ? text.tableEmpty : text.areasEmpty) : text.noScreens }}</p>\n } @else {\n <div class=\"rt__scroll\">\n <table class=\"rt__table\">\n <thead>\n <tr>\n <th scope=\"col\">{{ mode() === 'screens' ? text.colScreen : text.colArea }}</th>\n @if (mode() === 'screens') {\n <th scope=\"col\" class=\"rt__c-area\">{{ text.colArea }}</th>\n } @else {\n <th scope=\"col\" class=\"rt__num rt__c-area\">{{ text.colScreens }}</th>\n }\n <th scope=\"col\">{{ text.colScore }}</th>\n <th scope=\"col\" class=\"rt__num\">{{ text.colTotal }}</th>\n <th scope=\"col\" class=\"rt__num rt__c-comments\">{{ text.colComments }}</th>\n <th scope=\"col\" class=\"rt__c-trend\">{{ text.colTrend }}</th>\n <th scope=\"col\" class=\"rt__c-chev\"><span class=\"rt__sr\">{{ text.colOpen }}</span></th>\n </tr>\n </thead>\n <tbody>\n @for (row of page(); track row.key) {\n <tr class=\"rt__row\" [class.rt__row--on]=\"selected() === row.key\" (click)=\"pick(row)\">\n <td>\n <div class=\"rt__name\">\n <span class=\"rt__glyph rt__glyph--c{{ hue(row) }}\" aria-hidden=\"true\"><hm-icon [shape]=\"row.icon\" /></span>\n <span class=\"rt__titles\">\n <!-- O bot\u00E3o \u00E9 o alvo do teclado; o clique em qualquer lugar da linha sobe at\u00E9 o (click) da linha. -->\n <button type=\"button\" class=\"rt__link\" [attr.aria-pressed]=\"selected() === row.key\" [attr.title]=\"rowLabel(row)\">{{ row.name }}</button>\n @if (row.sub) { <span class=\"rt__sub\">{{ row.sub }}</span> }\n </span>\n </div>\n </td>\n @if (mode() === 'screens') {\n <td class=\"rt__c-area\">\n @if (row.area) {\n <span class=\"rt__area\"><hm-icon class=\"rt__area-icon\" [shape]=\"row.areaIcon || areaFallback\" aria-hidden=\"true\" /><span class=\"rt__area-name\">{{ row.area }}</span></span>\n } @else {\n <span class=\"rt__muted\">{{ text.noArea }}</span>\n }\n </td>\n } @else {\n <td class=\"rt__num rt__c-area\">{{ row.screens }}</td>\n }\n <td>\n <span class=\"rt__score\">\n <span class=\"rt__figure\" aria-hidden=\"true\">{{ figure(row.agg) }}</span>\n <spine-rating-stars size=\"small\" [readonly]=\"true\" [showValue]=\"false\" [value]=\"row.agg.average\" />\n </span>\n </td>\n <td class=\"rt__num\">{{ row.agg.total }}</td>\n <td class=\"rt__num rt__c-comments\">{{ row.agg.comments }}</td>\n <td class=\"rt__c-trend\">\n <span class=\"rt__spark-wrap\" role=\"img\" [attr.aria-label]=\"sparkLabel(row)\">\n @if (spark(row).length) {\n <svg class=\"rt__spark rt__spark--{{ tone(row) }}\" viewBox=\"0 0 64 20\" preserveAspectRatio=\"none\" aria-hidden=\"true\">\n <line class=\"rt__spark-mid\" x1=\"0\" x2=\"64\" y1=\"10\" y2=\"10\" />\n @for (d of spark(row); track $index) {\n <polyline [attr.points]=\"d\" />\n }\n </svg>\n } @else {\n <span class=\"rt__muted\" aria-hidden=\"true\">\u2014</span>\n }\n </span>\n </td>\n <td class=\"rt__c-chev\"><hm-icon shape=\"angle-right\" aria-hidden=\"true\" /></td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n <hm-paginator\n class=\"rt__pager\"\n [label]=\"mode() === 'screens' ? text.tablePagination : text.areasPagination\"\n [state]=\"pageState()\"\n [totalItems]=\"filtered().length\"\n [sizes]=\"[10, 25, 50]\"\n (stateChanged)=\"onPage($event)\"\n />\n }\n</div>\n", styles: [":host{display:block;min-inline-size:0;container-type:inline-size}.rt{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-inline-size:0}.rt__muted{color:var(--hm-muted)}.rt__sr{position:absolute;inline-size:1px;block-size:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.rt__controls{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs) var(--hm-space-sm)}.rt__search{flex:1 1 14rem;min-inline-size:0}.rt__filter{flex:0 1 11rem;min-inline-size:8rem}.rt__sort{flex:none;margin-inline-start:auto}.rt__empty{margin:0;padding:var(--hm-space-md);border-radius:var(--hm-radius-control);background:var(--hm-surface-muted);color:var(--hm-muted);text-align:center}.rt__scroll{min-inline-size:0;overflow-x:auto}.rt__table{inline-size:100%;border-collapse:separate;border-spacing:0;font-size:var(--hm-font-size-sm)}.rt__table th{padding:var(--hm-space-xs) var(--hm-space-sm);background:var(--hm-table-head-bg);color:var(--hm-table-head-text);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);text-align:start;white-space:nowrap}.rt__table th:first-child{border-start-start-radius:var(--hm-radius-control);border-end-start-radius:var(--hm-radius-control)}.rt__table th:last-child{border-start-end-radius:var(--hm-radius-control);border-end-end-radius:var(--hm-radius-control)}.rt__table td{padding:var(--hm-space-xs) var(--hm-space-sm);border-block-end:1px solid var(--hm-divider);color:var(--hm-text);vertical-align:middle}.rt__table .rt__num{font-variant-numeric:tabular-nums;text-align:end}.rt__row{cursor:pointer}.rt__row:hover td{background:var(--hm-surface-muted)}.rt__row--on td{background:color-mix(in srgb,var(--hm-action) 10%,transparent)}.rt__row--on td:first-child{box-shadow:inset 3px 0 0 var(--hm-action)}.rt__name{display:flex;align-items:center;gap:var(--hm-space-sm);min-inline-size:0}.rt__glyph{display:inline-flex;flex:none;align-items:center;justify-content:center;inline-size:2rem;block-size:2rem;border-radius:var(--hm-radius);font-size:var(--hm-font-size-md)}.rt__glyph--c1{background:color-mix(in srgb,var(--hm-data-categorical-1) 14%,transparent);color:var(--hm-data-categorical-1)}.rt__glyph--c2{background:color-mix(in srgb,var(--hm-data-categorical-2) 14%,transparent);color:var(--hm-data-categorical-2)}.rt__glyph--c3{background:color-mix(in srgb,var(--hm-data-categorical-3) 14%,transparent);color:var(--hm-data-categorical-3)}.rt__glyph--c4{background:color-mix(in srgb,var(--hm-data-categorical-4) 14%,transparent);color:var(--hm-data-categorical-4)}.rt__glyph--c5{background:color-mix(in srgb,var(--hm-data-categorical-5) 14%,transparent);color:var(--hm-data-categorical-5)}.rt__glyph--c6{background:color-mix(in srgb,var(--hm-data-categorical-6) 14%,transparent);color:var(--hm-data-categorical-6)}.rt__glyph--c7{background:color-mix(in srgb,var(--hm-data-categorical-7) 14%,transparent);color:var(--hm-data-categorical-7)}.rt__glyph--c8{background:color-mix(in srgb,var(--hm-data-categorical-8) 14%,transparent);color:var(--hm-data-categorical-8)}.rt__titles{display:flex;flex-direction:column;min-inline-size:0}.rt__link{padding:0;overflow:hidden;border:0;background:none;color:var(--hm-text);font:inherit;font-weight:var(--hm-font-weight-semibold);text-align:start;text-overflow:ellipsis;white-space:nowrap;cursor:pointer}.rt__link:focus-visible{border-radius:var(--hm-radius-micro);outline:2px solid var(--hm-focus-color);outline-offset:2px}.rt__sub{overflow:hidden;color:var(--hm-muted);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-xs);text-overflow:ellipsis;white-space:nowrap}.rt__area{display:inline-flex;align-items:center;gap:var(--hm-space-xs);max-inline-size:16rem;color:var(--hm-text)}.rt__area-icon{flex:none;color:var(--hm-muted)}.rt__area-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rt__score{display:inline-flex;align-items:center;gap:var(--hm-space-xs);white-space:nowrap}.rt__figure{min-inline-size:2rem;font-variant-numeric:tabular-nums;font-weight:var(--hm-font-weight-semibold)}.rt__spark-wrap{display:inline-flex;align-items:center}.rt__spark{display:block;inline-size:4rem;block-size:1.25rem;overflow:visible}.rt__spark polyline{fill:none;stroke:var(--hm-muted);stroke-width:2;stroke-linecap:round;stroke-linejoin:round;vector-effect:non-scaling-stroke}.rt__spark--success polyline{stroke:var(--hm-success)}.rt__spark--warning polyline{stroke:var(--hm-warning)}.rt__spark--danger polyline{stroke:var(--hm-danger)}.rt__spark-mid{stroke:var(--hm-divider);stroke-dasharray:2 2;stroke-width:1;vector-effect:non-scaling-stroke}.rt__c-chev{inline-size:1.5rem;color:var(--hm-muted);text-align:end}.rt__pager{margin-block-start:var(--hm-space-micro)}@container (max-width: 44rem){.rt__c-trend,.rt__c-comments{display:none}}@container (max-width: 32rem){.rt__c-area{display:none}.rt__sort{margin-inline-start:0}}\n"], dependencies: [{ kind: "component", type: ComboboxComponent, selector: "hm-combobox", inputs: ["label", "options", "value", "placeholder", "icon", "hint", "size", "labelHidden", "clearable", "loading", "allowCustomValue"], outputs: ["valueChange"] }, { kind: "component", type: IconComponent, selector: "hm-icon", inputs: ["shape", "direction", "size", "solid", "badge", "status", "flip", "inverse", "label"] }, { kind: "component", type: PaginatorComponent, selector: "hm-paginator", inputs: ["state", "totalItems", "variant", "label", "showPage", "hasNext", "hasPrevious", "sizes", "maxNumbers"], outputs: ["stateChanged", "nextPage", "previousPage"] }, { kind: "component", type: SearchFieldComponent, selector: "hm-search-field", inputs: ["value", "label", "labelHidden", "placeholder", "hint", "size", "debounce", "landmark", "loading"], outputs: ["valueChange", "search", "cleared"] }, { kind: "component", type: SegmentedControlComponent, selector: "hm-segmented-control", inputs: ["options", "value", "label", "size", "emphasis"], outputs: ["valueChange"] }, { kind: "component", type: SpineRatingStarsComponent, selector: "spine-rating-stars", inputs: ["value", "readonly", "showValue", "showName", "size", "label", "labelledBy", "describedBy", "invalid"], outputs: ["valueChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
260
|
+
}
|
|
261
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RatingsTableComponent, decorators: [{
|
|
262
|
+
type: Component,
|
|
263
|
+
args: [{ selector: 'spine-ratings-table', imports: [ComboboxComponent, IconComponent, PaginatorComponent, SearchFieldComponent, SegmentedControlComponent, SpineRatingStarsComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"rt\">\n <div class=\"rt__controls\">\n <hm-search-field\n class=\"rt__search\"\n size=\"small\"\n labelHidden\n [landmark]=\"false\"\n [label]=\"mode() === 'screens' ? text.searchScreensLabel : text.searchAreasLabel\"\n [placeholder]=\"mode() === 'screens' ? text.searchScreens : text.searchAreas\"\n [value]=\"search()\"\n (valueChange)=\"setSearch($event)\"\n />\n @if (mode() === 'screens') {\n <hm-combobox class=\"rt__filter\" size=\"small\" labelHidden [label]=\"text.filterArea\" [options]=\"areaOptions()\" [value]=\"area()\" (valueChange)=\"setArea($event)\" />\n }\n <hm-combobox class=\"rt__filter\" size=\"small\" labelHidden [label]=\"text.bandFilter\" [options]=\"bandOptions()\" [value]=\"band()\" (valueChange)=\"setBand($event)\" />\n <hm-segmented-control class=\"rt__sort\" size=\"small\" [label]=\"text.sort\" [options]=\"sortOptions()\" [value]=\"sort()\" (valueChange)=\"setSort($event)\" />\n </div>\n\n @if (!filtered().length) {\n <p class=\"rt__empty\">{{ rows().length ? (mode() === 'screens' ? text.tableEmpty : text.areasEmpty) : text.noScreens }}</p>\n } @else {\n <div class=\"rt__scroll\">\n <table class=\"rt__table\">\n <thead>\n <tr>\n <th scope=\"col\">{{ mode() === 'screens' ? text.colScreen : text.colArea }}</th>\n @if (mode() === 'screens') {\n <th scope=\"col\" class=\"rt__c-area\">{{ text.colArea }}</th>\n } @else {\n <th scope=\"col\" class=\"rt__num rt__c-area\">{{ text.colScreens }}</th>\n }\n <th scope=\"col\">{{ text.colScore }}</th>\n <th scope=\"col\" class=\"rt__num\">{{ text.colTotal }}</th>\n <th scope=\"col\" class=\"rt__num rt__c-comments\">{{ text.colComments }}</th>\n <th scope=\"col\" class=\"rt__c-trend\">{{ text.colTrend }}</th>\n <th scope=\"col\" class=\"rt__c-chev\"><span class=\"rt__sr\">{{ text.colOpen }}</span></th>\n </tr>\n </thead>\n <tbody>\n @for (row of page(); track row.key) {\n <tr class=\"rt__row\" [class.rt__row--on]=\"selected() === row.key\" (click)=\"pick(row)\">\n <td>\n <div class=\"rt__name\">\n <span class=\"rt__glyph rt__glyph--c{{ hue(row) }}\" aria-hidden=\"true\"><hm-icon [shape]=\"row.icon\" /></span>\n <span class=\"rt__titles\">\n <!-- O bot\u00E3o \u00E9 o alvo do teclado; o clique em qualquer lugar da linha sobe at\u00E9 o (click) da linha. -->\n <button type=\"button\" class=\"rt__link\" [attr.aria-pressed]=\"selected() === row.key\" [attr.title]=\"rowLabel(row)\">{{ row.name }}</button>\n @if (row.sub) { <span class=\"rt__sub\">{{ row.sub }}</span> }\n </span>\n </div>\n </td>\n @if (mode() === 'screens') {\n <td class=\"rt__c-area\">\n @if (row.area) {\n <span class=\"rt__area\"><hm-icon class=\"rt__area-icon\" [shape]=\"row.areaIcon || areaFallback\" aria-hidden=\"true\" /><span class=\"rt__area-name\">{{ row.area }}</span></span>\n } @else {\n <span class=\"rt__muted\">{{ text.noArea }}</span>\n }\n </td>\n } @else {\n <td class=\"rt__num rt__c-area\">{{ row.screens }}</td>\n }\n <td>\n <span class=\"rt__score\">\n <span class=\"rt__figure\" aria-hidden=\"true\">{{ figure(row.agg) }}</span>\n <spine-rating-stars size=\"small\" [readonly]=\"true\" [showValue]=\"false\" [value]=\"row.agg.average\" />\n </span>\n </td>\n <td class=\"rt__num\">{{ row.agg.total }}</td>\n <td class=\"rt__num rt__c-comments\">{{ row.agg.comments }}</td>\n <td class=\"rt__c-trend\">\n <span class=\"rt__spark-wrap\" role=\"img\" [attr.aria-label]=\"sparkLabel(row)\">\n @if (spark(row).length) {\n <svg class=\"rt__spark rt__spark--{{ tone(row) }}\" viewBox=\"0 0 64 20\" preserveAspectRatio=\"none\" aria-hidden=\"true\">\n <line class=\"rt__spark-mid\" x1=\"0\" x2=\"64\" y1=\"10\" y2=\"10\" />\n @for (d of spark(row); track $index) {\n <polyline [attr.points]=\"d\" />\n }\n </svg>\n } @else {\n <span class=\"rt__muted\" aria-hidden=\"true\">\u2014</span>\n }\n </span>\n </td>\n <td class=\"rt__c-chev\"><hm-icon shape=\"angle-right\" aria-hidden=\"true\" /></td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n <hm-paginator\n class=\"rt__pager\"\n [label]=\"mode() === 'screens' ? text.tablePagination : text.areasPagination\"\n [state]=\"pageState()\"\n [totalItems]=\"filtered().length\"\n [sizes]=\"[10, 25, 50]\"\n (stateChanged)=\"onPage($event)\"\n />\n }\n</div>\n", styles: [":host{display:block;min-inline-size:0;container-type:inline-size}.rt{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-inline-size:0}.rt__muted{color:var(--hm-muted)}.rt__sr{position:absolute;inline-size:1px;block-size:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.rt__controls{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs) var(--hm-space-sm)}.rt__search{flex:1 1 14rem;min-inline-size:0}.rt__filter{flex:0 1 11rem;min-inline-size:8rem}.rt__sort{flex:none;margin-inline-start:auto}.rt__empty{margin:0;padding:var(--hm-space-md);border-radius:var(--hm-radius-control);background:var(--hm-surface-muted);color:var(--hm-muted);text-align:center}.rt__scroll{min-inline-size:0;overflow-x:auto}.rt__table{inline-size:100%;border-collapse:separate;border-spacing:0;font-size:var(--hm-font-size-sm)}.rt__table th{padding:var(--hm-space-xs) var(--hm-space-sm);background:var(--hm-table-head-bg);color:var(--hm-table-head-text);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);text-align:start;white-space:nowrap}.rt__table th:first-child{border-start-start-radius:var(--hm-radius-control);border-end-start-radius:var(--hm-radius-control)}.rt__table th:last-child{border-start-end-radius:var(--hm-radius-control);border-end-end-radius:var(--hm-radius-control)}.rt__table td{padding:var(--hm-space-xs) var(--hm-space-sm);border-block-end:1px solid var(--hm-divider);color:var(--hm-text);vertical-align:middle}.rt__table .rt__num{font-variant-numeric:tabular-nums;text-align:end}.rt__row{cursor:pointer}.rt__row:hover td{background:var(--hm-surface-muted)}.rt__row--on td{background:color-mix(in srgb,var(--hm-action) 10%,transparent)}.rt__row--on td:first-child{box-shadow:inset 3px 0 0 var(--hm-action)}.rt__name{display:flex;align-items:center;gap:var(--hm-space-sm);min-inline-size:0}.rt__glyph{display:inline-flex;flex:none;align-items:center;justify-content:center;inline-size:2rem;block-size:2rem;border-radius:var(--hm-radius);font-size:var(--hm-font-size-md)}.rt__glyph--c1{background:color-mix(in srgb,var(--hm-data-categorical-1) 14%,transparent);color:var(--hm-data-categorical-1)}.rt__glyph--c2{background:color-mix(in srgb,var(--hm-data-categorical-2) 14%,transparent);color:var(--hm-data-categorical-2)}.rt__glyph--c3{background:color-mix(in srgb,var(--hm-data-categorical-3) 14%,transparent);color:var(--hm-data-categorical-3)}.rt__glyph--c4{background:color-mix(in srgb,var(--hm-data-categorical-4) 14%,transparent);color:var(--hm-data-categorical-4)}.rt__glyph--c5{background:color-mix(in srgb,var(--hm-data-categorical-5) 14%,transparent);color:var(--hm-data-categorical-5)}.rt__glyph--c6{background:color-mix(in srgb,var(--hm-data-categorical-6) 14%,transparent);color:var(--hm-data-categorical-6)}.rt__glyph--c7{background:color-mix(in srgb,var(--hm-data-categorical-7) 14%,transparent);color:var(--hm-data-categorical-7)}.rt__glyph--c8{background:color-mix(in srgb,var(--hm-data-categorical-8) 14%,transparent);color:var(--hm-data-categorical-8)}.rt__titles{display:flex;flex-direction:column;min-inline-size:0}.rt__link{padding:0;overflow:hidden;border:0;background:none;color:var(--hm-text);font:inherit;font-weight:var(--hm-font-weight-semibold);text-align:start;text-overflow:ellipsis;white-space:nowrap;cursor:pointer}.rt__link:focus-visible{border-radius:var(--hm-radius-micro);outline:2px solid var(--hm-focus-color);outline-offset:2px}.rt__sub{overflow:hidden;color:var(--hm-muted);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-xs);text-overflow:ellipsis;white-space:nowrap}.rt__area{display:inline-flex;align-items:center;gap:var(--hm-space-xs);max-inline-size:16rem;color:var(--hm-text)}.rt__area-icon{flex:none;color:var(--hm-muted)}.rt__area-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rt__score{display:inline-flex;align-items:center;gap:var(--hm-space-xs);white-space:nowrap}.rt__figure{min-inline-size:2rem;font-variant-numeric:tabular-nums;font-weight:var(--hm-font-weight-semibold)}.rt__spark-wrap{display:inline-flex;align-items:center}.rt__spark{display:block;inline-size:4rem;block-size:1.25rem;overflow:visible}.rt__spark polyline{fill:none;stroke:var(--hm-muted);stroke-width:2;stroke-linecap:round;stroke-linejoin:round;vector-effect:non-scaling-stroke}.rt__spark--success polyline{stroke:var(--hm-success)}.rt__spark--warning polyline{stroke:var(--hm-warning)}.rt__spark--danger polyline{stroke:var(--hm-danger)}.rt__spark-mid{stroke:var(--hm-divider);stroke-dasharray:2 2;stroke-width:1;vector-effect:non-scaling-stroke}.rt__c-chev{inline-size:1.5rem;color:var(--hm-muted);text-align:end}.rt__pager{margin-block-start:var(--hm-space-micro)}@container (max-width: 44rem){.rt__c-trend,.rt__c-comments{display:none}}@container (max-width: 32rem){.rt__c-area{display:none}.rt__sort{margin-inline-start:0}}\n"] }]
|
|
264
|
+
}], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: true }] }], area: [{ type: i0.Input, args: [{ isSignal: true, alias: "area", required: false }] }, { type: i0.Output, args: ["areaChange"] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], picked: [{ type: i0.Output, args: ["picked"] }] } });
|
|
265
|
+
|
|
266
|
+
/** Largura e altura do quadro do SVG (esticado na largura; o traço não escala). */
|
|
267
|
+
const W = 100;
|
|
268
|
+
const H = 100;
|
|
269
|
+
/**
|
|
270
|
+
* A média semana a semana (0 a 10) da ferramenta e das telas, em SVG próprio (o spine não depende do pacote de
|
|
271
|
+
* gráficos): grade recessiva em 0, 5 e 10, uma linha por série (a segunda tracejada, para não depender só da cor) com
|
|
272
|
+
* os pontos, legenda sempre visível e, em cada semana, uma coluna focável com a dica "Semana de 19/10: A ferramenta
|
|
273
|
+
* 8,5 · As telas 6,2" (mouse e teclado; o mesmo texto vai para o leitor). Semana sem nota quebra a linha.
|
|
274
|
+
*/
|
|
275
|
+
class RatingsTrendComponent {
|
|
276
|
+
constructor() {
|
|
277
|
+
this.series = input.required(...(ngDevMode ? [{ debugName: "series" }] : /* istanbul ignore next */ []));
|
|
278
|
+
this.messages = inject(SPINE_MESSAGES);
|
|
279
|
+
this.text = this.messages.ratings;
|
|
280
|
+
this.locale = this.messages.meta.locale;
|
|
281
|
+
this.numberFormat = new Intl.NumberFormat(this.locale, { maximumFractionDigits: 1 });
|
|
282
|
+
this.weekFormat = new Intl.DateTimeFormat(this.locale, { day: '2-digit', month: '2-digit', timeZone: 'UTC' });
|
|
283
|
+
/** As semanas (as da primeira série; o servidor manda as mesmas em todas). */
|
|
284
|
+
this.weeks = computed(() => this.series()[0]?.points.map((p) => p.week) ?? [], ...(ngDevMode ? [{ debugName: "weeks" }] : /* istanbul ignore next */ []));
|
|
285
|
+
this.hasData = computed(() => this.series().some((s) => s.points.some((p) => p.total > 0)), ...(ngDevMode ? [{ debugName: "hasData" }] : /* istanbul ignore next */ []));
|
|
286
|
+
this.lines = computed(() => this.series().map((s) => ({ ...s, paths: linePaths(s.points, W, H, 0), dots: this.dots(s) })), ...(ngDevMode ? [{ debugName: "lines" }] : /* istanbul ignore next */ []));
|
|
287
|
+
/** Rótulos do eixo x: todas as semanas até 6; mais que isso, uma sim, uma não (sempre a última). */
|
|
288
|
+
this.ticks = computed(() => {
|
|
289
|
+
const weeks = this.weeks();
|
|
290
|
+
const step = weeks.length > 6 ? 2 : 1;
|
|
291
|
+
return weeks.map((w, i) => ((weeks.length - 1 - i) % step === 0 ? this.weekLabel(w) : ''));
|
|
292
|
+
}, ...(ngDevMode ? [{ debugName: "ticks" }] : /* istanbul ignore next */ []));
|
|
293
|
+
/** Uma coluna por semana: a dica e o nome para o leitor. */
|
|
294
|
+
this.columns = computed(() => {
|
|
295
|
+
const series = this.series();
|
|
296
|
+
return this.weeks().map((week, i) => {
|
|
297
|
+
const values = series.map((s) => {
|
|
298
|
+
const p = s.points[i];
|
|
299
|
+
const value = p?.average == null ? this.text.noScore : this.numberFormat.format(p.average);
|
|
300
|
+
return formatMessage(this.text.trendValue, { series: s.label, value });
|
|
301
|
+
});
|
|
302
|
+
const total = series.reduce((a, s) => a + (s.points[i]?.total ?? 0), 0);
|
|
303
|
+
return formatMessage(this.text.trendWeek, {
|
|
304
|
+
week: this.weekLabel(week), values: values.join(' · '), count: pluralMessage(this.text.total, total, this.locale),
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
}, ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
|
|
308
|
+
}
|
|
309
|
+
dots(s) {
|
|
310
|
+
const n = s.points.length;
|
|
311
|
+
return s.points.flatMap((p, i) => (p.average == null ? [] : [{ x: (weekX(i, n, W) / W) * 100, y: (p.average / 10) * 100 }]));
|
|
312
|
+
}
|
|
313
|
+
weekLabel(week) {
|
|
314
|
+
const d = new Date(`${week}T00:00:00Z`);
|
|
315
|
+
return Number.isNaN(d.getTime()) ? week : this.weekFormat.format(d);
|
|
316
|
+
}
|
|
317
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RatingsTrendComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
318
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: RatingsTrendComponent, isStandalone: true, selector: "spine-ratings-trend", inputs: { series: { classPropertyName: "series", publicName: "series", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "@if (!hasData()) {\n <p class=\"tr__empty\">{{ text.trendEmpty }}</p>\n} @else {\n <figure class=\"tr\">\n <figcaption class=\"tr__caption\">{{ text.trendCaption }}</figcaption>\n <ul class=\"tr__legend\">\n @for (s of lines(); track s.id) {\n <li class=\"tr__legend-item\"><span class=\"tr__swatch tr__swatch--{{ s.hue }}\" aria-hidden=\"true\"></span>{{ s.label }}</li>\n }\n </ul>\n\n <div class=\"tr__plot\">\n <div class=\"tr__y\" aria-hidden=\"true\"><span>10</span><span>5</span><span>0</span></div>\n <div class=\"tr__area\">\n <svg class=\"tr__svg\" viewBox=\"0 0 100 100\" preserveAspectRatio=\"none\" aria-hidden=\"true\">\n <line class=\"tr__grid\" x1=\"0\" x2=\"100\" y1=\"0\" y2=\"0\" />\n <line class=\"tr__grid\" x1=\"0\" x2=\"100\" y1=\"50\" y2=\"50\" />\n <line class=\"tr__grid tr__grid--base\" x1=\"0\" x2=\"100\" y1=\"100\" y2=\"100\" />\n @for (s of lines(); track s.id) {\n @for (d of s.paths; track $index) {\n <polyline class=\"tr__line tr__line--{{ s.hue }}\" [attr.points]=\"d\" />\n }\n }\n </svg>\n <!-- Pontos em HTML (o SVG estica na largura e deixaria o c\u00EDrculo oval). -->\n @for (s of lines(); track s.id) {\n @for (p of s.dots; track $index) {\n <span class=\"tr__dot tr__dot--{{ s.hue }}\" [style.left.%]=\"p.x\" [style.bottom.%]=\"p.y\" aria-hidden=\"true\"></span>\n }\n }\n <ol class=\"tr__cols\" [attr.aria-label]=\"text.trendCaption\">\n @for (label of columns(); track $index) {\n <li class=\"tr__col\" tabindex=\"0\" [attr.aria-label]=\"label\" [hmTooltip]=\"label\"></li>\n }\n </ol>\n </div>\n </div>\n <div class=\"tr__x\" aria-hidden=\"true\">\n @for (t of ticks(); track $index) {\n <span class=\"tr__tick\">{{ t }}</span>\n }\n </div>\n </figure>\n}\n", styles: [":host{display:block;min-inline-size:0}.tr{display:flex;flex-direction:column;gap:var(--hm-space-xs);margin:0}.tr__caption{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.tr__empty{margin:0;padding:var(--hm-space-md);border-radius:var(--hm-radius-control);background:var(--hm-surface-muted);color:var(--hm-muted);text-align:center}.tr__legend{display:flex;flex-wrap:wrap;gap:var(--hm-space-xs) var(--hm-space-md);margin:0;padding:0;color:var(--hm-text);font-size:var(--hm-font-size-xs);list-style:none}.tr__legend-item{display:inline-flex;align-items:center;gap:var(--hm-space-xs)}.tr__swatch{display:inline-block;inline-size:1rem;block-size:0;border-block-start:2px solid}.tr__swatch--1{border-color:var(--hm-data-categorical-1)}.tr__swatch--2{border-block-start-style:dashed;border-color:var(--hm-data-categorical-2)}.tr__plot,.tr__x{direction:ltr}.tr__plot{display:grid;grid-template-columns:1.5rem minmax(0,1fr);gap:var(--hm-space-xs)}.tr__y{display:flex;flex-direction:column;justify-content:space-between;block-size:11rem;margin-block:-.5em;color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-variant-numeric:tabular-nums;text-align:end}.tr__area{position:relative;block-size:11rem}.tr__svg{position:absolute;inset:0;inline-size:100%;block-size:100%;overflow:visible}.tr__grid{stroke:var(--hm-divider);stroke-width:1;stroke-dasharray:3 3;vector-effect:non-scaling-stroke}.tr__grid--base{stroke-dasharray:none}.tr__line{fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;vector-effect:non-scaling-stroke}.tr__line--1{stroke:var(--hm-data-categorical-1)}.tr__line--2{stroke:var(--hm-data-categorical-2);stroke-dasharray:6 4}.tr__dot{position:absolute;inline-size:8px;block-size:8px;border-radius:50%;box-shadow:0 0 0 2px var(--hm-surface-raised);transform:translate(-50%,50%);pointer-events:none}.tr__dot--1{background:var(--hm-data-categorical-1)}.tr__dot--2{background:var(--hm-data-categorical-2)}.tr__cols{position:absolute;inset:0;display:flex;margin:0;padding:0;list-style:none}.tr__col{flex:1 1 0;border-radius:var(--hm-radius-micro);outline:none}.tr__col:hover,.tr__col:focus-visible{background:color-mix(in srgb,var(--hm-text) 6%,transparent)}.tr__col:focus-visible{box-shadow:inset 0 0 0 2px var(--hm-focus-color)}.tr__x{display:grid;grid-auto-columns:minmax(0,1fr);grid-auto-flow:column;margin-inline-start:calc(1.5rem + var(--hm-space-xs));color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-variant-numeric:tabular-nums}.tr__tick{overflow:hidden;text-align:center;white-space:nowrap}\n"], dependencies: [{ kind: "directive", type: TooltipDirective, selector: "[hmTooltip]", inputs: ["hmTooltip", "hmTooltipSide", "hmTooltipDisabled", "hmTooltipShowDelay", "hmTooltipHideDelay"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
319
|
+
}
|
|
320
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RatingsTrendComponent, decorators: [{
|
|
321
|
+
type: Component,
|
|
322
|
+
args: [{ selector: 'spine-ratings-trend', imports: [TooltipDirective], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (!hasData()) {\n <p class=\"tr__empty\">{{ text.trendEmpty }}</p>\n} @else {\n <figure class=\"tr\">\n <figcaption class=\"tr__caption\">{{ text.trendCaption }}</figcaption>\n <ul class=\"tr__legend\">\n @for (s of lines(); track s.id) {\n <li class=\"tr__legend-item\"><span class=\"tr__swatch tr__swatch--{{ s.hue }}\" aria-hidden=\"true\"></span>{{ s.label }}</li>\n }\n </ul>\n\n <div class=\"tr__plot\">\n <div class=\"tr__y\" aria-hidden=\"true\"><span>10</span><span>5</span><span>0</span></div>\n <div class=\"tr__area\">\n <svg class=\"tr__svg\" viewBox=\"0 0 100 100\" preserveAspectRatio=\"none\" aria-hidden=\"true\">\n <line class=\"tr__grid\" x1=\"0\" x2=\"100\" y1=\"0\" y2=\"0\" />\n <line class=\"tr__grid\" x1=\"0\" x2=\"100\" y1=\"50\" y2=\"50\" />\n <line class=\"tr__grid tr__grid--base\" x1=\"0\" x2=\"100\" y1=\"100\" y2=\"100\" />\n @for (s of lines(); track s.id) {\n @for (d of s.paths; track $index) {\n <polyline class=\"tr__line tr__line--{{ s.hue }}\" [attr.points]=\"d\" />\n }\n }\n </svg>\n <!-- Pontos em HTML (o SVG estica na largura e deixaria o c\u00EDrculo oval). -->\n @for (s of lines(); track s.id) {\n @for (p of s.dots; track $index) {\n <span class=\"tr__dot tr__dot--{{ s.hue }}\" [style.left.%]=\"p.x\" [style.bottom.%]=\"p.y\" aria-hidden=\"true\"></span>\n }\n }\n <ol class=\"tr__cols\" [attr.aria-label]=\"text.trendCaption\">\n @for (label of columns(); track $index) {\n <li class=\"tr__col\" tabindex=\"0\" [attr.aria-label]=\"label\" [hmTooltip]=\"label\"></li>\n }\n </ol>\n </div>\n </div>\n <div class=\"tr__x\" aria-hidden=\"true\">\n @for (t of ticks(); track $index) {\n <span class=\"tr__tick\">{{ t }}</span>\n }\n </div>\n </figure>\n}\n", styles: [":host{display:block;min-inline-size:0}.tr{display:flex;flex-direction:column;gap:var(--hm-space-xs);margin:0}.tr__caption{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.tr__empty{margin:0;padding:var(--hm-space-md);border-radius:var(--hm-radius-control);background:var(--hm-surface-muted);color:var(--hm-muted);text-align:center}.tr__legend{display:flex;flex-wrap:wrap;gap:var(--hm-space-xs) var(--hm-space-md);margin:0;padding:0;color:var(--hm-text);font-size:var(--hm-font-size-xs);list-style:none}.tr__legend-item{display:inline-flex;align-items:center;gap:var(--hm-space-xs)}.tr__swatch{display:inline-block;inline-size:1rem;block-size:0;border-block-start:2px solid}.tr__swatch--1{border-color:var(--hm-data-categorical-1)}.tr__swatch--2{border-block-start-style:dashed;border-color:var(--hm-data-categorical-2)}.tr__plot,.tr__x{direction:ltr}.tr__plot{display:grid;grid-template-columns:1.5rem minmax(0,1fr);gap:var(--hm-space-xs)}.tr__y{display:flex;flex-direction:column;justify-content:space-between;block-size:11rem;margin-block:-.5em;color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-variant-numeric:tabular-nums;text-align:end}.tr__area{position:relative;block-size:11rem}.tr__svg{position:absolute;inset:0;inline-size:100%;block-size:100%;overflow:visible}.tr__grid{stroke:var(--hm-divider);stroke-width:1;stroke-dasharray:3 3;vector-effect:non-scaling-stroke}.tr__grid--base{stroke-dasharray:none}.tr__line{fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;vector-effect:non-scaling-stroke}.tr__line--1{stroke:var(--hm-data-categorical-1)}.tr__line--2{stroke:var(--hm-data-categorical-2);stroke-dasharray:6 4}.tr__dot{position:absolute;inline-size:8px;block-size:8px;border-radius:50%;box-shadow:0 0 0 2px var(--hm-surface-raised);transform:translate(-50%,50%);pointer-events:none}.tr__dot--1{background:var(--hm-data-categorical-1)}.tr__dot--2{background:var(--hm-data-categorical-2)}.tr__cols{position:absolute;inset:0;display:flex;margin:0;padding:0;list-style:none}.tr__col{flex:1 1 0;border-radius:var(--hm-radius-micro);outline:none}.tr__col:hover,.tr__col:focus-visible{background:color-mix(in srgb,var(--hm-text) 6%,transparent)}.tr__col:focus-visible{box-shadow:inset 0 0 0 2px var(--hm-focus-color)}.tr__x{display:grid;grid-auto-columns:minmax(0,1fr);grid-auto-flow:column;margin-inline-start:calc(1.5rem + var(--hm-space-xs));color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-variant-numeric:tabular-nums}.tr__tick{overflow:hidden;text-align:center;white-space:nowrap}\n"] }]
|
|
323
|
+
}], propDecorators: { series: [{ type: i0.Input, args: [{ isSignal: true, alias: "series", required: true }] }] } });
|
|
324
|
+
|
|
325
|
+
const PERIOD_DAYS = { all: 0, d7: 7, d30: 30, d90: 90 };
|
|
326
|
+
/** Página da lista completa (no drawer "Todas as avaliações"). */
|
|
327
|
+
const RATINGS_PAGE_SIZE = 10;
|
|
328
|
+
/** Quantas avaliações a coluna "Avaliações recentes" mostra. */
|
|
329
|
+
const RATINGS_RECENT_SIZE = 6;
|
|
330
|
+
/** Valor do filtro de área da tabela para as telas sem área (o mesmo do `spine-ratings-table`). */
|
|
331
|
+
const NO_AREA = '__none__';
|
|
332
|
+
let sequence = 0;
|
|
333
|
+
/**
|
|
334
|
+
* Tela "Avaliações" do auto-serviço: o que as pessoas acharam da ferramenta e de cada tela (nota de 0 a 10, 5 estrelas
|
|
335
|
+
* com meia, e comentário, dados por `SpineFeatures.ratings`). Feita para caber sem rolar muito:
|
|
336
|
+
*
|
|
337
|
+
* - **Barra da página** (`spinePageActions`): "Atualizado em…", o período e "Atualizar".
|
|
338
|
+
* - **KPIs** baixos lado a lado: a ferramenta e as telas juntas, com a média em destaque, as estrelas, os totais, o
|
|
339
|
+
* histograma das 11 notas (dica "7 avaliações" no hover) e "Ver detalhes".
|
|
340
|
+
* - **Duas colunas** (2/3 e 1/3; empilham no estreito): à esquerda, abas **Telas** (tabela com busca, área, faixa,
|
|
341
|
+
* ordem, sparkline e paginação), **Áreas** (as telas somadas por área do menu) e **Tendências** (média por semana);
|
|
342
|
+
* à direita, as **avaliações recentes** em lista curta e "Ver todas as avaliações", que abre o drawer com a lista
|
|
343
|
+
* completa, os filtros e a paginação no servidor. Clicar numa tela abre esse drawer com as avaliações dela.
|
|
344
|
+
*/
|
|
345
|
+
class AutoServiceRatingsComponent {
|
|
346
|
+
constructor() {
|
|
347
|
+
this.clientId = input.required(...(ngDevMode ? [{ debugName: "clientId" }] : /* istanbul ignore next */ []));
|
|
348
|
+
this.messages = inject(SPINE_MESSAGES);
|
|
349
|
+
this.text = this.messages.ratings;
|
|
350
|
+
this.api = inject(AutoServiceRatingsService);
|
|
351
|
+
this.toast = inject(HermesToastService);
|
|
352
|
+
this.locale = this.messages.meta.locale;
|
|
353
|
+
/** Prefixo dos ids dos títulos (cada card é uma região nomeada pelo próprio título). */
|
|
354
|
+
this.uid = `spine-ratings-${++sequence}`;
|
|
355
|
+
/** As notas possíveis, 0 a 10 (colunas da distribuição e o filtro de nota). */
|
|
356
|
+
this.scores = Array.from({ length: SPINE_RATING_SCORE_MAX + 1 }, (_, i) => i);
|
|
357
|
+
this.period = signal('all', ...(ngDevMode ? [{ debugName: "period" }] : /* istanbul ignore next */ []));
|
|
358
|
+
this.tab = signal('screens', ...(ngDevMode ? [{ debugName: "tab" }] : /* istanbul ignore next */ []));
|
|
359
|
+
/** Filtro de área da aba Telas (clicar numa área preenche). */
|
|
360
|
+
this.tableArea = signal('', ...(ngDevMode ? [{ debugName: "tableArea" }] : /* istanbul ignore next */ []));
|
|
361
|
+
this.summary = signal(null, ...(ngDevMode ? [{ debugName: "summary" }] : /* istanbul ignore next */ []));
|
|
362
|
+
this.summaryLoading = signal(false, ...(ngDevMode ? [{ debugName: "summaryLoading" }] : /* istanbul ignore next */ []));
|
|
363
|
+
this.updatedAt = signal(null, ...(ngDevMode ? [{ debugName: "updatedAt" }] : /* istanbul ignore next */ []));
|
|
364
|
+
this.error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
365
|
+
// ── Coluna "Avaliações recentes" (as últimas, sem filtro) ──
|
|
366
|
+
this.recent = signal([], ...(ngDevMode ? [{ debugName: "recent" }] : /* istanbul ignore next */ []));
|
|
367
|
+
this.recentTotal = signal(0, ...(ngDevMode ? [{ debugName: "recentTotal" }] : /* istanbul ignore next */ []));
|
|
368
|
+
this.recentLoading = signal(false, ...(ngDevMode ? [{ debugName: "recentLoading" }] : /* istanbul ignore next */ []));
|
|
369
|
+
// ── Drawer "Todas as avaliações": filtros e paginação no servidor ──
|
|
370
|
+
this.allOpen = signal(false, ...(ngDevMode ? [{ debugName: "allOpen" }] : /* istanbul ignore next */ []));
|
|
371
|
+
this.scope = signal('', ...(ngDevMode ? [{ debugName: "scope" }] : /* istanbul ignore next */ []));
|
|
372
|
+
this.screen = signal('', ...(ngDevMode ? [{ debugName: "screen" }] : /* istanbul ignore next */ []));
|
|
373
|
+
this.score = signal('', ...(ngDevMode ? [{ debugName: "score" }] : /* istanbul ignore next */ []));
|
|
374
|
+
this.withComment = signal(false, ...(ngDevMode ? [{ debugName: "withComment" }] : /* istanbul ignore next */ []));
|
|
375
|
+
this.pageState = signal({ page: 1, size: RATINGS_PAGE_SIZE }, ...(ngDevMode ? [{ debugName: "pageState" }] : /* istanbul ignore next */ []));
|
|
376
|
+
this.items = signal([], ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
|
|
377
|
+
this.total = signal(0, ...(ngDevMode ? [{ debugName: "total" }] : /* istanbul ignore next */ []));
|
|
378
|
+
this.listLoading = signal(false, ...(ngDevMode ? [{ debugName: "listLoading" }] : /* istanbul ignore next */ []));
|
|
379
|
+
this.tabsCard = viewChild('tabsCard', ...(ngDevMode ? [{ debugName: "tabsCard" }] : /* istanbul ignore next */ []));
|
|
380
|
+
this.periodOptions = computed(() => Object.keys(PERIOD_DAYS).map((value) => ({ value, label: this.text.periods[value] })), ...(ngDevMode ? [{ debugName: "periodOptions" }] : /* istanbul ignore next */ []));
|
|
381
|
+
this.scopeOptions = computed(() => [
|
|
382
|
+
{ value: '', label: this.text.allScopes },
|
|
383
|
+
{ value: 'app', label: this.text.tool },
|
|
384
|
+
{ value: 'screen', label: this.text.screens },
|
|
385
|
+
], ...(ngDevMode ? [{ debugName: "scopeOptions" }] : /* istanbul ignore next */ []));
|
|
386
|
+
this.screenOptions = computed(() => [
|
|
387
|
+
{ value: '', label: this.text.allScreens },
|
|
388
|
+
...(this.summary()?.by_screen ?? []).map((s) => ({ value: s.screen_path, label: this.screenName(s) })),
|
|
389
|
+
], ...(ngDevMode ? [{ debugName: "screenOptions" }] : /* istanbul ignore next */ []));
|
|
390
|
+
this.scoreOptions = computed(() => [
|
|
391
|
+
{ value: '', label: this.text.allScores },
|
|
392
|
+
...[...this.scores].reverse().map((n) => ({ value: String(n), label: this.scoreOf(n) })),
|
|
393
|
+
], ...(ngDevMode ? [{ debugName: "scoreOptions" }] : /* istanbul ignore next */ []));
|
|
394
|
+
/** Os dois KPIs de cima: a ferramenta e as telas juntas, cada um com as 11 colunas já calculadas. */
|
|
395
|
+
this.overview = computed(() => {
|
|
396
|
+
const s = this.summary();
|
|
397
|
+
if (!s)
|
|
398
|
+
return [];
|
|
399
|
+
const card = (id, title, hint, icon, tone, data) => ({ id, headingId: `${this.uid}-${id}`, title, hint, icon, tone, data, columns: this.histogram(data) });
|
|
400
|
+
return [
|
|
401
|
+
card('app', this.text.appCard, this.text.appCardHint, 'application', 'accent', s.app),
|
|
402
|
+
card('screens', this.text.screensCard, this.text.screensCardHint, 'applications', 'info', s.screens),
|
|
403
|
+
];
|
|
404
|
+
}, ...(ngDevMode ? [{ debugName: "overview" }] : /* istanbul ignore next */ []));
|
|
405
|
+
/** Sem avaliação nenhuma no período (nem da ferramenta, nem de tela). */
|
|
406
|
+
this.empty = computed(() => {
|
|
407
|
+
const s = this.summary();
|
|
408
|
+
return !!s && s.app.total === 0 && s.screens.total === 0;
|
|
409
|
+
}, ...(ngDevMode ? [{ debugName: "empty" }] : /* istanbul ignore next */ []));
|
|
410
|
+
this.hasFilters = computed(() => !!(this.scope() || this.screen() || this.score() || this.withComment()), ...(ngDevMode ? [{ debugName: "hasFilters" }] : /* istanbul ignore next */ []));
|
|
411
|
+
/** Linhas da aba Telas. */
|
|
412
|
+
this.screenRows = computed(() => (this.summary()?.by_screen ?? []).map((s) => ({
|
|
413
|
+
key: s.screen_path,
|
|
414
|
+
name: this.screenName(s),
|
|
415
|
+
sub: s.screen_label ? s.screen_path : null,
|
|
416
|
+
icon: s.screen_icon || RATINGS_SCREEN_FALLBACK_ICON,
|
|
417
|
+
area: areaOf(s),
|
|
418
|
+
areaIcon: s.screen_area_icon ?? null,
|
|
419
|
+
agg: s,
|
|
420
|
+
trend: s.trend,
|
|
421
|
+
})), ...(ngDevMode ? [{ debugName: "screenRows" }] : /* istanbul ignore next */ []));
|
|
422
|
+
/** Linhas da aba Áreas: as telas somadas por área (as sem área juntas em "Sem área"). */
|
|
423
|
+
this.areaRows = computed(() => {
|
|
424
|
+
const groups = new Map();
|
|
425
|
+
for (const r of this.screenRows())
|
|
426
|
+
groups.set(r.area ?? '', [...(groups.get(r.area ?? '') ?? []), r]);
|
|
427
|
+
return [...groups].map(([area, rows]) => ({
|
|
428
|
+
key: area || NO_AREA,
|
|
429
|
+
name: area || this.text.noArea,
|
|
430
|
+
sub: null,
|
|
431
|
+
icon: rows.find((r) => r.areaIcon)?.areaIcon || RATINGS_AREA_FALLBACK_ICON,
|
|
432
|
+
area: area || null,
|
|
433
|
+
areaIcon: null,
|
|
434
|
+
agg: combineAggregates(rows.map((r) => r.agg)),
|
|
435
|
+
trend: rows.some((r) => r.trend) ? combineTrends(rows.map((r) => r.trend)) : undefined,
|
|
436
|
+
screens: rows.length,
|
|
437
|
+
}));
|
|
438
|
+
}, ...(ngDevMode ? [{ debugName: "areaRows" }] : /* istanbul ignore next */ []));
|
|
439
|
+
/** As duas séries da aba Tendências (servidor novo; sem `trend`, a aba avisa que não há dados). */
|
|
440
|
+
this.trendSeries = computed(() => {
|
|
441
|
+
const t = this.summary()?.trend;
|
|
442
|
+
return t
|
|
443
|
+
? [
|
|
444
|
+
{ id: 'app', label: this.text.appCard, hue: 1, points: t.app },
|
|
445
|
+
{ id: 'screens', label: this.text.screensCard, hue: 2, points: t.screens },
|
|
446
|
+
]
|
|
447
|
+
: [];
|
|
448
|
+
}, ...(ngDevMode ? [{ debugName: "trendSeries" }] : /* istanbul ignore next */ []));
|
|
449
|
+
this.rows = computed(() => this.items().map((item) => ({ item, menu: this.itemMenu(item) })), ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
|
|
450
|
+
this.dateFormat = new Intl.DateTimeFormat(this.locale, { dateStyle: 'short', timeStyle: 'short' });
|
|
451
|
+
this.dayFormat = new Intl.DateTimeFormat(this.locale, { dateStyle: 'short' });
|
|
452
|
+
this.timeFormat = new Intl.DateTimeFormat(this.locale, { timeStyle: 'short' });
|
|
453
|
+
this.relativeFormat = new Intl.RelativeTimeFormat(this.locale, { numeric: 'always' });
|
|
454
|
+
this.numberFormat = new Intl.NumberFormat(this.locale, { maximumFractionDigits: 1 });
|
|
455
|
+
this.averageFormat = new Intl.NumberFormat(this.locale, { minimumFractionDigits: 1, maximumFractionDigits: 1 });
|
|
456
|
+
this.percentFormat = new Intl.NumberFormat(this.locale, { style: 'percent', maximumFractionDigits: 0 });
|
|
457
|
+
/** Só a resposta mais recente vale (filtro trocado com outra consulta em andamento). */
|
|
458
|
+
this.summarySeq = 0;
|
|
459
|
+
this.recentSeq = 0;
|
|
460
|
+
this.listSeq = 0;
|
|
461
|
+
// Client ou período mudou: relê o resumo e as recentes (e a lista completa, se estiver aberta).
|
|
462
|
+
effect(() => {
|
|
463
|
+
this.clientId();
|
|
464
|
+
this.period();
|
|
465
|
+
untracked(() => this.reload());
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
reload() {
|
|
469
|
+
// O início do período é fixado aqui, uma vez: resumo, listas e as páginas seguintes contam a partir do mesmo
|
|
470
|
+
// instante (calcular a cada chamada deixava os números do resumo e da lista de janelas diferentes).
|
|
471
|
+
const days = PERIOD_DAYS[this.period()];
|
|
472
|
+
this.periodFrom = days ? new Date(Date.now() - days * 86_400_000).toISOString() : undefined;
|
|
473
|
+
void this.loadSummary();
|
|
474
|
+
void this.loadRecent();
|
|
475
|
+
if (this.allOpen())
|
|
476
|
+
void this.loadList(1);
|
|
477
|
+
}
|
|
478
|
+
from() {
|
|
479
|
+
return this.periodFrom;
|
|
480
|
+
}
|
|
481
|
+
async loadSummary() {
|
|
482
|
+
const seq = ++this.summarySeq;
|
|
483
|
+
this.summaryLoading.set(true);
|
|
484
|
+
try {
|
|
485
|
+
const s = await this.api.summary(this.clientId(), this.from());
|
|
486
|
+
if (seq !== this.summarySeq)
|
|
487
|
+
return;
|
|
488
|
+
this.summary.set(s);
|
|
489
|
+
this.updatedAt.set(new Date());
|
|
490
|
+
this.error.set(null);
|
|
491
|
+
}
|
|
492
|
+
catch (err) {
|
|
493
|
+
if (seq !== this.summarySeq)
|
|
494
|
+
return;
|
|
495
|
+
this.summary.set(null);
|
|
496
|
+
this.error.set(this.errorText(err));
|
|
497
|
+
}
|
|
498
|
+
finally {
|
|
499
|
+
if (seq === this.summarySeq)
|
|
500
|
+
this.summaryLoading.set(false);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
async loadRecent() {
|
|
504
|
+
const seq = ++this.recentSeq;
|
|
505
|
+
this.recentLoading.set(true);
|
|
506
|
+
try {
|
|
507
|
+
const res = await this.api.list(this.clientId(), { from: this.from(), page: 1, limit: RATINGS_RECENT_SIZE });
|
|
508
|
+
if (seq !== this.recentSeq)
|
|
509
|
+
return;
|
|
510
|
+
this.recent.set(res.items);
|
|
511
|
+
this.recentTotal.set(res.total);
|
|
512
|
+
}
|
|
513
|
+
catch (err) {
|
|
514
|
+
if (seq !== this.recentSeq)
|
|
515
|
+
return;
|
|
516
|
+
this.recent.set([]);
|
|
517
|
+
this.recentTotal.set(0);
|
|
518
|
+
this.error.set(this.errorText(err));
|
|
519
|
+
}
|
|
520
|
+
finally {
|
|
521
|
+
if (seq === this.recentSeq)
|
|
522
|
+
this.recentLoading.set(false);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
async loadList(page = 1, size = this.pageState().size) {
|
|
526
|
+
const seq = ++this.listSeq;
|
|
527
|
+
this.listLoading.set(true);
|
|
528
|
+
try {
|
|
529
|
+
const scope = this.scope();
|
|
530
|
+
const res = await this.api.list(this.clientId(), {
|
|
531
|
+
from: this.from(),
|
|
532
|
+
scope: scope || undefined,
|
|
533
|
+
screen: scope === 'app' ? undefined : this.screen() || undefined,
|
|
534
|
+
score: this.score() ? Number(this.score()) : undefined,
|
|
535
|
+
comment: this.withComment(),
|
|
536
|
+
page,
|
|
537
|
+
limit: size,
|
|
538
|
+
});
|
|
539
|
+
if (seq !== this.listSeq)
|
|
540
|
+
return;
|
|
541
|
+
this.items.set(res.items);
|
|
542
|
+
this.total.set(res.total);
|
|
543
|
+
this.pageState.set({ page, size });
|
|
544
|
+
}
|
|
545
|
+
catch (err) {
|
|
546
|
+
if (seq !== this.listSeq)
|
|
547
|
+
return;
|
|
548
|
+
this.items.set([]);
|
|
549
|
+
this.total.set(0);
|
|
550
|
+
this.error.set(this.errorText(err));
|
|
551
|
+
}
|
|
552
|
+
finally {
|
|
553
|
+
if (seq === this.listSeq)
|
|
554
|
+
this.listLoading.set(false);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
setPeriod(value) { this.period.set(value); }
|
|
558
|
+
setTab(value) { this.tab.set(value); }
|
|
559
|
+
/** Abre "Todas as avaliações" com os filtros pedidos (vazio = sem filtro) e lê a página 1. */
|
|
560
|
+
openAll(filters = {}) {
|
|
561
|
+
this.scope.set(filters.scope ?? '');
|
|
562
|
+
this.screen.set(filters.screen ?? '');
|
|
563
|
+
this.score.set('');
|
|
564
|
+
this.withComment.set(false);
|
|
565
|
+
this.allOpen.set(true);
|
|
566
|
+
void this.loadList(1);
|
|
567
|
+
}
|
|
568
|
+
/** "Ver detalhes" do KPI: a ferramenta abre as avaliações dela; as telas levam à aba Telas. */
|
|
569
|
+
details(card) {
|
|
570
|
+
if (card.id === 'app') {
|
|
571
|
+
this.openAll({ scope: 'app' });
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
this.tab.set('screens');
|
|
575
|
+
const el = this.tabsCard()?.nativeElement;
|
|
576
|
+
el?.scrollIntoView?.({ block: 'start', behavior: 'smooth' });
|
|
577
|
+
el?.querySelector('[role="tab"][aria-selected="true"]')?.focus();
|
|
578
|
+
}
|
|
579
|
+
/** Clique numa tela da tabela: o drawer com as avaliações dela. */
|
|
580
|
+
pickScreen(row) { this.openAll({ scope: 'screen', screen: row.key }); }
|
|
581
|
+
/** Clique numa área: a aba Telas filtrada por ela. */
|
|
582
|
+
pickArea(row) {
|
|
583
|
+
this.tableArea.set(row.area ?? NO_AREA);
|
|
584
|
+
this.tab.set('screens');
|
|
585
|
+
}
|
|
586
|
+
setScope(value) {
|
|
587
|
+
this.scope.set(value);
|
|
588
|
+
if (value === 'app')
|
|
589
|
+
this.screen.set('');
|
|
590
|
+
void this.loadList(1);
|
|
591
|
+
}
|
|
592
|
+
setScreen(value) {
|
|
593
|
+
this.screen.set(value);
|
|
594
|
+
if (value)
|
|
595
|
+
this.scope.set('screen');
|
|
596
|
+
void this.loadList(1);
|
|
597
|
+
}
|
|
598
|
+
setScore(value) { this.score.set(value); void this.loadList(1); }
|
|
599
|
+
setWithComment(value) { this.withComment.set(value); void this.loadList(1); }
|
|
600
|
+
clearFilters() {
|
|
601
|
+
this.scope.set('');
|
|
602
|
+
this.screen.set('');
|
|
603
|
+
this.score.set('');
|
|
604
|
+
this.withComment.set(false);
|
|
605
|
+
void this.loadList(1);
|
|
606
|
+
}
|
|
607
|
+
onPage(state) { void this.loadList(state.page, state.size); }
|
|
608
|
+
/** Menu ⋮ de uma avaliação: só o que existe de fato (copiar o comentário, ver as avaliações do mesmo alvo). */
|
|
609
|
+
itemMenu(i) {
|
|
610
|
+
const menu = [];
|
|
611
|
+
if (i.comment?.trim())
|
|
612
|
+
menu.push({ id: 'copy', label: this.text.copyComment, icon: 'copy' });
|
|
613
|
+
if (i.scope === 'app' && this.scope() !== 'app') {
|
|
614
|
+
menu.push({ id: 'filter', label: this.text.showAppRatings, icon: 'filter' });
|
|
615
|
+
}
|
|
616
|
+
else if (i.scope === 'screen' && i.screen_path && this.screen() !== i.screen_path) {
|
|
617
|
+
menu.push({ id: 'filter', label: this.text.showScreenRatings, icon: 'filter' });
|
|
618
|
+
}
|
|
619
|
+
return menu;
|
|
620
|
+
}
|
|
621
|
+
async onItemAction(i, action) {
|
|
622
|
+
if (action === 'filter') {
|
|
623
|
+
if (i.scope === 'app')
|
|
624
|
+
this.setScope('app');
|
|
625
|
+
else if (i.screen_path)
|
|
626
|
+
this.setScreen(i.screen_path);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (action === 'copy' && i.comment) {
|
|
630
|
+
try {
|
|
631
|
+
await navigator.clipboard.writeText(i.comment);
|
|
632
|
+
this.toast.success(this.text.commentCopied);
|
|
633
|
+
}
|
|
634
|
+
catch {
|
|
635
|
+
this.toast.error(this.text.copyFailed);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
// ── formatação ──
|
|
640
|
+
screenName(s) {
|
|
641
|
+
return s.screen_label || s.screen_path || '';
|
|
642
|
+
}
|
|
643
|
+
/** "Atualizado em 23/10/2026 às 16:49". */
|
|
644
|
+
updatedText(d) {
|
|
645
|
+
return formatMessage(this.text.updatedAt, { date: this.dayFormat.format(d), time: this.timeFormat.format(d) });
|
|
646
|
+
}
|
|
647
|
+
/** Uma nota como "8/10". */
|
|
648
|
+
scoreShort(score) {
|
|
649
|
+
return formatMessage(this.text.scoreShort, { value: this.numberFormat.format(score) });
|
|
650
|
+
}
|
|
651
|
+
/** Só o número da média em destaque, sempre com uma casa ("4,7"; "10,0"); "—" sem avaliação. */
|
|
652
|
+
averageFigure(a) {
|
|
653
|
+
return a.average == null ? '—' : this.averageFormat.format(a.average);
|
|
654
|
+
}
|
|
655
|
+
/** Nota ou média para o leitor: "8,4 de 10". */
|
|
656
|
+
scoreOf(value) {
|
|
657
|
+
return formatMessage(this.text.scoreOf, { value: this.numberFormat.format(value) });
|
|
658
|
+
}
|
|
659
|
+
plural(message, count) {
|
|
660
|
+
return pluralMessage(message, count, this.locale);
|
|
661
|
+
}
|
|
662
|
+
/** "3 avaliações • 3 comentários". */
|
|
663
|
+
totals(a) {
|
|
664
|
+
return `${this.plural(this.text.total, a.total)} • ${this.plural(this.text.comments, a.comments)}`;
|
|
665
|
+
}
|
|
666
|
+
/** "Ver todas as avaliações (117)": o total do período, quando já se sabe. */
|
|
667
|
+
viewAllLabel() {
|
|
668
|
+
const n = this.recentTotal();
|
|
669
|
+
return n ? `${this.text.viewAll} (${n})` : this.text.viewAll;
|
|
670
|
+
}
|
|
671
|
+
/** O nome do "Ver detalhes" para o leitor, distinto em cada KPI. */
|
|
672
|
+
detailsLabel(card) {
|
|
673
|
+
return formatMessage(this.text.detailsOf, { name: card.title });
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* As 11 colunas de um agregado. A altura é relativa à nota mais frequente (a maior coluna enche o trilho), para as
|
|
677
|
+
* colunas não ficarem rasas quando as notas se espalham; coluna com contagem tem um mínimo visível.
|
|
678
|
+
*/
|
|
679
|
+
histogram(a) {
|
|
680
|
+
const top = Math.max(0, ...a.distribution);
|
|
681
|
+
return this.scores.map((score) => {
|
|
682
|
+
const count = a.distribution[score] ?? 0;
|
|
683
|
+
const tip = this.plural(this.text.total, count);
|
|
684
|
+
return {
|
|
685
|
+
score,
|
|
686
|
+
count,
|
|
687
|
+
height: top && count ? Math.max(8, Math.round((count / top) * 100)) : 0,
|
|
688
|
+
tone: count ? ratingTone(score) : 'neutral',
|
|
689
|
+
tip,
|
|
690
|
+
label: formatMessage(this.text.distributionRow, {
|
|
691
|
+
score, count: tip, share: this.percentFormat.format(a.total ? count / a.total : 0),
|
|
692
|
+
}),
|
|
693
|
+
};
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
date(iso) {
|
|
697
|
+
const d = new Date(iso);
|
|
698
|
+
return Number.isNaN(d.getTime()) ? iso : this.dateFormat.format(d);
|
|
699
|
+
}
|
|
700
|
+
/** Quando, na lista curta: "há 5 minutos", "há 1 hora", "Ontem às 14:21" ou "20/10/2026 às 14:21". */
|
|
701
|
+
when(iso, now = new Date()) {
|
|
702
|
+
const d = new Date(iso);
|
|
703
|
+
if (Number.isNaN(d.getTime()))
|
|
704
|
+
return iso;
|
|
705
|
+
const w = relativeWhen(d, now);
|
|
706
|
+
switch (w.kind) {
|
|
707
|
+
case 'now': return this.text.justNow;
|
|
708
|
+
case 'minutes': return this.relativeFormat.format(-w.value, 'minute');
|
|
709
|
+
case 'hours': return this.relativeFormat.format(-w.value, 'hour');
|
|
710
|
+
case 'yesterday': return formatMessage(this.text.yesterdayAt, { time: this.timeFormat.format(d) });
|
|
711
|
+
default: return formatMessage(this.text.dateAt, { date: this.dayFormat.format(d), time: this.timeFormat.format(d) });
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
author(i) {
|
|
715
|
+
return i.user_name || i.user_username || this.text.unknownUser;
|
|
716
|
+
}
|
|
717
|
+
/** O alvo como chip: "A ferramenta" ou o nome da tela. */
|
|
718
|
+
target(i) {
|
|
719
|
+
return i.scope === 'app' ? this.text.tool : this.screenName(i);
|
|
720
|
+
}
|
|
721
|
+
errorText(err) {
|
|
722
|
+
return err instanceof HttpErrorResponse && err.status === 403 ? this.text.forbidden : this.text.loadError;
|
|
723
|
+
}
|
|
724
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceRatingsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
725
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: AutoServiceRatingsComponent, isStandalone: true, selector: "spine-auto-service-ratings", inputs: { clientId: { classPropertyName: "clientId", publicName: "clientId", isSignal: true, isRequired: true, transformFunction: null } }, providers: [AutoServiceRatingsService], viewQueries: [{ propertyName: "tabsCard", first: true, predicate: ["tabsCard"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Na barra da p\u00E1gina do painel (fora de um painel, aqui mesmo): quando foi lido, o per\u00EDodo e Atualizar. -->\n<ng-template spinePageActions>\n <div class=\"ratings__page-actions\">\n @if (updatedAt(); as at) {\n <span class=\"ratings__updated\">{{ updatedText(at) }}</span>\n }\n <hm-segmented-control\n [label]=\"text.period\"\n [options]=\"periodOptions()\"\n [value]=\"period()\"\n (valueChange)=\"setPeriod($event)\"\n size=\"small\"\n emphasis=\"action\"\n />\n <hm-button emphasis=\"medium\" size=\"small\" icon=\"refresh\" [loading]=\"summaryLoading() || recentLoading()\" (clicked)=\"reload()\">\n {{ text.refresh }}\n </hm-button>\n </div>\n</ng-template>\n\n<div class=\"ratings\" [attr.aria-busy]=\"summaryLoading() || recentLoading()\">\n @if (error(); as message) {\n <hm-alert variant=\"error\" [closable]=\"true\" (closed)=\"error.set(null)\">{{ message }}</hm-alert>\n }\n\n @if (!summary() && summaryLoading()) {\n <hm-skeleton shape=\"cards\" [count]=\"2\" [label]=\"text.refresh\" />\n } @else if (empty()) {\n <hm-state-screen situation=\"empty\" [heading]=\"text.noRatings\" />\n } @else if (summary()) {\n <!-- KPIs baixos: m\u00E9dia em destaque, estrelas e totais \u00E0 esquerda; o histograma das 11 notas \u00E0 direita. -->\n <div class=\"ratings__kpis\">\n @for (card of overview(); track card.id) {\n <hm-card class=\"ratings__kpi\">\n <section class=\"ratings__kpi-body\" [attr.aria-labelledby]=\"card.headingId\">\n <header class=\"ratings__head\">\n <span class=\"ratings__badge ratings__badge--{{ card.tone }}\" aria-hidden=\"true\"><hm-icon [shape]=\"card.icon\" /></span>\n <span class=\"ratings__titles\">\n <h3 class=\"ratings__title\" [id]=\"card.headingId\">{{ card.title }}</h3>\n <span class=\"ratings__subtitle\">{{ card.hint }}</span>\n </span>\n <hm-button class=\"ratings__details\" emphasis=\"minimal\" size=\"small\" iconEnd=\"arrow-right\" (clicked)=\"details(card)\">\n {{ text.details }}<span class=\"ratings__sr\"> {{ detailsLabel(card) }}</span>\n </hm-button>\n </header>\n\n <div class=\"ratings__kpi-main\">\n <div class=\"ratings__figure\">\n <span class=\"ratings__average\" aria-hidden=\"true\">\n <span class=\"ratings__average-value\">{{ averageFigure(card.data) }}</span><span class=\"ratings__average-max\">{{ text.scoreMax }}</span>\n </span>\n @if (card.data.total) {\n <spine-rating-stars class=\"ratings__stars\" [readonly]=\"true\" [showValue]=\"false\" [value]=\"card.data.average\" />\n }\n <span class=\"ratings__totals\">{{ totals(card.data) }}</span>\n </div>\n\n <ul class=\"ratings__hist\" [attr.aria-label]=\"text.distribution\">\n @for (col of card.columns; track col.score) {\n <li class=\"ratings__hist-col\" [attr.aria-label]=\"col.label\" [hmTooltip]=\"col.tip\">\n <span class=\"ratings__hist-track\" aria-hidden=\"true\">\n <span class=\"ratings__hist-bar ratings__fill--{{ col.tone }}\" [style.height.%]=\"col.height\"></span>\n </span>\n <span class=\"ratings__hist-score\" aria-hidden=\"true\">{{ col.score }}</span>\n </li>\n }\n </ul>\n </div>\n </section>\n </hm-card>\n }\n </div>\n\n <!-- Duas colunas: as telas (abas) e as recentes, lado a lado. -->\n <div class=\"ratings__columns\">\n <div class=\"ratings__main\" #tabsCard>\n <hm-card class=\"ratings__tabs-card\">\n <hm-tabs [label]=\"text.ranking\" [active]=\"tab()\" (activeChange)=\"setTab($event)\">\n <hm-tab id=\"screens\" [label]=\"text.tabScreens\" [count]=\"screenRows().length\">\n <spine-ratings-table *hmTabContent mode=\"screens\" [rows]=\"screenRows()\" [(area)]=\"tableArea\" [selected]=\"allOpen() ? screen() : null\" (picked)=\"pickScreen($event)\" />\n </hm-tab>\n <hm-tab id=\"areas\" [label]=\"text.tabAreas\" [count]=\"areaRows().length\">\n <spine-ratings-table *hmTabContent mode=\"areas\" [rows]=\"areaRows()\" (picked)=\"pickArea($event)\" />\n </hm-tab>\n <hm-tab id=\"trends\" [label]=\"text.tabTrends\">\n <spine-ratings-trend *hmTabContent [series]=\"trendSeries()\" />\n </hm-tab>\n </hm-tabs>\n </hm-card>\n </div>\n\n <hm-card class=\"ratings__recent-card\">\n <section class=\"ratings__recent\" [attr.aria-labelledby]=\"uid + '-recent'\">\n <header class=\"ratings__head\">\n <span class=\"ratings__badge ratings__badge--warning\" aria-hidden=\"true\"><hm-icon shape=\"comment-message\" /></span>\n <span class=\"ratings__titles\">\n <h3 class=\"ratings__title\" [id]=\"uid + '-recent'\">{{ text.recent }}</h3>\n <span class=\"ratings__subtitle\">{{ text.recentHint }}</span>\n </span>\n </header>\n\n @if (!recent().length && recentLoading()) {\n <hm-skeleton shape=\"list\" [count]=\"3\" [label]=\"text.refresh\" />\n } @else if (!recent().length) {\n <p class=\"ratings__empty\">{{ text.emptyList }}</p>\n } @else {\n <ul class=\"ratings__feed\">\n @for (item of recent(); track item.id) {\n <li class=\"ratings__feed-item\">\n <hm-account-avatar class=\"ratings__feed-avatar\" size=\"small\" [name]=\"author(item)\" [imageUrl]=\"item.user_picture\" />\n <div class=\"ratings__feed-body\">\n <div class=\"ratings__feed-head\">\n <span class=\"ratings__author\">{{ author(item) }}</span>\n <span class=\"ratings__when\" [attr.title]=\"date(item.updated_at)\">{{ when(item.updated_at) }}</span>\n </div>\n <div class=\"ratings__feed-meta\">\n <spine-rating-stars class=\"ratings__stars\" size=\"small\" [readonly]=\"true\" [showValue]=\"false\" [value]=\"item.score\" />\n <span class=\"ratings__item-value\" aria-hidden=\"true\">{{ scoreShort(item.score) }}</span>\n <hm-tag class=\"ratings__chip\" [color]=\"item.scope === 'app' ? 'primary' : 'info'\" [name]=\"target(item)\" />\n </div>\n @if (item.comment) {\n <p class=\"ratings__feed-comment\">{{ item.comment }}</p>\n }\n </div>\n </li>\n }\n </ul>\n }\n <hm-button class=\"ratings__all\" emphasis=\"minimal\" size=\"small\" iconEnd=\"arrow-right\" (clicked)=\"openAll()\">\n {{ viewAllLabel() }}\n </hm-button>\n </section>\n </hm-card>\n </div>\n }\n</div>\n\n<!-- Todas as avalia\u00E7\u00F5es: a lista completa, com filtros e pagina\u00E7\u00E3o no servidor. -->\n<hm-drawer [heading]=\"text.allHeading\" size=\"large\" [(open)]=\"allOpen\">\n @if (allOpen()) {\n <div class=\"ratings__all-body\" [attr.aria-busy]=\"listLoading()\">\n <div class=\"ratings__filters\">\n <hm-combobox class=\"ratings__filter\" size=\"small\" [label]=\"text.filterScope\" [options]=\"scopeOptions()\" [value]=\"scope()\" (valueChange)=\"setScope($event)\" />\n <hm-combobox class=\"ratings__filter ratings__filter--wide\" size=\"small\" [label]=\"text.filterScreen\" [options]=\"screenOptions()\" [value]=\"screen()\" (valueChange)=\"setScreen($event)\" />\n <hm-combobox class=\"ratings__filter\" size=\"small\" [label]=\"text.filterScore\" [options]=\"scoreOptions()\" [value]=\"score()\" (valueChange)=\"setScore($event)\" />\n <hm-switch class=\"ratings__switch\" [label]=\"text.withComment\" [value]=\"withComment()\" (valueChange)=\"setWithComment($event)\" />\n @if (hasFilters()) {\n <hm-button emphasis=\"minimal\" size=\"small\" (clicked)=\"clearFilters()\">{{ text.clearFilters }}</hm-button>\n }\n </div>\n\n @if (!items().length && listLoading()) {\n <hm-skeleton shape=\"list\" [count]=\"3\" [label]=\"text.refresh\" />\n } @else if (!items().length) {\n <p class=\"ratings__empty\">{{ text.emptyList }}</p>\n } @else {\n <ul class=\"ratings__list\" [class.ratings__list--busy]=\"listLoading()\">\n @for (row of rows(); track row.item.id) {\n <li class=\"ratings__item\">\n <hm-account-avatar class=\"ratings__item-avatar\" size=\"small\" [name]=\"author(row.item)\" [imageUrl]=\"row.item.user_picture\" />\n <span class=\"ratings__author ratings__item-who\">{{ author(row.item) }}</span>\n <span class=\"ratings__item-score\">\n <spine-rating-stars class=\"ratings__stars\" size=\"small\" [readonly]=\"true\" [showValue]=\"false\" [value]=\"row.item.score\" />\n <span class=\"ratings__item-value\" aria-hidden=\"true\">{{ scoreShort(row.item.score) }}</span>\n </span>\n <span class=\"ratings__target\">\n <hm-tag class=\"ratings__chip\" [color]=\"row.item.scope === 'app' ? 'primary' : 'info'\" [name]=\"target(row.item)\" />\n @if (row.item.scope === 'screen' && row.item.screen_label && row.item.screen_path) {\n <span class=\"ratings__route\">{{ row.item.screen_path }}</span>\n }\n </span>\n @if (row.item.comment) {\n <p class=\"ratings__comment\">{{ row.item.comment }}</p>\n } @else {\n <p class=\"ratings__comment ratings__comment--none\">{{ text.noComment }}</p>\n }\n <span class=\"ratings__date\">{{ date(row.item.updated_at) }}</span>\n <span class=\"ratings__item-menu\">\n @if (row.menu.length) {\n <hm-menu-button\n size=\"small\"\n icon=\"ellipsis-vertical\"\n align=\"end\"\n [showArrow]=\"false\"\n [label]=\"text.itemActions\"\n [items]=\"row.menu\"\n (chosen)=\"onItemAction(row.item, $event)\"\n />\n }\n </span>\n </li>\n }\n </ul>\n @if (total() > pageState().size) {\n <hm-paginator\n class=\"ratings__pager\"\n variant=\"simple\"\n [label]=\"text.pagination\"\n [state]=\"pageState()\"\n [totalItems]=\"total()\"\n [sizes]=\"[10, 25, 50]\"\n (stateChanged)=\"onPage($event)\"\n />\n }\n }\n </div>\n }\n</hm-drawer>\n", styles: [".as-stack{display:flex;flex-direction:column;gap:var(--hm-space-md);min-inline-size:0}.as-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.as-field-row{display:flex;flex-wrap:wrap;align-items:flex-end;gap:var(--hm-space-sm)}.as-field-row>hm-combobox{flex:1 1 16rem;min-inline-size:0}.as-summary{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.as-summary__info{display:flex;flex:1 1 16rem;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.as-summary__title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs);margin:0;color:var(--hm-text);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold)}.as-summary__detail{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.muted{color:var(--hm-muted)}.as-empty{margin:var(--hm-space-xs) 0 0;font-size:var(--hm-font-size-sm)}.as-secret{display:grid;gap:var(--hm-space-micro)}.as-secret__label{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-secret__row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.as-secret__value{flex:1 1 240px;min-width:0;border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:var(--hm-radius-control);padding:var(--hm-space-xs) var(--hm-space-sm);background:var(--hm-surface-muted);color:var(--hm-text);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere;-webkit-user-select:all;user-select:all}:host{display:block;min-inline-size:0;container-type:inline-size}.ratings{display:flex;flex-direction:column;gap:var(--hm-space-md);min-inline-size:0}.ratings__sr{position:absolute;inline-size:1px;block-size:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.ratings__page-actions{display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-end;gap:var(--hm-space-xs) var(--hm-space-sm);min-inline-size:0}.ratings__updated{color:var(--hm-muted);font-size:var(--hm-font-size-xs);white-space:nowrap}.ratings__head{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm);min-inline-size:0}.ratings__titles{display:flex;flex:1 1 8rem;flex-direction:column;gap:var(--hm-space-nano);min-inline-size:0}.ratings__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold);line-height:var(--hm-line-height-tight)}.ratings__subtitle{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.ratings__details{flex:none;margin-inline-start:auto}.ratings__badge{display:inline-flex;flex:none;align-items:center;justify-content:center;inline-size:2.25rem;block-size:2.25rem;border-radius:var(--hm-radius);font-size:var(--hm-icon-md)}.ratings__badge--accent{background:var(--hm-accent-subtle);color:var(--hm-accent-strong)}.ratings__badge--info{background:var(--hm-info-bg);color:var(--hm-info)}.ratings__badge--warning{background:var(--hm-warning-bg);color:var(--hm-warning)}.ratings__kpis{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--hm-space-md)}@container (max-width: 56rem){.ratings__kpis{grid-template-columns:minmax(0,1fr)}}.ratings__kpi-body{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-inline-size:0}.ratings__kpi-main{display:flex;align-items:flex-end;gap:var(--hm-space-lg);min-inline-size:0}.ratings__figure{display:flex;flex:none;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.ratings__average{display:inline-flex;align-items:baseline;gap:var(--hm-space-nano);color:var(--hm-text);font-variant-numeric:tabular-nums;line-height:1}.ratings__average-value{font-size:calc(var(--hm-font-size-xxl) * 1.4);font-weight:var(--hm-font-weight-bold);letter-spacing:-.02em}.ratings__average-max{color:var(--hm-muted);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-medium)}.ratings__totals{color:var(--hm-muted);font-size:var(--hm-font-size-xs);white-space:nowrap}.ratings__stars{flex:none}.ratings__hist{display:grid;flex:1 1 auto;grid-template-columns:repeat(11,minmax(0,1fr));gap:var(--hm-space-micro);min-inline-size:0;margin:0;padding:0;list-style:none}.ratings__hist-col{display:flex;flex-direction:column;align-items:center;gap:var(--hm-space-nano);min-inline-size:0;font-size:var(--hm-font-size-xs);font-variant-numeric:tabular-nums;cursor:default}.ratings__hist-col:hover .ratings__hist-track{box-shadow:0 0 0 2px var(--hm-divider)}.ratings__hist-track{display:flex;align-items:flex-end;inline-size:100%;max-inline-size:1.75rem;block-size:3.75rem;border-radius:var(--hm-radius-micro);background:var(--hm-surface-muted);overflow:hidden}.ratings__hist-bar{display:block;inline-size:100%;border-radius:var(--hm-radius-micro) var(--hm-radius-micro) 0 0;transition:height .16s ease}.ratings__hist-score{color:var(--hm-muted)}@container (max-width: 30rem){.ratings__kpi-main{flex-direction:column;align-items:stretch;gap:var(--hm-space-sm)}.ratings__figure{flex-flow:row wrap;align-items:center;gap:var(--hm-space-xs) var(--hm-space-sm)}}.ratings__fill--success{background:var(--hm-success)}.ratings__fill--warning{background:var(--hm-warning)}.ratings__fill--danger{background:var(--hm-danger)}.ratings__fill--neutral{background:transparent}.ratings__columns{display:grid;grid-template-columns:minmax(0,2fr) minmax(18rem,1fr);align-items:start;gap:var(--hm-space-md)}@container (max-width: 64rem){.ratings__columns{grid-template-columns:minmax(0,1fr)}}.ratings__main{min-inline-size:0;scroll-margin-block-start:var(--hm-space-md)}.ratings__recent{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-inline-size:0}.ratings__empty{margin:0;padding:var(--hm-space-md);border-radius:var(--hm-radius-control);background:var(--hm-surface-muted);color:var(--hm-muted);text-align:center}.ratings__feed{display:flex;flex-direction:column;gap:var(--hm-space-micro);margin:0;padding:0;list-style:none}.ratings__feed-item{display:flex;align-items:flex-start;gap:var(--hm-space-sm);padding:var(--hm-space-xs) var(--hm-space-sm);border-radius:var(--hm-radius-control);background:var(--hm-surface-muted);font-size:var(--hm-font-size-sm)}.ratings__feed-avatar{flex:none}.ratings__feed-body{display:flex;flex:1 1 auto;flex-direction:column;gap:var(--hm-space-nano);min-inline-size:0}.ratings__feed-head{display:flex;align-items:baseline;gap:var(--hm-space-xs);min-inline-size:0}.ratings__author{overflow:hidden;color:var(--hm-text);font-weight:var(--hm-font-weight-semibold);text-overflow:ellipsis;white-space:nowrap}.ratings__when{flex:none;margin-inline-start:auto;color:var(--hm-muted);font-size:var(--hm-font-size-xs);white-space:nowrap}.ratings__feed-meta{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs);min-inline-size:0}.ratings__feed-meta>hm-tag{max-inline-size:100%}.ratings__item-value{color:var(--hm-text);font-variant-numeric:tabular-nums;font-weight:var(--hm-font-weight-semibold)}.ratings__feed-comment{display:-webkit-box;margin:0;overflow:hidden;color:var(--hm-text);overflow-wrap:anywhere;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2}.ratings__all{align-self:flex-end}.ratings__all-body{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-inline-size:0;container-type:inline-size}.ratings__filters{display:flex;flex-wrap:wrap;align-items:flex-end;gap:var(--hm-space-xs) var(--hm-space-sm)}.ratings__filter{flex:1 1 9rem;min-inline-size:0}.ratings__filter--wide{flex-basis:13rem}.ratings__switch{flex:none;padding-block-end:var(--hm-space-xs)}.ratings__list{display:flex;flex-direction:column;gap:var(--hm-space-micro);margin:0;padding:0;list-style:none;transition:opacity .12s ease}.ratings__list--busy{opacity:.55}.ratings__item{display:grid;grid-template-areas:\"avatar who score date menu\" \"avatar target target target target\" \"avatar comment comment comment comment\";grid-template-columns:auto minmax(0,1fr) auto auto 2rem;align-items:center;gap:var(--hm-space-nano) var(--hm-space-sm);padding:var(--hm-space-xs) var(--hm-space-sm);border-radius:var(--hm-radius-control);background:var(--hm-surface-muted);font-size:var(--hm-font-size-sm)}.ratings__item-avatar{grid-area:avatar;align-self:start}.ratings__item-who{grid-area:who}.ratings__item-score{display:inline-flex;grid-area:score;align-items:center;gap:var(--hm-space-xs);white-space:nowrap}.ratings__target{display:flex;flex-wrap:wrap;grid-area:target;align-items:center;gap:var(--hm-space-xs);min-inline-size:0}.ratings__target>hm-tag{max-inline-size:100%}.ratings__route{overflow:hidden;color:var(--hm-muted);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-xs);text-overflow:ellipsis;white-space:nowrap}.ratings__comment{grid-area:comment;margin:0;color:var(--hm-text);white-space:pre-line;overflow-wrap:anywhere}.ratings__comment--none{color:var(--hm-muted);font-style:italic}.ratings__date{grid-area:date;color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-variant-numeric:tabular-nums;white-space:nowrap}.ratings__item-menu{display:inline-flex;grid-area:menu;justify-content:flex-end}@container (max-width: 28rem){.ratings__item{grid-template-areas:\"avatar who menu\" \"avatar score date\" \"avatar target target\" \"avatar comment comment\";grid-template-columns:auto minmax(0,1fr) auto}}.ratings__pager{margin-block-start:var(--hm-space-xs)}@media(prefers-reduced-motion:reduce){.ratings__hist-bar,.ratings__list{transition:none}}:host-context([data-motion=reduce]) .ratings__hist-bar,:host-context([data-motion=reduce]) .ratings__list{transition:none}\n"], dependencies: [{ kind: "component", type: AccountAvatarComponent, selector: "hm-account-avatar", inputs: ["name", "imageUrl", "size", "loading"] }, { kind: "component", type: AlertComponent, selector: "hm-alert", inputs: ["variant", "appearance", "heading", "closable", "size", "showIcon", "link", "linkLabel", "linkTarget", "dismissKey", "duration"], outputs: ["closed"] }, { kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "loadingState", "block", "link", "newTab", "caret", "inMenu", "toggle", "pressed"], outputs: ["loadingStateChange", "pressedChange", "clicked"] }, { kind: "component", type: CardComponent, selector: "hm-card", inputs: ["heading", "icon", "selected", "fill", "media", "mediaAlt", "mediaDecorative", "mediaPosition"] }, { kind: "component", type: ComboboxComponent, selector: "hm-combobox", inputs: ["label", "options", "value", "placeholder", "icon", "hint", "size", "labelHidden", "clearable", "loading", "allowCustomValue"], outputs: ["valueChange"] }, { kind: "component", type: DrawerComponent, selector: "hm-drawer", inputs: ["open", "heading", "size", "position", "pinned", "closeLabel", "closable", "beforeClose"], outputs: ["openChange", "closed"] }, { kind: "component", type: IconComponent, selector: "hm-icon", inputs: ["shape", "direction", "size", "solid", "badge", "status", "flip", "inverse", "label"] }, { kind: "component", type: MenuButtonComponent, selector: "hm-menu-button", inputs: ["label", "icon", "items", "showArrow", "align", "side", "size", "shape", "layer", "closeOnCheck", "disabled"], outputs: ["chosen", "checkedChange"] }, { kind: "component", type: PaginatorComponent, selector: "hm-paginator", inputs: ["state", "totalItems", "variant", "label", "showPage", "hasNext", "hasPrevious", "sizes", "maxNumbers"], outputs: ["stateChanged", "nextPage", "previousPage"] }, { kind: "component", type: SegmentedControlComponent, selector: "hm-segmented-control", inputs: ["options", "value", "label", "size", "emphasis"], outputs: ["valueChange"] }, { kind: "component", type: SkeletonComponent, selector: "hm-skeleton", inputs: ["shape", "count", "label"] }, { kind: "component", type: StateScreenComponent, selector: "hm-state-screen", inputs: ["heading", "description", "situation", "action", "actionIcon"], outputs: ["actionTriggered"] }, { kind: "component", type: SwitchComponent, selector: "hm-switch", inputs: ["value", "label", "hint", "labelPosition", "loading"], outputs: ["valueChange"] }, { kind: "component", type: TabComponent, selector: "hm-tab", inputs: ["id", "label", "disabled", "count", "route", "exact", "destroyOnHide"] }, { kind: "directive", type: TabContentDirective, selector: "ng-template[hmTabContent]" }, { kind: "component", type: TabsComponent, selector: "hm-tabs", inputs: ["label", "active", "orientation", "mode", "overflow", "fillHeight", "noPanelGap"], outputs: ["activeChange"] }, { kind: "component", type: TagComponent, selector: "hm-tag", inputs: ["name", "color", "compact"] }, { kind: "directive", type: TooltipDirective, selector: "[hmTooltip]", inputs: ["hmTooltip", "hmTooltipSide", "hmTooltipDisabled", "hmTooltipShowDelay", "hmTooltipHideDelay"] }, { kind: "directive", type: SpinePageActionsDirective, selector: "ng-template[spinePageActions]" }, { kind: "component", type: SpineRatingStarsComponent, selector: "spine-rating-stars", inputs: ["value", "readonly", "showValue", "showName", "size", "label", "labelledBy", "describedBy", "invalid"], outputs: ["valueChange"] }, { kind: "component", type: RatingsTableComponent, selector: "spine-ratings-table", inputs: ["mode", "rows", "area", "selected"], outputs: ["areaChange", "picked"] }, { kind: "component", type: RatingsTrendComponent, selector: "spine-ratings-trend", inputs: ["series"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
726
|
+
}
|
|
727
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceRatingsComponent, decorators: [{
|
|
728
|
+
type: Component,
|
|
729
|
+
args: [{ selector: 'spine-auto-service-ratings', imports: [
|
|
730
|
+
AccountAvatarComponent, AlertComponent, ButtonComponent, CardComponent, ComboboxComponent, DrawerComponent,
|
|
731
|
+
IconComponent, MenuButtonComponent, PaginatorComponent, SegmentedControlComponent, SkeletonComponent,
|
|
732
|
+
StateScreenComponent, SwitchComponent, TabComponent, TabContentDirective, TabsComponent, TagComponent,
|
|
733
|
+
TooltipDirective, SpinePageActionsDirective, SpineRatingStarsComponent, RatingsTableComponent, RatingsTrendComponent,
|
|
734
|
+
], providers: [AutoServiceRatingsService], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Na barra da p\u00E1gina do painel (fora de um painel, aqui mesmo): quando foi lido, o per\u00EDodo e Atualizar. -->\n<ng-template spinePageActions>\n <div class=\"ratings__page-actions\">\n @if (updatedAt(); as at) {\n <span class=\"ratings__updated\">{{ updatedText(at) }}</span>\n }\n <hm-segmented-control\n [label]=\"text.period\"\n [options]=\"periodOptions()\"\n [value]=\"period()\"\n (valueChange)=\"setPeriod($event)\"\n size=\"small\"\n emphasis=\"action\"\n />\n <hm-button emphasis=\"medium\" size=\"small\" icon=\"refresh\" [loading]=\"summaryLoading() || recentLoading()\" (clicked)=\"reload()\">\n {{ text.refresh }}\n </hm-button>\n </div>\n</ng-template>\n\n<div class=\"ratings\" [attr.aria-busy]=\"summaryLoading() || recentLoading()\">\n @if (error(); as message) {\n <hm-alert variant=\"error\" [closable]=\"true\" (closed)=\"error.set(null)\">{{ message }}</hm-alert>\n }\n\n @if (!summary() && summaryLoading()) {\n <hm-skeleton shape=\"cards\" [count]=\"2\" [label]=\"text.refresh\" />\n } @else if (empty()) {\n <hm-state-screen situation=\"empty\" [heading]=\"text.noRatings\" />\n } @else if (summary()) {\n <!-- KPIs baixos: m\u00E9dia em destaque, estrelas e totais \u00E0 esquerda; o histograma das 11 notas \u00E0 direita. -->\n <div class=\"ratings__kpis\">\n @for (card of overview(); track card.id) {\n <hm-card class=\"ratings__kpi\">\n <section class=\"ratings__kpi-body\" [attr.aria-labelledby]=\"card.headingId\">\n <header class=\"ratings__head\">\n <span class=\"ratings__badge ratings__badge--{{ card.tone }}\" aria-hidden=\"true\"><hm-icon [shape]=\"card.icon\" /></span>\n <span class=\"ratings__titles\">\n <h3 class=\"ratings__title\" [id]=\"card.headingId\">{{ card.title }}</h3>\n <span class=\"ratings__subtitle\">{{ card.hint }}</span>\n </span>\n <hm-button class=\"ratings__details\" emphasis=\"minimal\" size=\"small\" iconEnd=\"arrow-right\" (clicked)=\"details(card)\">\n {{ text.details }}<span class=\"ratings__sr\"> {{ detailsLabel(card) }}</span>\n </hm-button>\n </header>\n\n <div class=\"ratings__kpi-main\">\n <div class=\"ratings__figure\">\n <span class=\"ratings__average\" aria-hidden=\"true\">\n <span class=\"ratings__average-value\">{{ averageFigure(card.data) }}</span><span class=\"ratings__average-max\">{{ text.scoreMax }}</span>\n </span>\n @if (card.data.total) {\n <spine-rating-stars class=\"ratings__stars\" [readonly]=\"true\" [showValue]=\"false\" [value]=\"card.data.average\" />\n }\n <span class=\"ratings__totals\">{{ totals(card.data) }}</span>\n </div>\n\n <ul class=\"ratings__hist\" [attr.aria-label]=\"text.distribution\">\n @for (col of card.columns; track col.score) {\n <li class=\"ratings__hist-col\" [attr.aria-label]=\"col.label\" [hmTooltip]=\"col.tip\">\n <span class=\"ratings__hist-track\" aria-hidden=\"true\">\n <span class=\"ratings__hist-bar ratings__fill--{{ col.tone }}\" [style.height.%]=\"col.height\"></span>\n </span>\n <span class=\"ratings__hist-score\" aria-hidden=\"true\">{{ col.score }}</span>\n </li>\n }\n </ul>\n </div>\n </section>\n </hm-card>\n }\n </div>\n\n <!-- Duas colunas: as telas (abas) e as recentes, lado a lado. -->\n <div class=\"ratings__columns\">\n <div class=\"ratings__main\" #tabsCard>\n <hm-card class=\"ratings__tabs-card\">\n <hm-tabs [label]=\"text.ranking\" [active]=\"tab()\" (activeChange)=\"setTab($event)\">\n <hm-tab id=\"screens\" [label]=\"text.tabScreens\" [count]=\"screenRows().length\">\n <spine-ratings-table *hmTabContent mode=\"screens\" [rows]=\"screenRows()\" [(area)]=\"tableArea\" [selected]=\"allOpen() ? screen() : null\" (picked)=\"pickScreen($event)\" />\n </hm-tab>\n <hm-tab id=\"areas\" [label]=\"text.tabAreas\" [count]=\"areaRows().length\">\n <spine-ratings-table *hmTabContent mode=\"areas\" [rows]=\"areaRows()\" (picked)=\"pickArea($event)\" />\n </hm-tab>\n <hm-tab id=\"trends\" [label]=\"text.tabTrends\">\n <spine-ratings-trend *hmTabContent [series]=\"trendSeries()\" />\n </hm-tab>\n </hm-tabs>\n </hm-card>\n </div>\n\n <hm-card class=\"ratings__recent-card\">\n <section class=\"ratings__recent\" [attr.aria-labelledby]=\"uid + '-recent'\">\n <header class=\"ratings__head\">\n <span class=\"ratings__badge ratings__badge--warning\" aria-hidden=\"true\"><hm-icon shape=\"comment-message\" /></span>\n <span class=\"ratings__titles\">\n <h3 class=\"ratings__title\" [id]=\"uid + '-recent'\">{{ text.recent }}</h3>\n <span class=\"ratings__subtitle\">{{ text.recentHint }}</span>\n </span>\n </header>\n\n @if (!recent().length && recentLoading()) {\n <hm-skeleton shape=\"list\" [count]=\"3\" [label]=\"text.refresh\" />\n } @else if (!recent().length) {\n <p class=\"ratings__empty\">{{ text.emptyList }}</p>\n } @else {\n <ul class=\"ratings__feed\">\n @for (item of recent(); track item.id) {\n <li class=\"ratings__feed-item\">\n <hm-account-avatar class=\"ratings__feed-avatar\" size=\"small\" [name]=\"author(item)\" [imageUrl]=\"item.user_picture\" />\n <div class=\"ratings__feed-body\">\n <div class=\"ratings__feed-head\">\n <span class=\"ratings__author\">{{ author(item) }}</span>\n <span class=\"ratings__when\" [attr.title]=\"date(item.updated_at)\">{{ when(item.updated_at) }}</span>\n </div>\n <div class=\"ratings__feed-meta\">\n <spine-rating-stars class=\"ratings__stars\" size=\"small\" [readonly]=\"true\" [showValue]=\"false\" [value]=\"item.score\" />\n <span class=\"ratings__item-value\" aria-hidden=\"true\">{{ scoreShort(item.score) }}</span>\n <hm-tag class=\"ratings__chip\" [color]=\"item.scope === 'app' ? 'primary' : 'info'\" [name]=\"target(item)\" />\n </div>\n @if (item.comment) {\n <p class=\"ratings__feed-comment\">{{ item.comment }}</p>\n }\n </div>\n </li>\n }\n </ul>\n }\n <hm-button class=\"ratings__all\" emphasis=\"minimal\" size=\"small\" iconEnd=\"arrow-right\" (clicked)=\"openAll()\">\n {{ viewAllLabel() }}\n </hm-button>\n </section>\n </hm-card>\n </div>\n }\n</div>\n\n<!-- Todas as avalia\u00E7\u00F5es: a lista completa, com filtros e pagina\u00E7\u00E3o no servidor. -->\n<hm-drawer [heading]=\"text.allHeading\" size=\"large\" [(open)]=\"allOpen\">\n @if (allOpen()) {\n <div class=\"ratings__all-body\" [attr.aria-busy]=\"listLoading()\">\n <div class=\"ratings__filters\">\n <hm-combobox class=\"ratings__filter\" size=\"small\" [label]=\"text.filterScope\" [options]=\"scopeOptions()\" [value]=\"scope()\" (valueChange)=\"setScope($event)\" />\n <hm-combobox class=\"ratings__filter ratings__filter--wide\" size=\"small\" [label]=\"text.filterScreen\" [options]=\"screenOptions()\" [value]=\"screen()\" (valueChange)=\"setScreen($event)\" />\n <hm-combobox class=\"ratings__filter\" size=\"small\" [label]=\"text.filterScore\" [options]=\"scoreOptions()\" [value]=\"score()\" (valueChange)=\"setScore($event)\" />\n <hm-switch class=\"ratings__switch\" [label]=\"text.withComment\" [value]=\"withComment()\" (valueChange)=\"setWithComment($event)\" />\n @if (hasFilters()) {\n <hm-button emphasis=\"minimal\" size=\"small\" (clicked)=\"clearFilters()\">{{ text.clearFilters }}</hm-button>\n }\n </div>\n\n @if (!items().length && listLoading()) {\n <hm-skeleton shape=\"list\" [count]=\"3\" [label]=\"text.refresh\" />\n } @else if (!items().length) {\n <p class=\"ratings__empty\">{{ text.emptyList }}</p>\n } @else {\n <ul class=\"ratings__list\" [class.ratings__list--busy]=\"listLoading()\">\n @for (row of rows(); track row.item.id) {\n <li class=\"ratings__item\">\n <hm-account-avatar class=\"ratings__item-avatar\" size=\"small\" [name]=\"author(row.item)\" [imageUrl]=\"row.item.user_picture\" />\n <span class=\"ratings__author ratings__item-who\">{{ author(row.item) }}</span>\n <span class=\"ratings__item-score\">\n <spine-rating-stars class=\"ratings__stars\" size=\"small\" [readonly]=\"true\" [showValue]=\"false\" [value]=\"row.item.score\" />\n <span class=\"ratings__item-value\" aria-hidden=\"true\">{{ scoreShort(row.item.score) }}</span>\n </span>\n <span class=\"ratings__target\">\n <hm-tag class=\"ratings__chip\" [color]=\"row.item.scope === 'app' ? 'primary' : 'info'\" [name]=\"target(row.item)\" />\n @if (row.item.scope === 'screen' && row.item.screen_label && row.item.screen_path) {\n <span class=\"ratings__route\">{{ row.item.screen_path }}</span>\n }\n </span>\n @if (row.item.comment) {\n <p class=\"ratings__comment\">{{ row.item.comment }}</p>\n } @else {\n <p class=\"ratings__comment ratings__comment--none\">{{ text.noComment }}</p>\n }\n <span class=\"ratings__date\">{{ date(row.item.updated_at) }}</span>\n <span class=\"ratings__item-menu\">\n @if (row.menu.length) {\n <hm-menu-button\n size=\"small\"\n icon=\"ellipsis-vertical\"\n align=\"end\"\n [showArrow]=\"false\"\n [label]=\"text.itemActions\"\n [items]=\"row.menu\"\n (chosen)=\"onItemAction(row.item, $event)\"\n />\n }\n </span>\n </li>\n }\n </ul>\n @if (total() > pageState().size) {\n <hm-paginator\n class=\"ratings__pager\"\n variant=\"simple\"\n [label]=\"text.pagination\"\n [state]=\"pageState()\"\n [totalItems]=\"total()\"\n [sizes]=\"[10, 25, 50]\"\n (stateChanged)=\"onPage($event)\"\n />\n }\n }\n </div>\n }\n</hm-drawer>\n", styles: [".as-stack{display:flex;flex-direction:column;gap:var(--hm-space-md);min-inline-size:0}.as-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.as-field-row{display:flex;flex-wrap:wrap;align-items:flex-end;gap:var(--hm-space-sm)}.as-field-row>hm-combobox{flex:1 1 16rem;min-inline-size:0}.as-summary{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.as-summary__info{display:flex;flex:1 1 16rem;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.as-summary__title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs);margin:0;color:var(--hm-text);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold)}.as-summary__detail{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.muted{color:var(--hm-muted)}.as-empty{margin:var(--hm-space-xs) 0 0;font-size:var(--hm-font-size-sm)}.as-secret{display:grid;gap:var(--hm-space-micro)}.as-secret__label{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-secret__row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.as-secret__value{flex:1 1 240px;min-width:0;border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:var(--hm-radius-control);padding:var(--hm-space-xs) var(--hm-space-sm);background:var(--hm-surface-muted);color:var(--hm-text);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere;-webkit-user-select:all;user-select:all}:host{display:block;min-inline-size:0;container-type:inline-size}.ratings{display:flex;flex-direction:column;gap:var(--hm-space-md);min-inline-size:0}.ratings__sr{position:absolute;inline-size:1px;block-size:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.ratings__page-actions{display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-end;gap:var(--hm-space-xs) var(--hm-space-sm);min-inline-size:0}.ratings__updated{color:var(--hm-muted);font-size:var(--hm-font-size-xs);white-space:nowrap}.ratings__head{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm);min-inline-size:0}.ratings__titles{display:flex;flex:1 1 8rem;flex-direction:column;gap:var(--hm-space-nano);min-inline-size:0}.ratings__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold);line-height:var(--hm-line-height-tight)}.ratings__subtitle{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.ratings__details{flex:none;margin-inline-start:auto}.ratings__badge{display:inline-flex;flex:none;align-items:center;justify-content:center;inline-size:2.25rem;block-size:2.25rem;border-radius:var(--hm-radius);font-size:var(--hm-icon-md)}.ratings__badge--accent{background:var(--hm-accent-subtle);color:var(--hm-accent-strong)}.ratings__badge--info{background:var(--hm-info-bg);color:var(--hm-info)}.ratings__badge--warning{background:var(--hm-warning-bg);color:var(--hm-warning)}.ratings__kpis{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--hm-space-md)}@container (max-width: 56rem){.ratings__kpis{grid-template-columns:minmax(0,1fr)}}.ratings__kpi-body{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-inline-size:0}.ratings__kpi-main{display:flex;align-items:flex-end;gap:var(--hm-space-lg);min-inline-size:0}.ratings__figure{display:flex;flex:none;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.ratings__average{display:inline-flex;align-items:baseline;gap:var(--hm-space-nano);color:var(--hm-text);font-variant-numeric:tabular-nums;line-height:1}.ratings__average-value{font-size:calc(var(--hm-font-size-xxl) * 1.4);font-weight:var(--hm-font-weight-bold);letter-spacing:-.02em}.ratings__average-max{color:var(--hm-muted);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-medium)}.ratings__totals{color:var(--hm-muted);font-size:var(--hm-font-size-xs);white-space:nowrap}.ratings__stars{flex:none}.ratings__hist{display:grid;flex:1 1 auto;grid-template-columns:repeat(11,minmax(0,1fr));gap:var(--hm-space-micro);min-inline-size:0;margin:0;padding:0;list-style:none}.ratings__hist-col{display:flex;flex-direction:column;align-items:center;gap:var(--hm-space-nano);min-inline-size:0;font-size:var(--hm-font-size-xs);font-variant-numeric:tabular-nums;cursor:default}.ratings__hist-col:hover .ratings__hist-track{box-shadow:0 0 0 2px var(--hm-divider)}.ratings__hist-track{display:flex;align-items:flex-end;inline-size:100%;max-inline-size:1.75rem;block-size:3.75rem;border-radius:var(--hm-radius-micro);background:var(--hm-surface-muted);overflow:hidden}.ratings__hist-bar{display:block;inline-size:100%;border-radius:var(--hm-radius-micro) var(--hm-radius-micro) 0 0;transition:height .16s ease}.ratings__hist-score{color:var(--hm-muted)}@container (max-width: 30rem){.ratings__kpi-main{flex-direction:column;align-items:stretch;gap:var(--hm-space-sm)}.ratings__figure{flex-flow:row wrap;align-items:center;gap:var(--hm-space-xs) var(--hm-space-sm)}}.ratings__fill--success{background:var(--hm-success)}.ratings__fill--warning{background:var(--hm-warning)}.ratings__fill--danger{background:var(--hm-danger)}.ratings__fill--neutral{background:transparent}.ratings__columns{display:grid;grid-template-columns:minmax(0,2fr) minmax(18rem,1fr);align-items:start;gap:var(--hm-space-md)}@container (max-width: 64rem){.ratings__columns{grid-template-columns:minmax(0,1fr)}}.ratings__main{min-inline-size:0;scroll-margin-block-start:var(--hm-space-md)}.ratings__recent{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-inline-size:0}.ratings__empty{margin:0;padding:var(--hm-space-md);border-radius:var(--hm-radius-control);background:var(--hm-surface-muted);color:var(--hm-muted);text-align:center}.ratings__feed{display:flex;flex-direction:column;gap:var(--hm-space-micro);margin:0;padding:0;list-style:none}.ratings__feed-item{display:flex;align-items:flex-start;gap:var(--hm-space-sm);padding:var(--hm-space-xs) var(--hm-space-sm);border-radius:var(--hm-radius-control);background:var(--hm-surface-muted);font-size:var(--hm-font-size-sm)}.ratings__feed-avatar{flex:none}.ratings__feed-body{display:flex;flex:1 1 auto;flex-direction:column;gap:var(--hm-space-nano);min-inline-size:0}.ratings__feed-head{display:flex;align-items:baseline;gap:var(--hm-space-xs);min-inline-size:0}.ratings__author{overflow:hidden;color:var(--hm-text);font-weight:var(--hm-font-weight-semibold);text-overflow:ellipsis;white-space:nowrap}.ratings__when{flex:none;margin-inline-start:auto;color:var(--hm-muted);font-size:var(--hm-font-size-xs);white-space:nowrap}.ratings__feed-meta{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs);min-inline-size:0}.ratings__feed-meta>hm-tag{max-inline-size:100%}.ratings__item-value{color:var(--hm-text);font-variant-numeric:tabular-nums;font-weight:var(--hm-font-weight-semibold)}.ratings__feed-comment{display:-webkit-box;margin:0;overflow:hidden;color:var(--hm-text);overflow-wrap:anywhere;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2}.ratings__all{align-self:flex-end}.ratings__all-body{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-inline-size:0;container-type:inline-size}.ratings__filters{display:flex;flex-wrap:wrap;align-items:flex-end;gap:var(--hm-space-xs) var(--hm-space-sm)}.ratings__filter{flex:1 1 9rem;min-inline-size:0}.ratings__filter--wide{flex-basis:13rem}.ratings__switch{flex:none;padding-block-end:var(--hm-space-xs)}.ratings__list{display:flex;flex-direction:column;gap:var(--hm-space-micro);margin:0;padding:0;list-style:none;transition:opacity .12s ease}.ratings__list--busy{opacity:.55}.ratings__item{display:grid;grid-template-areas:\"avatar who score date menu\" \"avatar target target target target\" \"avatar comment comment comment comment\";grid-template-columns:auto minmax(0,1fr) auto auto 2rem;align-items:center;gap:var(--hm-space-nano) var(--hm-space-sm);padding:var(--hm-space-xs) var(--hm-space-sm);border-radius:var(--hm-radius-control);background:var(--hm-surface-muted);font-size:var(--hm-font-size-sm)}.ratings__item-avatar{grid-area:avatar;align-self:start}.ratings__item-who{grid-area:who}.ratings__item-score{display:inline-flex;grid-area:score;align-items:center;gap:var(--hm-space-xs);white-space:nowrap}.ratings__target{display:flex;flex-wrap:wrap;grid-area:target;align-items:center;gap:var(--hm-space-xs);min-inline-size:0}.ratings__target>hm-tag{max-inline-size:100%}.ratings__route{overflow:hidden;color:var(--hm-muted);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-xs);text-overflow:ellipsis;white-space:nowrap}.ratings__comment{grid-area:comment;margin:0;color:var(--hm-text);white-space:pre-line;overflow-wrap:anywhere}.ratings__comment--none{color:var(--hm-muted);font-style:italic}.ratings__date{grid-area:date;color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-variant-numeric:tabular-nums;white-space:nowrap}.ratings__item-menu{display:inline-flex;grid-area:menu;justify-content:flex-end}@container (max-width: 28rem){.ratings__item{grid-template-areas:\"avatar who menu\" \"avatar score date\" \"avatar target target\" \"avatar comment comment\";grid-template-columns:auto minmax(0,1fr) auto}}.ratings__pager{margin-block-start:var(--hm-space-xs)}@media(prefers-reduced-motion:reduce){.ratings__hist-bar,.ratings__list{transition:none}}:host-context([data-motion=reduce]) .ratings__hist-bar,:host-context([data-motion=reduce]) .ratings__list{transition:none}\n"] }]
|
|
735
|
+
}], ctorParameters: () => [], propDecorators: { clientId: [{ type: i0.Input, args: [{ isSignal: true, alias: "clientId", required: true }] }], tabsCard: [{ type: i0.ViewChild, args: ['tabsCard', { isSignal: true }] }] } });
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Tela "Avaliações" do auto-serviço (router `AUTO_SERVICE_RATINGS_TEMPLATE_ROUTER_ID`, na raiz do menu; migration 0036
|
|
739
|
+
* do argos): o seletor de client + o painel de avaliações que era a aba Cliente › Avaliações. Carregada só por
|
|
740
|
+
* `AUTO_SERVICE_SCREENS.ratings` -- nunca exportada pelo ponto de entrada (ver `public-api.ts`).
|
|
741
|
+
*/
|
|
742
|
+
class AutoServiceRatingsPageComponent {
|
|
743
|
+
constructor() {
|
|
744
|
+
this.client = signal(null, ...(ngDevMode ? [{ debugName: "client" }] : /* istanbul ignore next */ []));
|
|
745
|
+
}
|
|
746
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceRatingsPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
747
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: AutoServiceRatingsPageComponent, isStandalone: true, selector: "spine-auto-service-ratings-page", ngImport: i0, template: `
|
|
748
|
+
<div class="as-stack">
|
|
749
|
+
<spine-auto-service-client-picker (clientChange)="client.set($event)" />
|
|
750
|
+
@if (client(); as c) {
|
|
751
|
+
<spine-auto-service-ratings [clientId]="c.id" />
|
|
752
|
+
}
|
|
753
|
+
</div>
|
|
754
|
+
`, isInline: true, styles: [".as-stack{display:flex;flex-direction:column;gap:var(--hm-space-md);min-inline-size:0}.as-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.as-field-row{display:flex;flex-wrap:wrap;align-items:flex-end;gap:var(--hm-space-sm)}.as-field-row>hm-combobox{flex:1 1 16rem;min-inline-size:0}.as-summary{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.as-summary__info{display:flex;flex:1 1 16rem;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.as-summary__title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs);margin:0;color:var(--hm-text);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold)}.as-summary__detail{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.muted{color:var(--hm-muted)}.as-empty{margin:var(--hm-space-xs) 0 0;font-size:var(--hm-font-size-sm)}.as-secret{display:grid;gap:var(--hm-space-micro)}.as-secret__label{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-secret__row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.as-secret__value{flex:1 1 240px;min-width:0;border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:var(--hm-radius-control);padding:var(--hm-space-xs) var(--hm-space-sm);background:var(--hm-surface-muted);color:var(--hm-text);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere;-webkit-user-select:all;user-select:all}:host{display:block;min-inline-size:0}\n"], dependencies: [{ kind: "component", type: AutoServiceClientPickerComponent, selector: "spine-auto-service-client-picker", outputs: ["clientChange"] }, { kind: "component", type: AutoServiceRatingsComponent, selector: "spine-auto-service-ratings", inputs: ["clientId"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
755
|
+
}
|
|
756
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceRatingsPageComponent, decorators: [{
|
|
757
|
+
type: Component,
|
|
758
|
+
args: [{ selector: 'spine-auto-service-ratings-page', imports: [AutoServiceClientPickerComponent, AutoServiceRatingsComponent], template: `
|
|
759
|
+
<div class="as-stack">
|
|
760
|
+
<spine-auto-service-client-picker (clientChange)="client.set($event)" />
|
|
761
|
+
@if (client(); as c) {
|
|
762
|
+
<spine-auto-service-ratings [clientId]="c.id" />
|
|
763
|
+
}
|
|
764
|
+
</div>
|
|
765
|
+
`, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".as-stack{display:flex;flex-direction:column;gap:var(--hm-space-md);min-inline-size:0}.as-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.as-field-row{display:flex;flex-wrap:wrap;align-items:flex-end;gap:var(--hm-space-sm)}.as-field-row>hm-combobox{flex:1 1 16rem;min-inline-size:0}.as-summary{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.as-summary__info{display:flex;flex:1 1 16rem;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.as-summary__title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs);margin:0;color:var(--hm-text);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold)}.as-summary__detail{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.muted{color:var(--hm-muted)}.as-empty{margin:var(--hm-space-xs) 0 0;font-size:var(--hm-font-size-sm)}.as-secret{display:grid;gap:var(--hm-space-micro)}.as-secret__label{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-secret__row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.as-secret__value{flex:1 1 240px;min-width:0;border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:var(--hm-radius-control);padding:var(--hm-space-xs) var(--hm-space-sm);background:var(--hm-surface-muted);color:var(--hm-text);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere;-webkit-user-select:all;user-select:all}:host{display:block;min-inline-size:0}\n"] }]
|
|
766
|
+
}] });
|
|
767
|
+
|
|
768
|
+
export { AutoServiceRatingsPageComponent };
|
|
769
|
+
//# sourceMappingURL=trismegisto-spine-auto-service-auto-service-ratings-page.component-C_THGTkw.mjs.map
|