@trismegisto/spine 3.2.0 → 3.3.1
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-apps.usage-po9Ds_SU.mjs → trismegisto-spine-auto-service-apps.usage-C0F_MlCo.mjs} +3 -3
- package/fesm2022/{trismegisto-spine-auto-service-apps.usage-po9Ds_SU.mjs.map → trismegisto-spine-auto-service-apps.usage-C0F_MlCo.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-DzmlpLPu.mjs → trismegisto-spine-auto-service-auto-service-client.component-BAfeLyTB.mjs} +32 -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-C4FBAefe.mjs → trismegisto-spine-auto-service-auto-service.component-QsIQ61w2.mjs} +36 -23
- package/fesm2022/trismegisto-spine-auto-service-auto-service.component-QsIQ61w2.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-client.usage-B16r3IHk.mjs → trismegisto-spine-auto-service-client.usage-CJxn9Ysz.mjs} +7 -4
- 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 +50 -20
- 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 +3171 -1291
- 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-notification.mjs +27 -24
- package/fesm2022/trismegisto-spine-notification.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 +20 -45
- 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 +1972 -282
- 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 +1090 -286
- 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-notification.d.ts +13 -12
- package/types/trismegisto-spine-notification.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-DzmlpLPu.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-C4FBAefe.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-client.usage-B16r3IHk.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-D6cb4B3Z.mjs +0 -43
- package/fesm2022/trismegisto-spine-auto-service-home.usage-D6cb4B3Z.mjs.map +0 -1
package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-DIeunpQ9.mjs
ADDED
|
@@ -0,0 +1,2123 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, Injectable, input, output, computed, ChangeDetectionStrategy, Component, signal, effect, untracked, viewChild } from '@angular/core';
|
|
3
|
+
import { A as AutoServiceClientPickerComponent } from './trismegisto-spine-auto-service-auto-service-client-picker.component-NawbfdLW.mjs';
|
|
4
|
+
import { Location } from '@angular/common';
|
|
5
|
+
import { Router } from '@angular/router';
|
|
6
|
+
import { AuthorizationService, AUTH_CONFIG } from '@orfeu/angular/core';
|
|
7
|
+
import { ButtonComponent, IconComponent, AccountAvatarComponent, SearchFieldComponent, CheckboxComponent, HermesToastService, HermesDialogService, AlertComponent, DatePickerComponent, IconButtonComponent, InputComponent, ModalComponent, PopoverComponent, PopoverTriggerDirective, RichTextComponent, SelectComponent, SwitchComponent, TabComponent, TabsComponent, TagComponent, TextareaComponent, EmptyComponent, KanbanCardDirective, KanbanColumnActionsDirective, KanbanColumnEmptyDirective, KanbanColumnFooterDirective, KanbanColumnSummaryDirective, KanbanComponent, MenuButtonComponent, PaginatorComponent, SegmentedControlComponent } from '@trismegisto/angular';
|
|
8
|
+
import { urlPath, formatMessage, pluralMessage } from '@trismegisto/primitives';
|
|
9
|
+
import { SPINE_MESSAGES, SpinePreferencesService, SpineUserStorage } from '@trismegisto/spine';
|
|
10
|
+
import { SPINE_SUGGESTION_STATUSES, SPINE_SUGGESTION_STATUS_COLORS, SPINE_SUGGESTION_KINDS, SPINE_SUGGESTION_KIND_COLORS, SPINE_SUGGESTION_LIMITS, SPINE_SUGGESTION_STATUS_ICONS, SPINE_SUGGESTION_KIND_ICONS } from '@trismegisto/spine/layout';
|
|
11
|
+
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
|
12
|
+
import { firstValueFrom } from 'rxjs';
|
|
13
|
+
import { AUTO_SERVICE_CONFIG } from './trismegisto-spine-auto-service.mjs';
|
|
14
|
+
|
|
15
|
+
/** Janelas quando o servidor não diz (as mesmas constantes do argos-admin). */
|
|
16
|
+
const AUTO_SERVICE_SUGGESTION_DONE_DAYS = 7;
|
|
17
|
+
const AUTO_SERVICE_SUGGESTION_DUE_SOON_DAYS = 1;
|
|
18
|
+
/**
|
|
19
|
+
* O quadro de trabalho das sugestões do client — `argos-admin /auto-service/:clientId/suggestions` (atrás do
|
|
20
|
+
* clientAdminGuard, como o resto do auto-serviço). Serviço próprio da tela, para não engordar o `AutoServiceService`.
|
|
21
|
+
* Toda mudança na tarefa responde a tarefa inteira (com o checklist completo); mover responde a tarefa leve.
|
|
22
|
+
*/
|
|
23
|
+
class AutoServiceSuggestionsService {
|
|
24
|
+
constructor() {
|
|
25
|
+
this.http = inject(HttpClient);
|
|
26
|
+
this.auth = inject(AuthorizationService);
|
|
27
|
+
this.config = inject(AUTO_SERVICE_CONFIG);
|
|
28
|
+
}
|
|
29
|
+
url(strings, ...ids) {
|
|
30
|
+
return `${this.config.adminApi}/auto-service${urlPath(strings, ...ids)}`;
|
|
31
|
+
}
|
|
32
|
+
get headers() {
|
|
33
|
+
return new HttpHeaders({ 'x-access-token': this.auth.getToken() ?? '' });
|
|
34
|
+
}
|
|
35
|
+
read(url, params) {
|
|
36
|
+
return firstValueFrom(this.http.get(url, { headers: this.headers, params }));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* O quadro (`board=true`): sem arquivadas, recusadas e concluídas fora da janela — o corte é do servidor. A tela
|
|
40
|
+
* filtra o resto sobre a lista inteira: a posição de cada cartão é a da coluna inteira.
|
|
41
|
+
*/
|
|
42
|
+
async board(clientId) {
|
|
43
|
+
const res = await this.read(this.url `/${clientId}/suggestions`, new HttpParams().set('board', 'true'));
|
|
44
|
+
return {
|
|
45
|
+
suggestions: res.suggestions ?? [],
|
|
46
|
+
doneDays: res.done_days ?? AUTO_SERVICE_SUGGESTION_DONE_DAYS,
|
|
47
|
+
dueSoonDays: res.due_soon_days ?? AUTO_SERVICE_SUGGESTION_DUE_SOON_DAYS,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** O arquivo (`archive=true`): arquivadas, recusadas e concluídas antigas, paginado e filtrado no servidor. */
|
|
51
|
+
async archive(clientId, q) {
|
|
52
|
+
let params = new HttpParams().set('archive', 'true').set('limit', q.limit).set('offset', q.offset);
|
|
53
|
+
if (q.search)
|
|
54
|
+
params = params.set('search', q.search);
|
|
55
|
+
if (q.kind)
|
|
56
|
+
params = params.set('kind', q.kind);
|
|
57
|
+
if (q.status)
|
|
58
|
+
params = params.set('status', q.status);
|
|
59
|
+
const res = await this.read(this.url `/${clientId}/suggestions`, params);
|
|
60
|
+
return { suggestions: res.suggestions ?? [], total: res.total ?? 0 };
|
|
61
|
+
}
|
|
62
|
+
/** A tarefa inteira (checklist completo, pessoas, rótulos, última atividade). */
|
|
63
|
+
task(clientId, id) {
|
|
64
|
+
return this.read(this.url `/${clientId}/suggestions/${id}`);
|
|
65
|
+
}
|
|
66
|
+
/** Pela chave ("SUG-12"): o link copiado abre a tarefa. */
|
|
67
|
+
lookup(clientId, key) {
|
|
68
|
+
return this.read(this.url `/${clientId}/suggestions/lookup`, new HttpParams().set('key', key));
|
|
69
|
+
}
|
|
70
|
+
create(clientId, draft) {
|
|
71
|
+
return firstValueFrom(this.http.post(this.url `/${clientId}/suggestions`, draft, { headers: this.headers }));
|
|
72
|
+
}
|
|
73
|
+
update(clientId, id, edit) {
|
|
74
|
+
return firstValueFrom(this.http.patch(this.url `/${clientId}/suggestions/${id}`, edit, { headers: this.headers }));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Arrastar ou mudar pelo detalhe: status (coluna) e/ou posição (índice na coluna de destino, contando a coluna
|
|
78
|
+
* inteira); `comment` vai junto com a mudança de status (`internal` = só a equipe vê).
|
|
79
|
+
*/
|
|
80
|
+
move(clientId, id, change) {
|
|
81
|
+
return firstValueFrom(this.http.patch(this.url `/${clientId}/suggestions/${id}`, change, { headers: this.headers }));
|
|
82
|
+
}
|
|
83
|
+
remove(clientId, id) {
|
|
84
|
+
return firstValueFrom(this.http.delete(this.url `/${clientId}/suggestions/${id}`, { headers: this.headers }));
|
|
85
|
+
}
|
|
86
|
+
setArchived(clientId, id, archived) {
|
|
87
|
+
const url = archived ? this.url `/${clientId}/suggestions/${id}/archive` : this.url `/${clientId}/suggestions/${id}/restore`;
|
|
88
|
+
return firstValueFrom(this.http.post(url, {}, { headers: this.headers }));
|
|
89
|
+
}
|
|
90
|
+
/** Prévia do agrupamento em `into`: quantos relatores e apoios passariam (nada é gravado). */
|
|
91
|
+
mergePreview(clientId, id, into) {
|
|
92
|
+
return this.read(this.url `/${clientId}/suggestions/${id}/merge`, new HttpParams().set('into', into));
|
|
93
|
+
}
|
|
94
|
+
/** Agrupa esta em `into`: responde o destino inteiro (com os relatores e as agrupadas). */
|
|
95
|
+
merge(clientId, id, into) {
|
|
96
|
+
return firstValueFrom(this.http.post(this.url `/${clientId}/suggestions/${id}/merge`, { into }, { headers: this.headers }));
|
|
97
|
+
}
|
|
98
|
+
/** Desagrupa esta (a origem): ela volta ao quadro; responde ela inteira. */
|
|
99
|
+
unmerge(clientId, id) {
|
|
100
|
+
return firstValueFrom(this.http.post(this.url `/${clientId}/suggestions/${id}/unmerge`, {}, { headers: this.headers }));
|
|
101
|
+
}
|
|
102
|
+
/** Comentários ou atividades, do mais novo, paginado. */
|
|
103
|
+
events(clientId, id, q) {
|
|
104
|
+
const params = new HttpParams().set('type', q.type).set('limit', q.limit).set('offset', q.offset);
|
|
105
|
+
return this.read(this.url `/${clientId}/suggestions/${id}/events`, params);
|
|
106
|
+
}
|
|
107
|
+
comment(clientId, id, comment, internal = false) {
|
|
108
|
+
return firstValueFrom(this.http.post(this.url `/${clientId}/suggestions/${id}/comments`, { comment, internal }, { headers: this.headers }));
|
|
109
|
+
}
|
|
110
|
+
/** Responsável/envolvido: `userId` ou "me" (atribuir a mim). */
|
|
111
|
+
addPerson(clientId, id, userId, role) {
|
|
112
|
+
return firstValueFrom(this.http.post(this.url `/${clientId}/suggestions/${id}/people`, { user_id: userId, role }, { headers: this.headers }));
|
|
113
|
+
}
|
|
114
|
+
removePerson(clientId, id, userId, role) {
|
|
115
|
+
return firstValueFrom(this.http.delete(this.url `/${clientId}/suggestions/${id}/people/${userId}`, {
|
|
116
|
+
headers: this.headers, params: new HttpParams().set('role', role),
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
addChecklistItem(clientId, id, text) {
|
|
120
|
+
return firstValueFrom(this.http.post(this.url `/${clientId}/suggestions/${id}/checklist`, { text }, { headers: this.headers }));
|
|
121
|
+
}
|
|
122
|
+
updateChecklistItem(clientId, id, itemId, change) {
|
|
123
|
+
return firstValueFrom(this.http.patch(this.url `/${clientId}/suggestions/${id}/checklist/${itemId}`, change, { headers: this.headers }));
|
|
124
|
+
}
|
|
125
|
+
removeChecklistItem(clientId, id, itemId) {
|
|
126
|
+
return firstValueFrom(this.http.delete(this.url `/${clientId}/suggestions/${id}/checklist/${itemId}`, { headers: this.headers }));
|
|
127
|
+
}
|
|
128
|
+
// --- rótulos ---
|
|
129
|
+
async labels(clientId) {
|
|
130
|
+
const res = await this.read(this.url `/${clientId}/suggestions/labels`);
|
|
131
|
+
return res.labels ?? [];
|
|
132
|
+
}
|
|
133
|
+
createLabel(clientId, name, color) {
|
|
134
|
+
return firstValueFrom(this.http.post(this.url `/${clientId}/suggestions/labels`, { name, color }, { headers: this.headers }));
|
|
135
|
+
}
|
|
136
|
+
setLabel(clientId, id, labelId, linked) {
|
|
137
|
+
const url = this.url `/${clientId}/suggestions/${id}/labels/${labelId}`;
|
|
138
|
+
return firstValueFrom(linked
|
|
139
|
+
? this.http.put(url, {}, { headers: this.headers })
|
|
140
|
+
: this.http.delete(url, { headers: this.headers }));
|
|
141
|
+
}
|
|
142
|
+
// --- pessoas do realm do app (a mesma busca da aba Grupos, com a foto) ---
|
|
143
|
+
async searchPeople(clientId, search) {
|
|
144
|
+
let params = new HttpParams().set('limit', 10);
|
|
145
|
+
if (search.trim())
|
|
146
|
+
params = params.set('search', search.trim());
|
|
147
|
+
const res = await this.read(this.url `/${clientId}/users`, params);
|
|
148
|
+
return res.users ?? [];
|
|
149
|
+
}
|
|
150
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceSuggestionsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
151
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceSuggestionsService, providedIn: 'root' }); }
|
|
152
|
+
}
|
|
153
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceSuggestionsService, decorators: [{
|
|
154
|
+
type: Injectable,
|
|
155
|
+
args: [{ providedIn: 'root' }]
|
|
156
|
+
}] });
|
|
157
|
+
|
|
158
|
+
// O que a equipe vê de uma sugestão no quadro de trabalho (auto-serviço): a sugestão de quem pediu mais o que é da
|
|
159
|
+
// tarefa -- chave, prioridade, datas, capa, descrição rica, pessoas, rótulos e checklist. Constantes e contas puras
|
|
160
|
+
// (prazo, capa) ficam aqui, fora dos componentes, para o cartão, o detalhe e os testes usarem as mesmas.
|
|
161
|
+
const TASK_PRIORITIES = ['urgente', 'alta', 'media', 'baixa'];
|
|
162
|
+
/** Ícone e intenção de cada prioridade (o texto vem do catálogo; a cor nunca é a única pista). */
|
|
163
|
+
const TASK_PRIORITY_ICONS = {
|
|
164
|
+
urgente: 'exclamation-triangle', alta: 'arrow-up', media: 'minus', baixa: 'arrow-bottom',
|
|
165
|
+
};
|
|
166
|
+
/** Capas: tokens da paleta do tema (o servidor só aceita estes). Sólidas e gradientes. */
|
|
167
|
+
const TASK_SOLID_COVERS = ['cat-1', 'cat-2', 'cat-3', 'cat-4', 'cat-5', 'cat-6', 'cat-7', 'cat-8'];
|
|
168
|
+
const TASK_GRADIENT_COVERS = ['grad-1', 'grad-2', 'grad-3', 'grad-4', 'grad-5', 'grad-6'];
|
|
169
|
+
const TASK_LABEL_COLORS = TASK_SOLID_COVERS;
|
|
170
|
+
/** Hoje como AAAA-MM-DD no fuso dado (o do perfil da pessoa). */
|
|
171
|
+
function todayIso(timeZone, now = new Date()) {
|
|
172
|
+
try {
|
|
173
|
+
return new Intl.DateTimeFormat('en-CA', { timeZone, year: 'numeric', month: '2-digit', day: '2-digit' }).format(now);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return now.toISOString().slice(0, 10);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/** AAAA-MM-DD mais `days` dias (negativo volta). */
|
|
180
|
+
function addDays(iso, days) {
|
|
181
|
+
const d = new Date(`${iso}T00:00:00Z`);
|
|
182
|
+
d.setUTCDate(d.getUTCDate() + days);
|
|
183
|
+
return d.toISOString().slice(0, 10);
|
|
184
|
+
}
|
|
185
|
+
/** O prazo da tarefa contra hoje. Concluída/recusada não vence. */
|
|
186
|
+
function dueState(task, today) {
|
|
187
|
+
if (!task.due_date || task.status === 'concluida' || task.status === 'recusada')
|
|
188
|
+
return null;
|
|
189
|
+
if (task.due_date < today)
|
|
190
|
+
return 'overdue';
|
|
191
|
+
if (task.due_date === today)
|
|
192
|
+
return 'today';
|
|
193
|
+
if (task.due_date === addDays(today, 1))
|
|
194
|
+
return 'tomorrow';
|
|
195
|
+
return 'later';
|
|
196
|
+
}
|
|
197
|
+
/** Dias inteiros entre duas datas AAAA-MM-DD (`to` - `from`). */
|
|
198
|
+
function daysBetween(from, to) {
|
|
199
|
+
return Math.round((Date.parse(`${to}T00:00:00Z`) - Date.parse(`${from}T00:00:00Z`)) / 86_400_000);
|
|
200
|
+
}
|
|
201
|
+
/** Duração em dias, horas e minutos inteiros (o "4h 12min" do quadro). Negativa vira zero. */
|
|
202
|
+
function splitDuration(ms) {
|
|
203
|
+
const minutes = Math.max(0, Math.floor(ms / 60_000));
|
|
204
|
+
return { d: Math.floor(minutes / 1440), h: Math.floor((minutes % 1440) / 60), m: minutes % 60 };
|
|
205
|
+
}
|
|
206
|
+
/** Responsável por mim? (o `sub` da sessão) */
|
|
207
|
+
const assignedTo = (task, userId) => !!userId && task.assignees.some((p) => p.user_id === userId);
|
|
208
|
+
|
|
209
|
+
// Os pequenos seletores do quadro de sugestões, para ir dentro de um `hm-popover` (o cartão e o detalhe da tarefa):
|
|
210
|
+
// capa (cores e gradientes da paleta do tema), pessoas do realm do app (com foto) e rótulos do catálogo do app.
|
|
211
|
+
/**
|
|
212
|
+
* Capa: amostras sólidas e em gradiente (tokens da paleta, o nome falado em cada uma) e "Remover capa". A escolhida
|
|
213
|
+
* fica `aria-pressed`. Emite o token (ou null).
|
|
214
|
+
*/
|
|
215
|
+
class SuggestionCoverPickerComponent {
|
|
216
|
+
constructor() {
|
|
217
|
+
this.text = inject(SPINE_MESSAGES).suggestions;
|
|
218
|
+
this.value = input(null, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
219
|
+
this.picked = output();
|
|
220
|
+
this.groups = computed(() => [
|
|
221
|
+
{ heading: this.text.coverSolid, items: TASK_SOLID_COVERS.map((token, i) => ({ token, name: formatMessage(this.text.coverName, { n: i + 1 }) })) },
|
|
222
|
+
{ heading: this.text.coverGradient, items: TASK_GRADIENT_COVERS.map((token, i) => ({ token, name: formatMessage(this.text.gradientName, { n: i + 1 }) })) },
|
|
223
|
+
], ...(ngDevMode ? [{ debugName: "groups" }] : /* istanbul ignore next */ []));
|
|
224
|
+
}
|
|
225
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionCoverPickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
226
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SuggestionCoverPickerComponent, isStandalone: true, selector: "spine-suggestion-cover-picker", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { picked: "picked" }, ngImport: i0, template: `
|
|
227
|
+
<div class="as-picker">
|
|
228
|
+
@for (group of groups(); track group.heading) {
|
|
229
|
+
<span class="as-picker__heading">{{ group.heading }}</span>
|
|
230
|
+
<div class="as-picker__swatches">
|
|
231
|
+
@for (c of group.items; track c.token) {
|
|
232
|
+
<button type="button" class="as-picker__swatch" [class]="'as-tok--' + c.token" [attr.aria-label]="c.name"
|
|
233
|
+
[attr.title]="c.name" [attr.aria-pressed]="value() === c.token" (click)="picked.emit(c.token)">
|
|
234
|
+
@if (value() === c.token) { <hm-icon shape="check" /> }
|
|
235
|
+
</button>
|
|
236
|
+
}
|
|
237
|
+
</div>
|
|
238
|
+
}
|
|
239
|
+
<hm-button size="small" emphasis="minimal" icon="times" [disabled]="!value()" (clicked)="picked.emit(null)">{{ text.coverRemove }}</hm-button>
|
|
240
|
+
</div>
|
|
241
|
+
`, isInline: true, styles: [".as-tok--cat-1{--as-tok: var(--hm-data-categorical-1);background:var(--as-tok)}.as-tok--cat-2{--as-tok: var(--hm-data-categorical-2);background:var(--as-tok)}.as-tok--cat-3{--as-tok: var(--hm-data-categorical-3);background:var(--as-tok)}.as-tok--cat-4{--as-tok: var(--hm-data-categorical-4);background:var(--as-tok)}.as-tok--cat-5{--as-tok: var(--hm-data-categorical-5);background:var(--as-tok)}.as-tok--cat-6{--as-tok: var(--hm-data-categorical-6);background:var(--as-tok)}.as-tok--cat-7{--as-tok: var(--hm-data-categorical-7);background:var(--as-tok)}.as-tok--cat-8{--as-tok: var(--hm-data-categorical-8);background:var(--as-tok)}.as-tok--grad-1{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-2))}.as-tok--grad-2{--as-tok: var(--hm-data-categorical-3);background:linear-gradient(135deg,var(--hm-data-categorical-3),var(--hm-data-categorical-4))}.as-tok--grad-3{--as-tok: var(--hm-data-categorical-5);background:linear-gradient(135deg,var(--hm-data-categorical-5),var(--hm-data-categorical-6))}.as-tok--grad-4{--as-tok: var(--hm-data-categorical-7);background:linear-gradient(135deg,var(--hm-data-categorical-7),var(--hm-data-categorical-8))}.as-tok--grad-5{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-5))}.as-tok--grad-6{--as-tok: var(--hm-data-categorical-2);background:linear-gradient(135deg,var(--hm-data-categorical-2),var(--hm-data-categorical-6))}.as-label{display:inline-flex;align-items:center;gap:var(--hm-space-micro);max-inline-size:100%;padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:color-mix(in srgb,var(--as-tok, var(--hm-border)) 18%,var(--hm-surface-raised));color:var(--hm-text);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);line-height:1.6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.as-label:before{flex:none;inline-size:.5rem;block-size:.5rem;border-radius:50%;background:var(--as-tok, var(--hm-border));content:\"\"}@media(forced-colors:active){.as-label{border:var(--hm-border-hairline) solid CanvasText}}.as-picker{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-inline-size:15rem;max-inline-size:20rem}.as-picker__heading{color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold)}.as-picker__muted{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.as-picker__swatches{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--hm-space-xxxs)}.as-picker__swatch{display:inline-flex;align-items:center;justify-content:center;block-size:2rem;padding:0;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);color:var(--hm-surface-raised);cursor:pointer}.as-picker__swatch[aria-pressed=true]{outline:var(--hm-border-thin) solid var(--hm-text);outline-offset:1px}.as-picker__swatch:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:1px}.as-picker__swatch--small{block-size:1.5rem}.as-picker__list{display:flex;flex-direction:column;gap:var(--hm-space-micro);max-block-size:16rem;margin:0;padding:0;overflow-y:auto;list-style:none}.as-picker__person{display:flex;align-items:center;gap:var(--hm-space-xs);inline-size:100%;padding:var(--hm-space-micro) var(--hm-space-xxxs);border:0;border-radius:var(--hm-radius-control);background:none;color:var(--hm-text);font:inherit;text-align:start;cursor:pointer}.as-picker__person:hover{background:var(--hm-hover)}.as-picker__person:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color)}.as-picker__person-text{display:flex;flex-direction:column;min-inline-size:0}.as-picker__person-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.as-picker__label-row{display:flex;align-items:center;gap:var(--hm-space-xs)}.as-picker__dot{flex:none;inline-size:.625rem;block-size:.625rem;border-radius:50%}\n"], dependencies: [{ 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: IconComponent, selector: "hm-icon", inputs: ["shape", "direction", "size", "solid", "badge", "status", "flip", "inverse", "label"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
242
|
+
}
|
|
243
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionCoverPickerComponent, decorators: [{
|
|
244
|
+
type: Component,
|
|
245
|
+
args: [{ selector: 'spine-suggestion-cover-picker', imports: [ButtonComponent, IconComponent], template: `
|
|
246
|
+
<div class="as-picker">
|
|
247
|
+
@for (group of groups(); track group.heading) {
|
|
248
|
+
<span class="as-picker__heading">{{ group.heading }}</span>
|
|
249
|
+
<div class="as-picker__swatches">
|
|
250
|
+
@for (c of group.items; track c.token) {
|
|
251
|
+
<button type="button" class="as-picker__swatch" [class]="'as-tok--' + c.token" [attr.aria-label]="c.name"
|
|
252
|
+
[attr.title]="c.name" [attr.aria-pressed]="value() === c.token" (click)="picked.emit(c.token)">
|
|
253
|
+
@if (value() === c.token) { <hm-icon shape="check" /> }
|
|
254
|
+
</button>
|
|
255
|
+
}
|
|
256
|
+
</div>
|
|
257
|
+
}
|
|
258
|
+
<hm-button size="small" emphasis="minimal" icon="times" [disabled]="!value()" (clicked)="picked.emit(null)">{{ text.coverRemove }}</hm-button>
|
|
259
|
+
</div>
|
|
260
|
+
`, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".as-tok--cat-1{--as-tok: var(--hm-data-categorical-1);background:var(--as-tok)}.as-tok--cat-2{--as-tok: var(--hm-data-categorical-2);background:var(--as-tok)}.as-tok--cat-3{--as-tok: var(--hm-data-categorical-3);background:var(--as-tok)}.as-tok--cat-4{--as-tok: var(--hm-data-categorical-4);background:var(--as-tok)}.as-tok--cat-5{--as-tok: var(--hm-data-categorical-5);background:var(--as-tok)}.as-tok--cat-6{--as-tok: var(--hm-data-categorical-6);background:var(--as-tok)}.as-tok--cat-7{--as-tok: var(--hm-data-categorical-7);background:var(--as-tok)}.as-tok--cat-8{--as-tok: var(--hm-data-categorical-8);background:var(--as-tok)}.as-tok--grad-1{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-2))}.as-tok--grad-2{--as-tok: var(--hm-data-categorical-3);background:linear-gradient(135deg,var(--hm-data-categorical-3),var(--hm-data-categorical-4))}.as-tok--grad-3{--as-tok: var(--hm-data-categorical-5);background:linear-gradient(135deg,var(--hm-data-categorical-5),var(--hm-data-categorical-6))}.as-tok--grad-4{--as-tok: var(--hm-data-categorical-7);background:linear-gradient(135deg,var(--hm-data-categorical-7),var(--hm-data-categorical-8))}.as-tok--grad-5{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-5))}.as-tok--grad-6{--as-tok: var(--hm-data-categorical-2);background:linear-gradient(135deg,var(--hm-data-categorical-2),var(--hm-data-categorical-6))}.as-label{display:inline-flex;align-items:center;gap:var(--hm-space-micro);max-inline-size:100%;padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:color-mix(in srgb,var(--as-tok, var(--hm-border)) 18%,var(--hm-surface-raised));color:var(--hm-text);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);line-height:1.6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.as-label:before{flex:none;inline-size:.5rem;block-size:.5rem;border-radius:50%;background:var(--as-tok, var(--hm-border));content:\"\"}@media(forced-colors:active){.as-label{border:var(--hm-border-hairline) solid CanvasText}}.as-picker{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-inline-size:15rem;max-inline-size:20rem}.as-picker__heading{color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold)}.as-picker__muted{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.as-picker__swatches{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--hm-space-xxxs)}.as-picker__swatch{display:inline-flex;align-items:center;justify-content:center;block-size:2rem;padding:0;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);color:var(--hm-surface-raised);cursor:pointer}.as-picker__swatch[aria-pressed=true]{outline:var(--hm-border-thin) solid var(--hm-text);outline-offset:1px}.as-picker__swatch:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:1px}.as-picker__swatch--small{block-size:1.5rem}.as-picker__list{display:flex;flex-direction:column;gap:var(--hm-space-micro);max-block-size:16rem;margin:0;padding:0;overflow-y:auto;list-style:none}.as-picker__person{display:flex;align-items:center;gap:var(--hm-space-xs);inline-size:100%;padding:var(--hm-space-micro) var(--hm-space-xxxs);border:0;border-radius:var(--hm-radius-control);background:none;color:var(--hm-text);font:inherit;text-align:start;cursor:pointer}.as-picker__person:hover{background:var(--hm-hover)}.as-picker__person:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color)}.as-picker__person-text{display:flex;flex-direction:column;min-inline-size:0}.as-picker__person-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.as-picker__label-row{display:flex;align-items:center;gap:var(--hm-space-xs)}.as-picker__dot{flex:none;inline-size:.625rem;block-size:.625rem;border-radius:50%}\n"] }]
|
|
261
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], picked: [{ type: i0.Output, args: ["picked"] }] } });
|
|
262
|
+
/**
|
|
263
|
+
* Pessoa do realm do app: busca (a mesma do auto-serviço, com a foto), as já escolhidas de fora. Lê na abertura (as
|
|
264
|
+
* primeiras) e a cada termo, com espera.
|
|
265
|
+
*/
|
|
266
|
+
class SuggestionPeoplePickerComponent {
|
|
267
|
+
constructor() {
|
|
268
|
+
this.text = inject(SPINE_MESSAGES).suggestions;
|
|
269
|
+
this.service = inject(AutoServiceSuggestionsService);
|
|
270
|
+
this.clientId = input.required(...(ngDevMode ? [{ debugName: "clientId" }] : /* istanbul ignore next */ []));
|
|
271
|
+
/** Quem já está no papel (não aparece de novo). */
|
|
272
|
+
this.exclude = input([], ...(ngDevMode ? [{ debugName: "exclude" }] : /* istanbul ignore next */ []));
|
|
273
|
+
this.picked = output();
|
|
274
|
+
this.found = signal([], ...(ngDevMode ? [{ debugName: "found" }] : /* istanbul ignore next */ []));
|
|
275
|
+
this.loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
276
|
+
this.options = computed(() => this.found().filter((p) => !this.exclude().includes(p.id)), ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
|
|
277
|
+
this.seq = 0;
|
|
278
|
+
effect(() => {
|
|
279
|
+
this.clientId();
|
|
280
|
+
untracked(() => void this.load(''));
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
async load(term) {
|
|
284
|
+
const mine = ++this.seq;
|
|
285
|
+
this.loading.set(true);
|
|
286
|
+
try {
|
|
287
|
+
const list = await this.service.searchPeople(this.clientId(), term);
|
|
288
|
+
if (mine === this.seq)
|
|
289
|
+
this.found.set(list);
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
if (mine === this.seq)
|
|
293
|
+
this.found.set([]);
|
|
294
|
+
}
|
|
295
|
+
finally {
|
|
296
|
+
if (mine === this.seq)
|
|
297
|
+
this.loading.set(false);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
nameOf(p) {
|
|
301
|
+
return p.name || p.username || p.email || '';
|
|
302
|
+
}
|
|
303
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionPeoplePickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
304
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SuggestionPeoplePickerComponent, isStandalone: true, selector: "spine-suggestion-people-picker", inputs: { clientId: { classPropertyName: "clientId", publicName: "clientId", isSignal: true, isRequired: true, transformFunction: null }, exclude: { classPropertyName: "exclude", publicName: "exclude", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { picked: "picked" }, ngImport: i0, template: `
|
|
305
|
+
<div class="as-picker">
|
|
306
|
+
<hm-search-field [label]="text.peopleSearch" labelHidden [placeholder]="text.peopleSearch" [landmark]="false"
|
|
307
|
+
[debounce]="250" [loading]="loading()" (search)="load($event)" />
|
|
308
|
+
<ul class="as-picker__list" [attr.aria-busy]="loading() ? 'true' : null">
|
|
309
|
+
@for (p of options(); track p.id) {
|
|
310
|
+
<li>
|
|
311
|
+
<button type="button" class="as-picker__person" (click)="picked.emit(p)">
|
|
312
|
+
<hm-account-avatar size="small" [name]="nameOf(p)" [imageUrl]="p.picture" />
|
|
313
|
+
<span class="as-picker__person-text">
|
|
314
|
+
<span class="as-picker__person-name">{{ nameOf(p) }}</span>
|
|
315
|
+
@if (p.username && p.name) { <span class="as-picker__muted">{{ p.username }}</span> }
|
|
316
|
+
</span>
|
|
317
|
+
</button>
|
|
318
|
+
</li>
|
|
319
|
+
} @empty {
|
|
320
|
+
@if (!loading()) { <li class="as-picker__muted">{{ text.peopleEmpty }}</li> }
|
|
321
|
+
}
|
|
322
|
+
</ul>
|
|
323
|
+
</div>
|
|
324
|
+
`, isInline: true, styles: [".as-tok--cat-1{--as-tok: var(--hm-data-categorical-1);background:var(--as-tok)}.as-tok--cat-2{--as-tok: var(--hm-data-categorical-2);background:var(--as-tok)}.as-tok--cat-3{--as-tok: var(--hm-data-categorical-3);background:var(--as-tok)}.as-tok--cat-4{--as-tok: var(--hm-data-categorical-4);background:var(--as-tok)}.as-tok--cat-5{--as-tok: var(--hm-data-categorical-5);background:var(--as-tok)}.as-tok--cat-6{--as-tok: var(--hm-data-categorical-6);background:var(--as-tok)}.as-tok--cat-7{--as-tok: var(--hm-data-categorical-7);background:var(--as-tok)}.as-tok--cat-8{--as-tok: var(--hm-data-categorical-8);background:var(--as-tok)}.as-tok--grad-1{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-2))}.as-tok--grad-2{--as-tok: var(--hm-data-categorical-3);background:linear-gradient(135deg,var(--hm-data-categorical-3),var(--hm-data-categorical-4))}.as-tok--grad-3{--as-tok: var(--hm-data-categorical-5);background:linear-gradient(135deg,var(--hm-data-categorical-5),var(--hm-data-categorical-6))}.as-tok--grad-4{--as-tok: var(--hm-data-categorical-7);background:linear-gradient(135deg,var(--hm-data-categorical-7),var(--hm-data-categorical-8))}.as-tok--grad-5{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-5))}.as-tok--grad-6{--as-tok: var(--hm-data-categorical-2);background:linear-gradient(135deg,var(--hm-data-categorical-2),var(--hm-data-categorical-6))}.as-label{display:inline-flex;align-items:center;gap:var(--hm-space-micro);max-inline-size:100%;padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:color-mix(in srgb,var(--as-tok, var(--hm-border)) 18%,var(--hm-surface-raised));color:var(--hm-text);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);line-height:1.6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.as-label:before{flex:none;inline-size:.5rem;block-size:.5rem;border-radius:50%;background:var(--as-tok, var(--hm-border));content:\"\"}@media(forced-colors:active){.as-label{border:var(--hm-border-hairline) solid CanvasText}}.as-picker{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-inline-size:15rem;max-inline-size:20rem}.as-picker__heading{color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold)}.as-picker__muted{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.as-picker__swatches{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--hm-space-xxxs)}.as-picker__swatch{display:inline-flex;align-items:center;justify-content:center;block-size:2rem;padding:0;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);color:var(--hm-surface-raised);cursor:pointer}.as-picker__swatch[aria-pressed=true]{outline:var(--hm-border-thin) solid var(--hm-text);outline-offset:1px}.as-picker__swatch:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:1px}.as-picker__swatch--small{block-size:1.5rem}.as-picker__list{display:flex;flex-direction:column;gap:var(--hm-space-micro);max-block-size:16rem;margin:0;padding:0;overflow-y:auto;list-style:none}.as-picker__person{display:flex;align-items:center;gap:var(--hm-space-xs);inline-size:100%;padding:var(--hm-space-micro) var(--hm-space-xxxs);border:0;border-radius:var(--hm-radius-control);background:none;color:var(--hm-text);font:inherit;text-align:start;cursor:pointer}.as-picker__person:hover{background:var(--hm-hover)}.as-picker__person:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color)}.as-picker__person-text{display:flex;flex-direction:column;min-inline-size:0}.as-picker__person-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.as-picker__label-row{display:flex;align-items:center;gap:var(--hm-space-xs)}.as-picker__dot{flex:none;inline-size:.625rem;block-size:.625rem;border-radius:50%}\n"], dependencies: [{ kind: "component", type: AccountAvatarComponent, selector: "hm-account-avatar", inputs: ["name", "imageUrl", "size", "loading"] }, { kind: "component", type: SearchFieldComponent, selector: "hm-search-field", inputs: ["value", "label", "labelHidden", "placeholder", "hint", "size", "debounce", "landmark", "loading"], outputs: ["valueChange", "search", "cleared"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
325
|
+
}
|
|
326
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionPeoplePickerComponent, decorators: [{
|
|
327
|
+
type: Component,
|
|
328
|
+
args: [{ selector: 'spine-suggestion-people-picker', imports: [AccountAvatarComponent, SearchFieldComponent], template: `
|
|
329
|
+
<div class="as-picker">
|
|
330
|
+
<hm-search-field [label]="text.peopleSearch" labelHidden [placeholder]="text.peopleSearch" [landmark]="false"
|
|
331
|
+
[debounce]="250" [loading]="loading()" (search)="load($event)" />
|
|
332
|
+
<ul class="as-picker__list" [attr.aria-busy]="loading() ? 'true' : null">
|
|
333
|
+
@for (p of options(); track p.id) {
|
|
334
|
+
<li>
|
|
335
|
+
<button type="button" class="as-picker__person" (click)="picked.emit(p)">
|
|
336
|
+
<hm-account-avatar size="small" [name]="nameOf(p)" [imageUrl]="p.picture" />
|
|
337
|
+
<span class="as-picker__person-text">
|
|
338
|
+
<span class="as-picker__person-name">{{ nameOf(p) }}</span>
|
|
339
|
+
@if (p.username && p.name) { <span class="as-picker__muted">{{ p.username }}</span> }
|
|
340
|
+
</span>
|
|
341
|
+
</button>
|
|
342
|
+
</li>
|
|
343
|
+
} @empty {
|
|
344
|
+
@if (!loading()) { <li class="as-picker__muted">{{ text.peopleEmpty }}</li> }
|
|
345
|
+
}
|
|
346
|
+
</ul>
|
|
347
|
+
</div>
|
|
348
|
+
`, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".as-tok--cat-1{--as-tok: var(--hm-data-categorical-1);background:var(--as-tok)}.as-tok--cat-2{--as-tok: var(--hm-data-categorical-2);background:var(--as-tok)}.as-tok--cat-3{--as-tok: var(--hm-data-categorical-3);background:var(--as-tok)}.as-tok--cat-4{--as-tok: var(--hm-data-categorical-4);background:var(--as-tok)}.as-tok--cat-5{--as-tok: var(--hm-data-categorical-5);background:var(--as-tok)}.as-tok--cat-6{--as-tok: var(--hm-data-categorical-6);background:var(--as-tok)}.as-tok--cat-7{--as-tok: var(--hm-data-categorical-7);background:var(--as-tok)}.as-tok--cat-8{--as-tok: var(--hm-data-categorical-8);background:var(--as-tok)}.as-tok--grad-1{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-2))}.as-tok--grad-2{--as-tok: var(--hm-data-categorical-3);background:linear-gradient(135deg,var(--hm-data-categorical-3),var(--hm-data-categorical-4))}.as-tok--grad-3{--as-tok: var(--hm-data-categorical-5);background:linear-gradient(135deg,var(--hm-data-categorical-5),var(--hm-data-categorical-6))}.as-tok--grad-4{--as-tok: var(--hm-data-categorical-7);background:linear-gradient(135deg,var(--hm-data-categorical-7),var(--hm-data-categorical-8))}.as-tok--grad-5{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-5))}.as-tok--grad-6{--as-tok: var(--hm-data-categorical-2);background:linear-gradient(135deg,var(--hm-data-categorical-2),var(--hm-data-categorical-6))}.as-label{display:inline-flex;align-items:center;gap:var(--hm-space-micro);max-inline-size:100%;padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:color-mix(in srgb,var(--as-tok, var(--hm-border)) 18%,var(--hm-surface-raised));color:var(--hm-text);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);line-height:1.6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.as-label:before{flex:none;inline-size:.5rem;block-size:.5rem;border-radius:50%;background:var(--as-tok, var(--hm-border));content:\"\"}@media(forced-colors:active){.as-label{border:var(--hm-border-hairline) solid CanvasText}}.as-picker{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-inline-size:15rem;max-inline-size:20rem}.as-picker__heading{color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold)}.as-picker__muted{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.as-picker__swatches{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--hm-space-xxxs)}.as-picker__swatch{display:inline-flex;align-items:center;justify-content:center;block-size:2rem;padding:0;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);color:var(--hm-surface-raised);cursor:pointer}.as-picker__swatch[aria-pressed=true]{outline:var(--hm-border-thin) solid var(--hm-text);outline-offset:1px}.as-picker__swatch:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:1px}.as-picker__swatch--small{block-size:1.5rem}.as-picker__list{display:flex;flex-direction:column;gap:var(--hm-space-micro);max-block-size:16rem;margin:0;padding:0;overflow-y:auto;list-style:none}.as-picker__person{display:flex;align-items:center;gap:var(--hm-space-xs);inline-size:100%;padding:var(--hm-space-micro) var(--hm-space-xxxs);border:0;border-radius:var(--hm-radius-control);background:none;color:var(--hm-text);font:inherit;text-align:start;cursor:pointer}.as-picker__person:hover{background:var(--hm-hover)}.as-picker__person:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color)}.as-picker__person-text{display:flex;flex-direction:column;min-inline-size:0}.as-picker__person-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.as-picker__label-row{display:flex;align-items:center;gap:var(--hm-space-xs)}.as-picker__dot{flex:none;inline-size:.625rem;block-size:.625rem;border-radius:50%}\n"] }]
|
|
349
|
+
}], ctorParameters: () => [], propDecorators: { clientId: [{ type: i0.Input, args: [{ isSignal: true, alias: "clientId", required: true }] }], exclude: [{ type: i0.Input, args: [{ isSignal: true, alias: "exclude", required: false }] }], picked: [{ type: i0.Output, args: ["picked"] }] } });
|
|
350
|
+
/**
|
|
351
|
+
* Rótulos do catálogo do app: marcar/desmarcar (caixa por rótulo, o ponto na cor dele) e, quando o termo digitado não
|
|
352
|
+
* existe, "Criar o rótulo" com a cor escolhida.
|
|
353
|
+
*/
|
|
354
|
+
class SuggestionLabelPickerComponent {
|
|
355
|
+
constructor() {
|
|
356
|
+
this.text = inject(SPINE_MESSAGES).suggestions;
|
|
357
|
+
this.labels = input([], ...(ngDevMode ? [{ debugName: "labels" }] : /* istanbul ignore next */ []));
|
|
358
|
+
this.selected = input([], ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
|
|
359
|
+
this.toggled = output();
|
|
360
|
+
this.created = output();
|
|
361
|
+
this.colors = TASK_LABEL_COLORS;
|
|
362
|
+
this.term = signal('', ...(ngDevMode ? [{ debugName: "term" }] : /* istanbul ignore next */ []));
|
|
363
|
+
this.color = signal('cat-1', ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
|
|
364
|
+
this.shown = computed(() => {
|
|
365
|
+
const t = this.term().trim().toLocaleLowerCase();
|
|
366
|
+
return this.labels().filter((l) => !t || l.name.toLocaleLowerCase().includes(t));
|
|
367
|
+
}, ...(ngDevMode ? [{ debugName: "shown" }] : /* istanbul ignore next */ []));
|
|
368
|
+
/** Termo que ainda não é rótulo: dá para criar. */
|
|
369
|
+
this.creatable = computed(() => {
|
|
370
|
+
const t = this.term().trim().toLocaleLowerCase();
|
|
371
|
+
return !!t && t.length <= 40 && !this.labels().some((l) => l.name.toLocaleLowerCase() === t);
|
|
372
|
+
}, ...(ngDevMode ? [{ debugName: "creatable" }] : /* istanbul ignore next */ []));
|
|
373
|
+
this.createText = computed(() => formatMessage(this.text.createLabel, { name: this.term().trim() }), ...(ngDevMode ? [{ debugName: "createText" }] : /* istanbul ignore next */ []));
|
|
374
|
+
}
|
|
375
|
+
colorName(i) {
|
|
376
|
+
return formatMessage(this.text.coverName, { n: i + 1 });
|
|
377
|
+
}
|
|
378
|
+
create() {
|
|
379
|
+
const name = this.term().trim();
|
|
380
|
+
if (!name)
|
|
381
|
+
return;
|
|
382
|
+
this.created.emit({ name, color: this.color() });
|
|
383
|
+
this.term.set('');
|
|
384
|
+
}
|
|
385
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionLabelPickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
386
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SuggestionLabelPickerComponent, isStandalone: true, selector: "spine-suggestion-label-picker", inputs: { labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { toggled: "toggled", created: "created" }, ngImport: i0, template: `
|
|
387
|
+
<div class="as-picker">
|
|
388
|
+
<hm-search-field [label]="text.labelFilter" labelHidden [placeholder]="text.labelFilter" [landmark]="false"
|
|
389
|
+
[value]="term()" (valueChange)="term.set($event)" />
|
|
390
|
+
<ul class="as-picker__list">
|
|
391
|
+
@for (l of shown(); track l.id) {
|
|
392
|
+
<li class="as-picker__label-row">
|
|
393
|
+
<span class="as-picker__dot" [class]="'as-tok--' + l.color" aria-hidden="true"></span>
|
|
394
|
+
<hm-checkbox [label]="l.name" [checked]="selected().includes(l.id)" (checkedChange)="toggled.emit({ label: l, linked: $event })" />
|
|
395
|
+
</li>
|
|
396
|
+
} @empty {
|
|
397
|
+
@if (!creatable()) { <li class="as-picker__muted">{{ text.noLabels }}</li> }
|
|
398
|
+
}
|
|
399
|
+
</ul>
|
|
400
|
+
@if (creatable()) {
|
|
401
|
+
<span class="as-picker__heading">{{ text.labelColor }}</span>
|
|
402
|
+
<div class="as-picker__swatches">
|
|
403
|
+
@for (c of colors; track c; let i = $index) {
|
|
404
|
+
<button type="button" class="as-picker__swatch as-picker__swatch--small" [class]="'as-tok--' + c"
|
|
405
|
+
[attr.aria-label]="colorName(i)" [attr.aria-pressed]="color() === c" (click)="color.set(c)"></button>
|
|
406
|
+
}
|
|
407
|
+
</div>
|
|
408
|
+
<hm-button size="small" emphasis="medium" icon="plus" (clicked)="create()">{{ createText() }}</hm-button>
|
|
409
|
+
}
|
|
410
|
+
</div>
|
|
411
|
+
`, isInline: true, styles: [".as-tok--cat-1{--as-tok: var(--hm-data-categorical-1);background:var(--as-tok)}.as-tok--cat-2{--as-tok: var(--hm-data-categorical-2);background:var(--as-tok)}.as-tok--cat-3{--as-tok: var(--hm-data-categorical-3);background:var(--as-tok)}.as-tok--cat-4{--as-tok: var(--hm-data-categorical-4);background:var(--as-tok)}.as-tok--cat-5{--as-tok: var(--hm-data-categorical-5);background:var(--as-tok)}.as-tok--cat-6{--as-tok: var(--hm-data-categorical-6);background:var(--as-tok)}.as-tok--cat-7{--as-tok: var(--hm-data-categorical-7);background:var(--as-tok)}.as-tok--cat-8{--as-tok: var(--hm-data-categorical-8);background:var(--as-tok)}.as-tok--grad-1{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-2))}.as-tok--grad-2{--as-tok: var(--hm-data-categorical-3);background:linear-gradient(135deg,var(--hm-data-categorical-3),var(--hm-data-categorical-4))}.as-tok--grad-3{--as-tok: var(--hm-data-categorical-5);background:linear-gradient(135deg,var(--hm-data-categorical-5),var(--hm-data-categorical-6))}.as-tok--grad-4{--as-tok: var(--hm-data-categorical-7);background:linear-gradient(135deg,var(--hm-data-categorical-7),var(--hm-data-categorical-8))}.as-tok--grad-5{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-5))}.as-tok--grad-6{--as-tok: var(--hm-data-categorical-2);background:linear-gradient(135deg,var(--hm-data-categorical-2),var(--hm-data-categorical-6))}.as-label{display:inline-flex;align-items:center;gap:var(--hm-space-micro);max-inline-size:100%;padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:color-mix(in srgb,var(--as-tok, var(--hm-border)) 18%,var(--hm-surface-raised));color:var(--hm-text);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);line-height:1.6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.as-label:before{flex:none;inline-size:.5rem;block-size:.5rem;border-radius:50%;background:var(--as-tok, var(--hm-border));content:\"\"}@media(forced-colors:active){.as-label{border:var(--hm-border-hairline) solid CanvasText}}.as-picker{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-inline-size:15rem;max-inline-size:20rem}.as-picker__heading{color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold)}.as-picker__muted{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.as-picker__swatches{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--hm-space-xxxs)}.as-picker__swatch{display:inline-flex;align-items:center;justify-content:center;block-size:2rem;padding:0;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);color:var(--hm-surface-raised);cursor:pointer}.as-picker__swatch[aria-pressed=true]{outline:var(--hm-border-thin) solid var(--hm-text);outline-offset:1px}.as-picker__swatch:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:1px}.as-picker__swatch--small{block-size:1.5rem}.as-picker__list{display:flex;flex-direction:column;gap:var(--hm-space-micro);max-block-size:16rem;margin:0;padding:0;overflow-y:auto;list-style:none}.as-picker__person{display:flex;align-items:center;gap:var(--hm-space-xs);inline-size:100%;padding:var(--hm-space-micro) var(--hm-space-xxxs);border:0;border-radius:var(--hm-radius-control);background:none;color:var(--hm-text);font:inherit;text-align:start;cursor:pointer}.as-picker__person:hover{background:var(--hm-hover)}.as-picker__person:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color)}.as-picker__person-text{display:flex;flex-direction:column;min-inline-size:0}.as-picker__person-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.as-picker__label-row{display:flex;align-items:center;gap:var(--hm-space-xs)}.as-picker__dot{flex:none;inline-size:.625rem;block-size:.625rem;border-radius:50%}\n"], dependencies: [{ 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: CheckboxComponent, selector: "hm-checkbox", inputs: ["label", "labelHidden", "checked", "indeterminate"], outputs: ["checkedChange"] }, { kind: "component", type: SearchFieldComponent, selector: "hm-search-field", inputs: ["value", "label", "labelHidden", "placeholder", "hint", "size", "debounce", "landmark", "loading"], outputs: ["valueChange", "search", "cleared"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
412
|
+
}
|
|
413
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionLabelPickerComponent, decorators: [{
|
|
414
|
+
type: Component,
|
|
415
|
+
args: [{ selector: 'spine-suggestion-label-picker', imports: [ButtonComponent, CheckboxComponent, SearchFieldComponent], template: `
|
|
416
|
+
<div class="as-picker">
|
|
417
|
+
<hm-search-field [label]="text.labelFilter" labelHidden [placeholder]="text.labelFilter" [landmark]="false"
|
|
418
|
+
[value]="term()" (valueChange)="term.set($event)" />
|
|
419
|
+
<ul class="as-picker__list">
|
|
420
|
+
@for (l of shown(); track l.id) {
|
|
421
|
+
<li class="as-picker__label-row">
|
|
422
|
+
<span class="as-picker__dot" [class]="'as-tok--' + l.color" aria-hidden="true"></span>
|
|
423
|
+
<hm-checkbox [label]="l.name" [checked]="selected().includes(l.id)" (checkedChange)="toggled.emit({ label: l, linked: $event })" />
|
|
424
|
+
</li>
|
|
425
|
+
} @empty {
|
|
426
|
+
@if (!creatable()) { <li class="as-picker__muted">{{ text.noLabels }}</li> }
|
|
427
|
+
}
|
|
428
|
+
</ul>
|
|
429
|
+
@if (creatable()) {
|
|
430
|
+
<span class="as-picker__heading">{{ text.labelColor }}</span>
|
|
431
|
+
<div class="as-picker__swatches">
|
|
432
|
+
@for (c of colors; track c; let i = $index) {
|
|
433
|
+
<button type="button" class="as-picker__swatch as-picker__swatch--small" [class]="'as-tok--' + c"
|
|
434
|
+
[attr.aria-label]="colorName(i)" [attr.aria-pressed]="color() === c" (click)="color.set(c)"></button>
|
|
435
|
+
}
|
|
436
|
+
</div>
|
|
437
|
+
<hm-button size="small" emphasis="medium" icon="plus" (clicked)="create()">{{ createText() }}</hm-button>
|
|
438
|
+
}
|
|
439
|
+
</div>
|
|
440
|
+
`, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".as-tok--cat-1{--as-tok: var(--hm-data-categorical-1);background:var(--as-tok)}.as-tok--cat-2{--as-tok: var(--hm-data-categorical-2);background:var(--as-tok)}.as-tok--cat-3{--as-tok: var(--hm-data-categorical-3);background:var(--as-tok)}.as-tok--cat-4{--as-tok: var(--hm-data-categorical-4);background:var(--as-tok)}.as-tok--cat-5{--as-tok: var(--hm-data-categorical-5);background:var(--as-tok)}.as-tok--cat-6{--as-tok: var(--hm-data-categorical-6);background:var(--as-tok)}.as-tok--cat-7{--as-tok: var(--hm-data-categorical-7);background:var(--as-tok)}.as-tok--cat-8{--as-tok: var(--hm-data-categorical-8);background:var(--as-tok)}.as-tok--grad-1{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-2))}.as-tok--grad-2{--as-tok: var(--hm-data-categorical-3);background:linear-gradient(135deg,var(--hm-data-categorical-3),var(--hm-data-categorical-4))}.as-tok--grad-3{--as-tok: var(--hm-data-categorical-5);background:linear-gradient(135deg,var(--hm-data-categorical-5),var(--hm-data-categorical-6))}.as-tok--grad-4{--as-tok: var(--hm-data-categorical-7);background:linear-gradient(135deg,var(--hm-data-categorical-7),var(--hm-data-categorical-8))}.as-tok--grad-5{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-5))}.as-tok--grad-6{--as-tok: var(--hm-data-categorical-2);background:linear-gradient(135deg,var(--hm-data-categorical-2),var(--hm-data-categorical-6))}.as-label{display:inline-flex;align-items:center;gap:var(--hm-space-micro);max-inline-size:100%;padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:color-mix(in srgb,var(--as-tok, var(--hm-border)) 18%,var(--hm-surface-raised));color:var(--hm-text);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);line-height:1.6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.as-label:before{flex:none;inline-size:.5rem;block-size:.5rem;border-radius:50%;background:var(--as-tok, var(--hm-border));content:\"\"}@media(forced-colors:active){.as-label{border:var(--hm-border-hairline) solid CanvasText}}.as-picker{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-inline-size:15rem;max-inline-size:20rem}.as-picker__heading{color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold)}.as-picker__muted{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.as-picker__swatches{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--hm-space-xxxs)}.as-picker__swatch{display:inline-flex;align-items:center;justify-content:center;block-size:2rem;padding:0;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);color:var(--hm-surface-raised);cursor:pointer}.as-picker__swatch[aria-pressed=true]{outline:var(--hm-border-thin) solid var(--hm-text);outline-offset:1px}.as-picker__swatch:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:1px}.as-picker__swatch--small{block-size:1.5rem}.as-picker__list{display:flex;flex-direction:column;gap:var(--hm-space-micro);max-block-size:16rem;margin:0;padding:0;overflow-y:auto;list-style:none}.as-picker__person{display:flex;align-items:center;gap:var(--hm-space-xs);inline-size:100%;padding:var(--hm-space-micro) var(--hm-space-xxxs);border:0;border-radius:var(--hm-radius-control);background:none;color:var(--hm-text);font:inherit;text-align:start;cursor:pointer}.as-picker__person:hover{background:var(--hm-hover)}.as-picker__person:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color)}.as-picker__person-text{display:flex;flex-direction:column;min-inline-size:0}.as-picker__person-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.as-picker__label-row{display:flex;align-items:center;gap:var(--hm-space-xs)}.as-picker__dot{flex:none;inline-size:.625rem;block-size:.625rem;border-radius:50%}\n"] }]
|
|
441
|
+
}], propDecorators: { labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], toggled: [{ type: i0.Output, args: ["toggled"] }], created: [{ type: i0.Output, args: ["created"] }] } });
|
|
442
|
+
|
|
443
|
+
const EVENTS_PAGE = 20;
|
|
444
|
+
/** Copia para a área de transferência; false se o navegador recusar. */
|
|
445
|
+
async function copyText(value) {
|
|
446
|
+
try {
|
|
447
|
+
await navigator.clipboard.writeText(value);
|
|
448
|
+
return true;
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* A tarefa aberta (modal grande, duas colunas, como os quadros de tarefas conhecidos), montada pelo quadro.
|
|
456
|
+
*
|
|
457
|
+
* - **Esquerda**: título editável no lugar (Enter salva, Esc desiste), descrição em `hm-rich-text` (ler; "Editar"
|
|
458
|
+
* abre o editor e "Salvar" grava o HTML, que o servidor saneia), checklist (marcar, renomear, subir/descer, remover,
|
|
459
|
+
* "Mostrar no cartão") e as abas Comentários e Atividades (do mais novo, "Carregar mais"). Comentário é texto simples
|
|
460
|
+
* e pode ser interno (só a equipe vê; quem pediu não lê em "Minhas sugestões").
|
|
461
|
+
* - **Direita**: status, tipo, responsáveis ("Atribuir a mim") e envolvidos (busca de pessoas do realm, com foto),
|
|
462
|
+
* relator, prioridade, início e conclusão, rótulos (catálogo do app; cria na hora), capa, tela de origem ("Abrir a
|
|
463
|
+
* tela" quando o app administrado é o desta sessão), apoios, criada/atualizada e "Última alteração há X por Fulano";
|
|
464
|
+
* a chave com copiar; arquivar/restaurar e excluir (com confirmação).
|
|
465
|
+
* - **Agrupar**: "Relatores" (o autor e quem veio das agrupadas, com foto) no lugar do relator; "Agrupadas aqui" (chave,
|
|
466
|
+
* título e autor de cada origem, com link e "Desagrupar"); "Agrupar em…" pede o seletor ao quadro (`mergeRequested`).
|
|
467
|
+
* Na agrupada, o aviso "Agrupada em SUG-12" no topo, com "Abrir SUG-12" (`openTask`) e "Desagrupar" (`unmerged`).
|
|
468
|
+
* - **Nova tarefa** (`task` nulo, `draft`): título, tipo, coluna, prioridade, datas, descrição, rótulos e "Atribuir a
|
|
469
|
+
* mim"; criar troca para a tarefa completa.
|
|
470
|
+
*
|
|
471
|
+
* Toda mudança vai direto ao servidor, que devolve a tarefa inteira: ela vira a nova verdade aqui (`taskChange`) e no
|
|
472
|
+
* quadro (`changed`).
|
|
473
|
+
*/
|
|
474
|
+
class SuggestionTaskDetailComponent {
|
|
475
|
+
constructor() {
|
|
476
|
+
this.messages = inject(SPINE_MESSAGES);
|
|
477
|
+
this.text = this.messages.suggestions;
|
|
478
|
+
this.service = inject(AutoServiceSuggestionsService);
|
|
479
|
+
this.toast = inject(HermesToastService);
|
|
480
|
+
this.dialogs = inject(HermesDialogService);
|
|
481
|
+
this.preferences = inject(SpinePreferencesService);
|
|
482
|
+
this.userStorage = inject(SpineUserStorage, { optional: true });
|
|
483
|
+
this.router = inject(Router, { optional: true });
|
|
484
|
+
this.location = inject(Location, { optional: true });
|
|
485
|
+
this.clientId = input.required(...(ngDevMode ? [{ debugName: "clientId" }] : /* istanbul ignore next */ []));
|
|
486
|
+
/** A tarefa aberta (leve, do quadro: a completa chega do servidor). null + `draft` = nova tarefa. */
|
|
487
|
+
this.task = input(null, ...(ngDevMode ? [{ debugName: "task" }] : /* istanbul ignore next */ []));
|
|
488
|
+
this.draft = input(false, ...(ngDevMode ? [{ debugName: "draft" }] : /* istanbul ignore next */ []));
|
|
489
|
+
/** Coluna da nova tarefa. */
|
|
490
|
+
this.draftStatus = input('em_analise', ...(ngDevMode ? [{ debugName: "draftStatus" }] : /* istanbul ignore next */ []));
|
|
491
|
+
this.labels = input([], ...(ngDevMode ? [{ debugName: "labels" }] : /* istanbul ignore next */ []));
|
|
492
|
+
/** A tela da sugestão abre daqui (o app administrado é o desta sessão). */
|
|
493
|
+
this.canOpenScreen = input(false, ...(ngDevMode ? [{ debugName: "canOpenScreen" }] : /* istanbul ignore next */ []));
|
|
494
|
+
/** A tarefa mudou (o servidor devolveu a nova versão). */
|
|
495
|
+
this.changed = output();
|
|
496
|
+
this.created = output();
|
|
497
|
+
this.removed = output();
|
|
498
|
+
this.closed = output();
|
|
499
|
+
/** Um rótulo novo entrou no catálogo. */
|
|
500
|
+
this.labelCreated = output();
|
|
501
|
+
/** "Agrupar em…": o quadro fecha este modal e abre o seletor. */
|
|
502
|
+
this.mergeRequested = output();
|
|
503
|
+
/** Abrir outra tarefa pelo id (o destino de uma agrupada, uma das agrupadas). */
|
|
504
|
+
this.openTask = output();
|
|
505
|
+
/** Desagrupou (daqui): o quadro relê, a origem volta para ele. */
|
|
506
|
+
this.unmerged = output();
|
|
507
|
+
this.statuses = SPINE_SUGGESTION_STATUSES;
|
|
508
|
+
this.statusColors = SPINE_SUGGESTION_STATUS_COLORS;
|
|
509
|
+
this.kinds = SPINE_SUGGESTION_KINDS;
|
|
510
|
+
this.kindColors = SPINE_SUGGESTION_KIND_COLORS;
|
|
511
|
+
this.priorities = TASK_PRIORITIES;
|
|
512
|
+
this.priorityIcons = TASK_PRIORITY_ICONS;
|
|
513
|
+
this.limits = SPINE_SUGGESTION_LIMITS;
|
|
514
|
+
this.today = todayIso(this.preferences.effectiveTimeZone());
|
|
515
|
+
this.current = signal(null, ...(ngDevMode ? [{ debugName: "current" }] : /* istanbul ignore next */ []));
|
|
516
|
+
this.loadError = signal('', ...(ngDevMode ? [{ debugName: "loadError" }] : /* istanbul ignore next */ []));
|
|
517
|
+
this.busy = signal(false, ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
|
|
518
|
+
this.open = computed(() => !!this.current() || this.draft(), ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
|
|
519
|
+
this.heading = computed(() => {
|
|
520
|
+
const t = this.current();
|
|
521
|
+
return t ? `${t.key} · ${t.title}` : this.text.newTaskHeading;
|
|
522
|
+
}, ...(ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []));
|
|
523
|
+
// título e descrição
|
|
524
|
+
this.editingTitle = signal(false, ...(ngDevMode ? [{ debugName: "editingTitle" }] : /* istanbul ignore next */ []));
|
|
525
|
+
this.titleDraft = signal('', ...(ngDevMode ? [{ debugName: "titleDraft" }] : /* istanbul ignore next */ []));
|
|
526
|
+
this.editingDescription = signal(false, ...(ngDevMode ? [{ debugName: "editingDescription" }] : /* istanbul ignore next */ []));
|
|
527
|
+
this.descriptionDraft = signal('', ...(ngDevMode ? [{ debugName: "descriptionDraft" }] : /* istanbul ignore next */ []));
|
|
528
|
+
// checklist
|
|
529
|
+
this.newItem = signal('', ...(ngDevMode ? [{ debugName: "newItem" }] : /* istanbul ignore next */ []));
|
|
530
|
+
this.renaming = signal(null, ...(ngDevMode ? [{ debugName: "renaming" }] : /* istanbul ignore next */ []));
|
|
531
|
+
this.renameDraft = signal('', ...(ngDevMode ? [{ debugName: "renameDraft" }] : /* istanbul ignore next */ []));
|
|
532
|
+
// comentários e atividades
|
|
533
|
+
this.tab = signal('comments', ...(ngDevMode ? [{ debugName: "tab" }] : /* istanbul ignore next */ []));
|
|
534
|
+
this.comments = signal([], ...(ngDevMode ? [{ debugName: "comments" }] : /* istanbul ignore next */ []));
|
|
535
|
+
this.commentsTotal = signal(0, ...(ngDevMode ? [{ debugName: "commentsTotal" }] : /* istanbul ignore next */ []));
|
|
536
|
+
this.activity = signal([], ...(ngDevMode ? [{ debugName: "activity" }] : /* istanbul ignore next */ []));
|
|
537
|
+
this.activityTotal = signal(0, ...(ngDevMode ? [{ debugName: "activityTotal" }] : /* istanbul ignore next */ []));
|
|
538
|
+
this.eventsLoading = signal(false, ...(ngDevMode ? [{ debugName: "eventsLoading" }] : /* istanbul ignore next */ []));
|
|
539
|
+
this.comment = signal('', ...(ngDevMode ? [{ debugName: "comment" }] : /* istanbul ignore next */ []));
|
|
540
|
+
this.commentInternal = signal(false, ...(ngDevMode ? [{ debugName: "commentInternal" }] : /* istanbul ignore next */ []));
|
|
541
|
+
this.commentError = signal('', ...(ngDevMode ? [{ debugName: "commentError" }] : /* istanbul ignore next */ []));
|
|
542
|
+
this.commenting = signal(false, ...(ngDevMode ? [{ debugName: "commenting" }] : /* istanbul ignore next */ []));
|
|
543
|
+
// nova tarefa
|
|
544
|
+
this.draftTitle = signal('', ...(ngDevMode ? [{ debugName: "draftTitle" }] : /* istanbul ignore next */ []));
|
|
545
|
+
this.draftTitleError = signal('', ...(ngDevMode ? [{ debugName: "draftTitleError" }] : /* istanbul ignore next */ []));
|
|
546
|
+
this.draftKind = signal('melhoria', ...(ngDevMode ? [{ debugName: "draftKind" }] : /* istanbul ignore next */ []));
|
|
547
|
+
this.draftColumn = signal('em_analise', ...(ngDevMode ? [{ debugName: "draftColumn" }] : /* istanbul ignore next */ []));
|
|
548
|
+
this.draftPriority = signal('media', ...(ngDevMode ? [{ debugName: "draftPriority" }] : /* istanbul ignore next */ []));
|
|
549
|
+
this.draftStart = signal(null, ...(ngDevMode ? [{ debugName: "draftStart" }] : /* istanbul ignore next */ []));
|
|
550
|
+
this.draftDue = signal(null, ...(ngDevMode ? [{ debugName: "draftDue" }] : /* istanbul ignore next */ []));
|
|
551
|
+
this.draftDescription = signal('', ...(ngDevMode ? [{ debugName: "draftDescription" }] : /* istanbul ignore next */ []));
|
|
552
|
+
this.draftLabels = signal([], ...(ngDevMode ? [{ debugName: "draftLabels" }] : /* istanbul ignore next */ []));
|
|
553
|
+
this.draftAssignMe = signal(true, ...(ngDevMode ? [{ debugName: "draftAssignMe" }] : /* istanbul ignore next */ []));
|
|
554
|
+
this.selectedLabels = computed(() => this.current()?.labels.map((l) => l.id) ?? this.draftLabels(), ...(ngDevMode ? [{ debugName: "selectedLabels" }] : /* istanbul ignore next */ []));
|
|
555
|
+
this.assigneeIds = computed(() => this.current()?.assignees.map((p) => p.user_id) ?? [], ...(ngDevMode ? [{ debugName: "assigneeIds" }] : /* istanbul ignore next */ []));
|
|
556
|
+
this.watcherIds = computed(() => this.current()?.watchers.map((p) => p.user_id) ?? [], ...(ngDevMode ? [{ debugName: "watcherIds" }] : /* istanbul ignore next */ []));
|
|
557
|
+
this.checklistItems = computed(() => [...(this.current()?.checklist.items ?? [])].sort((a, b) => a.position - b.position), ...(ngDevMode ? [{ debugName: "checklistItems" }] : /* istanbul ignore next */ []));
|
|
558
|
+
this.dueSituation = computed(() => {
|
|
559
|
+
const t = this.current();
|
|
560
|
+
return t ? dueState(t, this.today) : null;
|
|
561
|
+
}, ...(ngDevMode ? [{ debugName: "dueSituation" }] : /* istanbul ignore next */ []));
|
|
562
|
+
this.isMine = computed(() => {
|
|
563
|
+
const me = this.me();
|
|
564
|
+
return !!me && this.assigneeIds().includes(me);
|
|
565
|
+
}, ...(ngDevMode ? [{ debugName: "isMine" }] : /* istanbul ignore next */ []));
|
|
566
|
+
effect(() => {
|
|
567
|
+
const task = this.task();
|
|
568
|
+
const draft = this.draft();
|
|
569
|
+
untracked(() => {
|
|
570
|
+
this.reset();
|
|
571
|
+
if (task)
|
|
572
|
+
void this.load(task);
|
|
573
|
+
else if (draft)
|
|
574
|
+
this.startDraft();
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
me() {
|
|
579
|
+
return this.userStorage?.userId() ?? '';
|
|
580
|
+
}
|
|
581
|
+
reset() {
|
|
582
|
+
this.current.set(null);
|
|
583
|
+
this.loadError.set('');
|
|
584
|
+
this.editingTitle.set(false);
|
|
585
|
+
this.editingDescription.set(false);
|
|
586
|
+
this.renaming.set(null);
|
|
587
|
+
this.newItem.set('');
|
|
588
|
+
this.comments.set([]);
|
|
589
|
+
this.activity.set([]);
|
|
590
|
+
this.commentsTotal.set(0);
|
|
591
|
+
this.activityTotal.set(0);
|
|
592
|
+
this.comment.set('');
|
|
593
|
+
this.commentInternal.set(false);
|
|
594
|
+
this.commentError.set('');
|
|
595
|
+
this.tab.set('comments');
|
|
596
|
+
}
|
|
597
|
+
startDraft() {
|
|
598
|
+
this.draftTitle.set('');
|
|
599
|
+
this.draftTitleError.set('');
|
|
600
|
+
this.draftKind.set('melhoria');
|
|
601
|
+
this.draftColumn.set(this.draftStatus());
|
|
602
|
+
this.draftPriority.set('media');
|
|
603
|
+
this.draftStart.set(null);
|
|
604
|
+
this.draftDue.set(null);
|
|
605
|
+
this.draftDescription.set('');
|
|
606
|
+
this.draftLabels.set([]);
|
|
607
|
+
this.draftAssignMe.set(true);
|
|
608
|
+
}
|
|
609
|
+
async load(light) {
|
|
610
|
+
this.current.set(light);
|
|
611
|
+
try {
|
|
612
|
+
const full = await this.service.task(this.clientId(), light.id);
|
|
613
|
+
if (this.task()?.id !== light.id)
|
|
614
|
+
return;
|
|
615
|
+
this.current.set({ ...full, author: full.author ?? light.author });
|
|
616
|
+
void this.loadEvents('comments', true);
|
|
617
|
+
}
|
|
618
|
+
catch {
|
|
619
|
+
this.loadError.set(this.text.loadError);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
setOpen(open) {
|
|
623
|
+
if (!open)
|
|
624
|
+
this.closed.emit();
|
|
625
|
+
}
|
|
626
|
+
/** Aplica a versão do servidor aqui e avisa o quadro. */
|
|
627
|
+
apply(task) {
|
|
628
|
+
const prev = this.current();
|
|
629
|
+
const next = { ...task, author: task.author ?? prev?.author ?? null };
|
|
630
|
+
this.current.set(next);
|
|
631
|
+
this.changed.emit(next);
|
|
632
|
+
}
|
|
633
|
+
/** Roda uma mudança no servidor; erro = aviso no canto. */
|
|
634
|
+
async run(call, success) {
|
|
635
|
+
if (this.busy())
|
|
636
|
+
return false;
|
|
637
|
+
this.busy.set(true);
|
|
638
|
+
try {
|
|
639
|
+
this.apply(await call());
|
|
640
|
+
if (success)
|
|
641
|
+
this.toast.success(success);
|
|
642
|
+
if (this.tab() === 'activity')
|
|
643
|
+
void this.loadEvents('activity', true);
|
|
644
|
+
return true;
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
this.toast.error(this.text.saveError);
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
finally {
|
|
651
|
+
this.busy.set(false);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
edit(patch) {
|
|
655
|
+
const t = this.current();
|
|
656
|
+
return t ? this.run(() => this.service.update(this.clientId(), t.id, patch)) : Promise.resolve(false);
|
|
657
|
+
}
|
|
658
|
+
// --- título e descrição ---
|
|
659
|
+
startTitle() {
|
|
660
|
+
this.titleDraft.set(this.current()?.title ?? '');
|
|
661
|
+
this.editingTitle.set(true);
|
|
662
|
+
}
|
|
663
|
+
async saveTitle() {
|
|
664
|
+
const title = this.titleDraft().trim();
|
|
665
|
+
if (!title || title === this.current()?.title) {
|
|
666
|
+
this.editingTitle.set(false);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
if (await this.edit({ title }))
|
|
670
|
+
this.editingTitle.set(false);
|
|
671
|
+
}
|
|
672
|
+
onTitleKey(event) {
|
|
673
|
+
if (event.key === 'Enter') {
|
|
674
|
+
event.preventDefault();
|
|
675
|
+
void this.saveTitle();
|
|
676
|
+
}
|
|
677
|
+
else if (event.key === 'Escape') {
|
|
678
|
+
event.stopPropagation();
|
|
679
|
+
this.editingTitle.set(false);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
startDescription() {
|
|
683
|
+
this.descriptionDraft.set(this.current()?.description_html ?? '');
|
|
684
|
+
this.editingDescription.set(true);
|
|
685
|
+
}
|
|
686
|
+
async saveDescription() {
|
|
687
|
+
if (await this.edit({ description_html: this.descriptionDraft() }))
|
|
688
|
+
this.editingDescription.set(false);
|
|
689
|
+
}
|
|
690
|
+
// --- campos da direita ---
|
|
691
|
+
changeStatus(status) {
|
|
692
|
+
const t = this.current();
|
|
693
|
+
if (!t || status === t.status)
|
|
694
|
+
return;
|
|
695
|
+
void this.run(() => this.service.move(this.clientId(), t.id, { status: status }), this.text.statusSaved);
|
|
696
|
+
}
|
|
697
|
+
changeKind(kind) {
|
|
698
|
+
if (kind !== this.current()?.kind)
|
|
699
|
+
void this.edit({ kind: kind });
|
|
700
|
+
}
|
|
701
|
+
changePriority(priority) {
|
|
702
|
+
if (priority !== this.current()?.priority)
|
|
703
|
+
void this.edit({ priority: priority });
|
|
704
|
+
}
|
|
705
|
+
changeDate(field, value) {
|
|
706
|
+
const t = this.current();
|
|
707
|
+
if (!t || (value || null) === t[field])
|
|
708
|
+
return;
|
|
709
|
+
void this.edit({ [field]: value || null });
|
|
710
|
+
}
|
|
711
|
+
changeCover(cover, popover) {
|
|
712
|
+
popover?.close();
|
|
713
|
+
if (cover !== this.current()?.cover)
|
|
714
|
+
void this.edit({ cover });
|
|
715
|
+
}
|
|
716
|
+
toggleChecklistOnCard(on) {
|
|
717
|
+
if (on !== this.current()?.checklist_on_card)
|
|
718
|
+
void this.edit({ checklist_on_card: on });
|
|
719
|
+
}
|
|
720
|
+
assignMe() {
|
|
721
|
+
const t = this.current();
|
|
722
|
+
if (t)
|
|
723
|
+
void this.run(() => this.service.addPerson(this.clientId(), t.id, 'me', 'responsavel'), this.text.assignedToMe);
|
|
724
|
+
}
|
|
725
|
+
addPerson(person, role, popover) {
|
|
726
|
+
popover.close();
|
|
727
|
+
const t = this.current();
|
|
728
|
+
if (t)
|
|
729
|
+
void this.run(() => this.service.addPerson(this.clientId(), t.id, person.id, role));
|
|
730
|
+
}
|
|
731
|
+
removePerson(person, role) {
|
|
732
|
+
const t = this.current();
|
|
733
|
+
if (t)
|
|
734
|
+
void this.run(() => this.service.removePerson(this.clientId(), t.id, person.user_id, role));
|
|
735
|
+
}
|
|
736
|
+
toggleLabel(label, linked) {
|
|
737
|
+
const t = this.current();
|
|
738
|
+
if (!t) {
|
|
739
|
+
this.draftLabels.update((ids) => (linked ? [...new Set([...ids, label.id])] : ids.filter((id) => id !== label.id)));
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
void this.run(() => this.service.setLabel(this.clientId(), t.id, label.id, linked));
|
|
743
|
+
}
|
|
744
|
+
async createLabel(name, color) {
|
|
745
|
+
try {
|
|
746
|
+
const label = await this.service.createLabel(this.clientId(), name, color);
|
|
747
|
+
const entry = { ...label, uses: 0 };
|
|
748
|
+
this.labelCreated.emit(entry);
|
|
749
|
+
this.toggleLabel(entry, true);
|
|
750
|
+
}
|
|
751
|
+
catch {
|
|
752
|
+
this.toast.error(this.text.saveError);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
async copyKey() {
|
|
756
|
+
const t = this.current();
|
|
757
|
+
if (!t)
|
|
758
|
+
return;
|
|
759
|
+
if (await copyText(t.key))
|
|
760
|
+
this.toast.success(this.text.keyCopied);
|
|
761
|
+
else
|
|
762
|
+
this.toast.error(this.text.copyError);
|
|
763
|
+
}
|
|
764
|
+
screenHref(path) {
|
|
765
|
+
return this.location?.prepareExternalUrl(path) ?? path;
|
|
766
|
+
}
|
|
767
|
+
/** Clique simples navega no app (e fecha); com Ctrl/Cmd/Shift ou botão do meio, o navegador abre a aba. */
|
|
768
|
+
openScreen(event, path) {
|
|
769
|
+
if (!this.router || event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey)
|
|
770
|
+
return;
|
|
771
|
+
event.preventDefault();
|
|
772
|
+
this.closed.emit();
|
|
773
|
+
void this.router.navigateByUrl(path);
|
|
774
|
+
}
|
|
775
|
+
toggleArchived() {
|
|
776
|
+
const t = this.current();
|
|
777
|
+
if (!t)
|
|
778
|
+
return;
|
|
779
|
+
const archive = !t.archived_at;
|
|
780
|
+
void this.run(() => this.service.setArchived(this.clientId(), t.id, archive), archive ? this.text.archivedToast : this.text.restoredToast);
|
|
781
|
+
}
|
|
782
|
+
async remove() {
|
|
783
|
+
const t = this.current();
|
|
784
|
+
if (!t)
|
|
785
|
+
return;
|
|
786
|
+
const ok = await this.dialogs.confirm({
|
|
787
|
+
heading: formatMessage(this.text.deleteHeading, { key: t.key }), message: this.text.deleteMessage,
|
|
788
|
+
confirmLabel: this.text.deleteConfirm, danger: true,
|
|
789
|
+
});
|
|
790
|
+
if (!ok)
|
|
791
|
+
return;
|
|
792
|
+
try {
|
|
793
|
+
await this.service.remove(this.clientId(), t.id);
|
|
794
|
+
this.toast.success(this.text.deleted);
|
|
795
|
+
this.removed.emit(t);
|
|
796
|
+
}
|
|
797
|
+
catch {
|
|
798
|
+
this.toast.error(this.text.saveError);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
// --- agrupar ---
|
|
802
|
+
/** Desagrupa esta (a agrupada): ela volta ao quadro. */
|
|
803
|
+
async unmergeSelf() {
|
|
804
|
+
const t = this.current();
|
|
805
|
+
if (!t?.merged_into || this.busy())
|
|
806
|
+
return;
|
|
807
|
+
if (await this.run(() => this.service.unmerge(this.clientId(), t.id), formatMessage(this.text.unmergedToast, { key: t.key }))) {
|
|
808
|
+
this.unmerged.emit();
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
/** Desagrupa uma das agrupadas nesta: relê esta (perde os relatores e apoios que vieram dela). */
|
|
812
|
+
async unmergeOrigin(origin) {
|
|
813
|
+
const t = this.current();
|
|
814
|
+
if (!t)
|
|
815
|
+
return;
|
|
816
|
+
const ok = await this.run(async () => {
|
|
817
|
+
await this.service.unmerge(this.clientId(), origin.id);
|
|
818
|
+
return this.service.task(this.clientId(), t.id);
|
|
819
|
+
}, formatMessage(this.text.unmergedToast, { key: origin.key }));
|
|
820
|
+
if (ok)
|
|
821
|
+
this.unmerged.emit();
|
|
822
|
+
}
|
|
823
|
+
/** Quem pediu: o autor e os relatores extras (do detalhe); sem a lista ainda, só o autor. */
|
|
824
|
+
reportersOf(t) {
|
|
825
|
+
if (t.reporters?.length)
|
|
826
|
+
return t.reporters;
|
|
827
|
+
return [{ ...(t.author ?? { user_id: '', username: null, name: null, picture: null }), role: 'author', from_suggestion_id: null }];
|
|
828
|
+
}
|
|
829
|
+
/** "Autor" ou "via SUG-30" (a agrupada de onde o relator veio). */
|
|
830
|
+
reporterHint(t, r) {
|
|
831
|
+
if (r.role === 'author')
|
|
832
|
+
return this.text.reporterAuthor;
|
|
833
|
+
const origin = t.merged?.find((m) => m.id === r.from_suggestion_id);
|
|
834
|
+
return origin ? formatMessage(this.text.reporterVia, { key: origin.key }) : '';
|
|
835
|
+
}
|
|
836
|
+
mergedIntoText(key) {
|
|
837
|
+
return formatMessage(this.text.mergedInto, { key });
|
|
838
|
+
}
|
|
839
|
+
openMergedText(key) {
|
|
840
|
+
return formatMessage(this.text.openMerged, { key });
|
|
841
|
+
}
|
|
842
|
+
unmergeLabel(key) {
|
|
843
|
+
return formatMessage(this.text.unmergeLabel, { key });
|
|
844
|
+
}
|
|
845
|
+
// --- checklist ---
|
|
846
|
+
addItem() {
|
|
847
|
+
const t = this.current();
|
|
848
|
+
const text = this.newItem().trim();
|
|
849
|
+
if (!t || !text)
|
|
850
|
+
return;
|
|
851
|
+
void this.run(() => this.service.addChecklistItem(this.clientId(), t.id, text)).then((ok) => ok && this.newItem.set(''));
|
|
852
|
+
}
|
|
853
|
+
onNewItemKey(event) {
|
|
854
|
+
if (event.key === 'Enter') {
|
|
855
|
+
event.preventDefault();
|
|
856
|
+
this.addItem();
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
toggleItem(item, done) {
|
|
860
|
+
const t = this.current();
|
|
861
|
+
if (t && done !== item.done)
|
|
862
|
+
void this.run(() => this.service.updateChecklistItem(this.clientId(), t.id, item.id, { done }));
|
|
863
|
+
}
|
|
864
|
+
startRename(item) {
|
|
865
|
+
this.renaming.set(item.id);
|
|
866
|
+
this.renameDraft.set(item.text);
|
|
867
|
+
}
|
|
868
|
+
saveRename(item) {
|
|
869
|
+
const text = this.renameDraft().trim();
|
|
870
|
+
this.renaming.set(null);
|
|
871
|
+
const t = this.current();
|
|
872
|
+
if (t && text && text !== item.text)
|
|
873
|
+
void this.run(() => this.service.updateChecklistItem(this.clientId(), t.id, item.id, { text }));
|
|
874
|
+
}
|
|
875
|
+
onRenameKey(event, item) {
|
|
876
|
+
if (event.key === 'Enter') {
|
|
877
|
+
event.preventDefault();
|
|
878
|
+
this.saveRename(item);
|
|
879
|
+
}
|
|
880
|
+
else if (event.key === 'Escape') {
|
|
881
|
+
event.stopPropagation();
|
|
882
|
+
this.renaming.set(null);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
moveItem(item, delta) {
|
|
886
|
+
const t = this.current();
|
|
887
|
+
const index = this.checklistItems().findIndex((i) => i.id === item.id);
|
|
888
|
+
const target = index + delta;
|
|
889
|
+
if (!t || index < 0 || target < 0 || target >= this.checklistItems().length)
|
|
890
|
+
return;
|
|
891
|
+
void this.run(() => this.service.updateChecklistItem(this.clientId(), t.id, item.id, { position: target }));
|
|
892
|
+
}
|
|
893
|
+
removeItem(item) {
|
|
894
|
+
const t = this.current();
|
|
895
|
+
if (t)
|
|
896
|
+
void this.run(() => this.service.removeChecklistItem(this.clientId(), t.id, item.id));
|
|
897
|
+
}
|
|
898
|
+
// --- comentários e atividades ---
|
|
899
|
+
selectTab(tab) {
|
|
900
|
+
const next = tab === 'activity' ? 'activity' : 'comments';
|
|
901
|
+
this.tab.set(next);
|
|
902
|
+
if ((next === 'activity' ? this.activity() : this.comments()).length === 0)
|
|
903
|
+
void this.loadEvents(next, true);
|
|
904
|
+
}
|
|
905
|
+
async loadEvents(type, reset) {
|
|
906
|
+
const t = this.current();
|
|
907
|
+
if (!t)
|
|
908
|
+
return;
|
|
909
|
+
const list = type === 'comments' ? this.comments : this.activity;
|
|
910
|
+
const total = type === 'comments' ? this.commentsTotal : this.activityTotal;
|
|
911
|
+
this.eventsLoading.set(true);
|
|
912
|
+
try {
|
|
913
|
+
const res = await this.service.events(this.clientId(), t.id, { type, limit: EVENTS_PAGE, offset: reset ? 0 : list().length });
|
|
914
|
+
if (this.current()?.id !== t.id)
|
|
915
|
+
return;
|
|
916
|
+
list.set(reset ? res.events : [...list(), ...res.events]);
|
|
917
|
+
total.set(res.total);
|
|
918
|
+
}
|
|
919
|
+
catch {
|
|
920
|
+
this.loadError.set(this.text.loadError);
|
|
921
|
+
}
|
|
922
|
+
finally {
|
|
923
|
+
this.eventsLoading.set(false);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
async sendComment() {
|
|
927
|
+
const t = this.current();
|
|
928
|
+
const comment = this.comment().trim();
|
|
929
|
+
if (!t || this.commenting())
|
|
930
|
+
return;
|
|
931
|
+
this.commentError.set(comment ? '' : this.text.commentRequired);
|
|
932
|
+
if (!comment)
|
|
933
|
+
return;
|
|
934
|
+
this.commenting.set(true);
|
|
935
|
+
try {
|
|
936
|
+
const event = await this.service.comment(this.clientId(), t.id, comment, this.commentInternal());
|
|
937
|
+
this.comments.update((list) => [event, ...list]);
|
|
938
|
+
this.commentsTotal.update((n) => n + 1);
|
|
939
|
+
this.comment.set('');
|
|
940
|
+
this.commentInternal.set(false);
|
|
941
|
+
const updated = {
|
|
942
|
+
...t, comments_count: t.comments_count + 1,
|
|
943
|
+
last_activity: { at: event.created_at, actor: event.actor },
|
|
944
|
+
...(event.internal ? {} : { last_comment: { comment: event.comment ?? comment, created_at: event.created_at } }),
|
|
945
|
+
};
|
|
946
|
+
this.current.set(updated);
|
|
947
|
+
this.changed.emit(updated);
|
|
948
|
+
this.toast.success(this.text.commentSent);
|
|
949
|
+
}
|
|
950
|
+
catch {
|
|
951
|
+
this.commentError.set(this.text.commentError);
|
|
952
|
+
}
|
|
953
|
+
finally {
|
|
954
|
+
this.commenting.set(false);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
// --- nova tarefa ---
|
|
958
|
+
async create() {
|
|
959
|
+
const title = this.draftTitle().trim();
|
|
960
|
+
this.draftTitleError.set(title ? '' : this.text.titleRequired);
|
|
961
|
+
if (!title || this.busy())
|
|
962
|
+
return;
|
|
963
|
+
this.busy.set(true);
|
|
964
|
+
try {
|
|
965
|
+
const task = await this.service.create(this.clientId(), {
|
|
966
|
+
title, kind: this.draftKind(), status: this.draftColumn(),
|
|
967
|
+
priority: this.draftPriority(), start_date: this.draftStart(), due_date: this.draftDue(),
|
|
968
|
+
description_html: this.draftDescription(), label_ids: this.draftLabels(),
|
|
969
|
+
people: this.draftAssignMe() ? [{ user_id: 'me', role: 'responsavel' }] : [],
|
|
970
|
+
});
|
|
971
|
+
this.toast.success(this.text.taskCreated);
|
|
972
|
+
this.created.emit(task);
|
|
973
|
+
}
|
|
974
|
+
catch {
|
|
975
|
+
this.toast.error(this.text.createError);
|
|
976
|
+
}
|
|
977
|
+
finally {
|
|
978
|
+
this.busy.set(false);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
// --- textos ---
|
|
982
|
+
personName(p) {
|
|
983
|
+
return p?.name || p?.username || this.text.removedAuthor;
|
|
984
|
+
}
|
|
985
|
+
statusLabel(status) {
|
|
986
|
+
if (!status)
|
|
987
|
+
return '—';
|
|
988
|
+
return this.text.statuses[status] ?? this.text.legacyStatuses[status] ?? status;
|
|
989
|
+
}
|
|
990
|
+
/** O valor de um evento 'change' legível: prioridade e tipo pelo catálogo, data no formato local. */
|
|
991
|
+
valueText(field, value) {
|
|
992
|
+
if (!value)
|
|
993
|
+
return '—';
|
|
994
|
+
if (field === 'priority')
|
|
995
|
+
return this.text.priorities[value] ?? value;
|
|
996
|
+
if (field === 'kind')
|
|
997
|
+
return this.text.kinds[value] ?? value;
|
|
998
|
+
if (field === 'start_date' || field === 'due_date')
|
|
999
|
+
return this.day(value);
|
|
1000
|
+
return value;
|
|
1001
|
+
}
|
|
1002
|
+
eventText(e) {
|
|
1003
|
+
const actor = e.actor ? this.personName(e.actor) : this.text.someone;
|
|
1004
|
+
if (e.kind === 'comment')
|
|
1005
|
+
return formatMessage(this.text.eventComment, { actor });
|
|
1006
|
+
if (e.kind === 'status')
|
|
1007
|
+
return formatMessage(this.text.eventStatus, { actor, from: this.statusLabel(e.from_status), to: this.statusLabel(e.to_status) });
|
|
1008
|
+
const field = e.field ?? 'other';
|
|
1009
|
+
const removed = ['start_date', 'due_date', 'cover'].includes(field) && !e.to_value ? `${field}_removed` : field;
|
|
1010
|
+
const template = this.text.activity[removed] ?? this.text.activity.other;
|
|
1011
|
+
return formatMessage(template, { actor, from: this.valueText(e.field, e.from_value), to: this.valueText(e.field, e.to_value) });
|
|
1012
|
+
}
|
|
1013
|
+
lastChange(t) {
|
|
1014
|
+
if (!t.last_activity)
|
|
1015
|
+
return '';
|
|
1016
|
+
const when = this.age(t.last_activity.at);
|
|
1017
|
+
return t.last_activity.actor
|
|
1018
|
+
? formatMessage(this.text.lastChange, { when, actor: this.personName(t.last_activity.actor) })
|
|
1019
|
+
: formatMessage(this.text.lastChangeNoActor, { when });
|
|
1020
|
+
}
|
|
1021
|
+
dueText(t) {
|
|
1022
|
+
if (!t.due_date)
|
|
1023
|
+
return '';
|
|
1024
|
+
const s = this.dueSituation();
|
|
1025
|
+
if (s === 'overdue')
|
|
1026
|
+
return formatMessage(this.text.dueLate, { date: this.day(t.due_date) });
|
|
1027
|
+
if (s === 'today')
|
|
1028
|
+
return this.text.dueToday;
|
|
1029
|
+
if (s === 'tomorrow')
|
|
1030
|
+
return this.text.dueTomorrow;
|
|
1031
|
+
return formatMessage(this.text.dueOn, { date: this.day(t.due_date) });
|
|
1032
|
+
}
|
|
1033
|
+
removePersonLabel(p) {
|
|
1034
|
+
return formatMessage(this.text.removePerson, { name: this.personName(p) });
|
|
1035
|
+
}
|
|
1036
|
+
itemLabel(template, item) {
|
|
1037
|
+
return formatMessage(template, { text: item.text });
|
|
1038
|
+
}
|
|
1039
|
+
age(iso) {
|
|
1040
|
+
const seconds = Math.round((Date.now() - new Date(iso).getTime()) / 1000);
|
|
1041
|
+
const units = [['year', 31_536_000], ['month', 2_592_000], ['week', 604_800], ['day', 86_400], ['hour', 3_600], ['minute', 60]];
|
|
1042
|
+
try {
|
|
1043
|
+
const rtf = new Intl.RelativeTimeFormat(this.messages.meta.locale, { numeric: 'auto' });
|
|
1044
|
+
for (const [unit, size] of units)
|
|
1045
|
+
if (Math.abs(seconds) >= size)
|
|
1046
|
+
return rtf.format(-Math.floor(seconds / size), unit);
|
|
1047
|
+
return rtf.format(0, 'minute');
|
|
1048
|
+
}
|
|
1049
|
+
catch {
|
|
1050
|
+
return this.date(iso);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
date(iso, withTime = false) {
|
|
1054
|
+
try {
|
|
1055
|
+
return new Intl.DateTimeFormat(this.messages.meta.locale, {
|
|
1056
|
+
dateStyle: 'short', ...(withTime ? { timeStyle: 'short' } : {}), timeZone: this.preferences.effectiveTimeZone(),
|
|
1057
|
+
}).format(new Date(iso));
|
|
1058
|
+
}
|
|
1059
|
+
catch {
|
|
1060
|
+
return iso.slice(0, 10);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
/** Data sem hora (AAAA-MM-DD), sem fuso: o dia é o dia. */
|
|
1064
|
+
day(iso) {
|
|
1065
|
+
try {
|
|
1066
|
+
return new Intl.DateTimeFormat(this.messages.meta.locale, { dateStyle: 'short', timeZone: 'UTC' }).format(new Date(`${iso}T00:00:00Z`));
|
|
1067
|
+
}
|
|
1068
|
+
catch {
|
|
1069
|
+
return iso;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionTaskDetailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1073
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SuggestionTaskDetailComponent, isStandalone: true, selector: "spine-suggestion-task-detail", inputs: { clientId: { classPropertyName: "clientId", publicName: "clientId", isSignal: true, isRequired: true, transformFunction: null }, task: { classPropertyName: "task", publicName: "task", isSignal: true, isRequired: false, transformFunction: null }, draft: { classPropertyName: "draft", publicName: "draft", isSignal: true, isRequired: false, transformFunction: null }, draftStatus: { classPropertyName: "draftStatus", publicName: "draftStatus", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null }, canOpenScreen: { classPropertyName: "canOpenScreen", publicName: "canOpenScreen", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { changed: "changed", created: "created", removed: "removed", closed: "closed", labelCreated: "labelCreated", mergeRequested: "mergeRequested", openTask: "openTask", unmerged: "unmerged" }, ngImport: i0, template: "<hm-modal [heading]=\"heading()\" [open]=\"open()\" (openChange)=\"setOpen($event)\" size=\"xlarge\">\n @if (current(); as t) {\n @if (t.cover) {\n <div class=\"as-task__cover\" [class]=\"'as-tok--' + t.cover\" aria-hidden=\"true\"></div>\n }\n @if (loadError()) {\n <hm-alert variant=\"error\">{{ loadError() }}</hm-alert>\n }\n <div class=\"as-task\">\n <!-- Esquerda: o trabalho (t\u00EDtulo, descri\u00E7\u00E3o, checklist, conversa). -->\n <div class=\"as-task__main\">\n <div class=\"as-task__key-row\">\n <span class=\"as-task__key\">{{ t.key }}</span>\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"copy\" [label]=\"text.copyKey\" (clicked)=\"copyKey()\" />\n <hm-tag [name]=\"text.kinds[t.kind]\" [color]=\"kindColors[t.kind]\" />\n @if (t.archived_at) { <hm-tag [name]=\"text.archivedTag\" /> }\n @if (t.merged_into) { <hm-tag [name]=\"text.mergedTag\" color=\"info\" /> }\n </div>\n\n <!-- Agrupada: o destino tem os relatores e os apoios; os coment\u00E1rios ficam aqui. -->\n @if (t.merged_into; as into) {\n <div class=\"as-task__merged\">\n <hm-icon class=\"as-task__merged-icon\" shape=\"layers\" />\n <div class=\"as-task__merged-text\">\n <strong>{{ mergedIntoText(into.key) }}</strong>\n <span>{{ into.title }}</span>\n <span class=\"as-field__hint\">{{ text.mergedIntoHint }}</span>\n </div>\n <div class=\"as-actions\">\n <hm-button size=\"small\" emphasis=\"medium\" (clicked)=\"openTask.emit(into.id)\">{{ openMergedText(into.key) }}</hm-button>\n <hm-button size=\"small\" emphasis=\"minimal\" icon=\"unlink\" [disabled]=\"busy()\" (clicked)=\"unmergeSelf()\">{{ text.unmerge }}</hm-button>\n </div>\n </div>\n }\n\n @if (editingTitle()) {\n <hm-input [label]=\"text.title\" labelHidden [maxLength]=\"limits.title\" [value]=\"titleDraft()\"\n (valueChange)=\"titleDraft.set($event)\" (keydown)=\"onTitleKey($event)\" (focusout)=\"saveTitle()\" />\n } @else {\n <h2 class=\"as-task__title\">\n <button type=\"button\" class=\"as-task__title-button\" [attr.aria-label]=\"text.editTitle + ': ' + t.title\" (click)=\"startTitle()\">\n {{ t.title }}<hm-icon class=\"as-task__title-pen\" shape=\"pencil\" />\n </button>\n </h2>\n }\n\n <section class=\"as-task__section\" aria-labelledby=\"as-task-description\">\n <div class=\"as-task__section-head\">\n <h3 id=\"as-task-description\" class=\"as-task__heading\">{{ text.description }}</h3>\n @if (!editingDescription()) {\n <hm-button size=\"small\" emphasis=\"minimal\" icon=\"pencil\" (clicked)=\"startDescription()\">{{ text.editDescription }}</hm-button>\n }\n </div>\n @if (editingDescription()) {\n <hm-rich-text [label]=\"text.description\" labelHidden [rows]=\"6\" [maxRows]=\"20\" [value]=\"descriptionDraft()\"\n (valueChange)=\"descriptionDraft.set($event)\" />\n <div class=\"as-actions\">\n <hm-button emphasis=\"medium\" (clicked)=\"editingDescription.set(false)\">{{ text.cancel }}</hm-button>\n <hm-button [loading]=\"busy()\" (clicked)=\"saveDescription()\">{{ text.save }}</hm-button>\n </div>\n } @else if (t.description_html) {\n <hm-rich-text class=\"as-task__description\" [label]=\"text.description\" labelHidden readonly hideToolbar [value]=\"t.description_html\" />\n } @else {\n <p class=\"muted\">{{ text.noDescription }}</p>\n }\n </section>\n\n <section class=\"as-task__section\" aria-labelledby=\"as-task-checklist\">\n <div class=\"as-task__section-head\">\n <h3 id=\"as-task-checklist\" class=\"as-task__heading\">\n {{ text.checklist }}\n @if (t.checklist.total) { <span class=\"as-task__count\">{{ t.checklist.done }}/{{ t.checklist.total }}</span> }\n </h3>\n <hm-switch [label]=\"text.checklistOnCard\" [value]=\"t.checklist_on_card\" (valueChange)=\"toggleChecklistOnCard($event)\" />\n </div>\n @if (t.checklist.total) {\n <div class=\"as-task__progress\" role=\"progressbar\" aria-valuemin=\"0\" [attr.aria-valuemax]=\"t.checklist.total\"\n [attr.aria-valuenow]=\"t.checklist.done\" [attr.aria-label]=\"text.checklist\">\n <span [style.inline-size.%]=\"(t.checklist.done / t.checklist.total) * 100\"></span>\n </div>\n }\n <ul class=\"as-checklist\">\n @for (item of checklistItems(); track item.id; let first = $first; let last = $last) {\n <li class=\"as-checklist__item\" [class.as-checklist__item--done]=\"item.done\">\n @if (renaming() === item.id) {\n <hm-input class=\"as-checklist__rename\" [label]=\"text.checklist\" labelHidden [maxLength]=\"500\" [value]=\"renameDraft()\"\n (valueChange)=\"renameDraft.set($event)\" (keydown)=\"onRenameKey($event, item)\" (focusout)=\"saveRename(item)\" />\n } @else {\n <hm-checkbox class=\"as-checklist__check\" [label]=\"item.text\" [checked]=\"item.done\" (checkedChange)=\"toggleItem(item, $event)\" />\n <span class=\"as-checklist__tools\">\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"pencil\" [label]=\"text.editTitle + ': ' + item.text\" (clicked)=\"startRename(item)\" />\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"angle-up\" [label]=\"itemLabel(text.checklistUp, item)\" [disabled]=\"first\" (clicked)=\"moveItem(item, -1)\" />\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"angle-down\" [label]=\"itemLabel(text.checklistDown, item)\" [disabled]=\"last\" (clicked)=\"moveItem(item, 1)\" />\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"trash\" [label]=\"itemLabel(text.checklistRemove, item)\" (clicked)=\"removeItem(item)\" />\n </span>\n }\n </li>\n } @empty {\n <li class=\"muted\">{{ text.checklistEmpty }}</li>\n }\n </ul>\n <div class=\"as-checklist__add\">\n <hm-input class=\"as-checklist__new\" [label]=\"text.checklistAdd\" labelHidden [placeholder]=\"text.checklistPlaceholder\"\n [maxLength]=\"500\" [value]=\"newItem()\" (valueChange)=\"newItem.set($event)\" (keydown)=\"onNewItemKey($event)\" />\n <hm-button emphasis=\"medium\" icon=\"plus\" [disabled]=\"!newItem().trim()\" (clicked)=\"addItem()\">{{ text.checklistAdd }}</hm-button>\n </div>\n </section>\n\n <hm-tabs [label]=\"text.taskLabel\" [active]=\"tab()\" (activeChange)=\"selectTab($event)\">\n <hm-tab id=\"comments\" [label]=\"text.tabComments\" [count]=\"commentsTotal() || null\">\n <div class=\"as-task__reply\">\n <hm-textarea [label]=\"text.comment\" [hint]=\"commentInternal() ? '' : text.commentHint\" [maxLength]=\"limits.description\"\n [rows]=\"2\" autoGrow [maxRows]=\"8\" [error]=\"commentError()\" [value]=\"comment()\" (valueChange)=\"comment.set($event)\" />\n <div class=\"as-task__reply-row\">\n <hm-checkbox [label]=\"text.commentInternal\" [checked]=\"commentInternal()\" (checkedChange)=\"commentInternal.set($event)\" />\n <hm-button icon=\"comment-message\" [loading]=\"commenting()\" (clicked)=\"sendComment()\">{{ text.commentSend }}</hm-button>\n </div>\n </div>\n <ol class=\"as-timeline\">\n @for (e of comments(); track e.id) {\n <li class=\"as-timeline__item\">\n <hm-account-avatar class=\"as-timeline__face\" size=\"small\" [name]=\"e.actor ? personName(e.actor) : text.someone\" [imageUrl]=\"e.actor?.picture\" />\n <div class=\"as-timeline__body\">\n <span class=\"as-timeline__line\">\n <strong>{{ e.actor ? personName(e.actor) : text.someone }}</strong> \u00B7\n <time class=\"muted\" [attr.datetime]=\"e.created_at\" [attr.title]=\"date(e.created_at, true)\">{{ age(e.created_at) }}</time>\n @if (e.kind === 'status') { \u00B7 <span class=\"muted\">{{ eventText(e) }}</span> }\n @if (e.internal) { <hm-tag [name]=\"text.internalTag\" color=\"warning\" /> }\n </span>\n <p class=\"as-timeline__comment\" [class.as-timeline__comment--internal]=\"e.internal\">{{ e.comment }}</p>\n </div>\n </li>\n } @empty {\n @if (!eventsLoading()) { <li class=\"muted\">{{ text.commentsEmpty }}</li> }\n }\n </ol>\n @if (comments().length < commentsTotal()) {\n <hm-button emphasis=\"medium\" [loading]=\"eventsLoading()\" (clicked)=\"loadEvents('comments', false)\">{{ text.loadMore }}</hm-button>\n }\n </hm-tab>\n <hm-tab id=\"activity\" [label]=\"text.tabActivity\">\n <ol class=\"as-timeline\">\n @for (e of activity(); track e.id) {\n <li class=\"as-timeline__item\">\n <hm-account-avatar class=\"as-timeline__face\" size=\"small\" [name]=\"e.actor ? personName(e.actor) : text.someone\" [imageUrl]=\"e.actor?.picture\" />\n <div class=\"as-timeline__body\">\n <span class=\"as-timeline__line\">{{ eventText(e) }} \u00B7\n <time class=\"muted\" [attr.datetime]=\"e.created_at\" [attr.title]=\"date(e.created_at, true)\">{{ age(e.created_at) }}</time>\n </span>\n @if (e.comment) {\n <p class=\"as-timeline__comment\" [class.as-timeline__comment--internal]=\"e.internal\">{{ e.comment }}</p>\n }\n </div>\n </li>\n } @empty {\n @if (!eventsLoading()) { <li class=\"muted\">{{ text.activityEmpty }}</li> }\n }\n </ol>\n @if (activity().length < activityTotal()) {\n <hm-button emphasis=\"medium\" [loading]=\"eventsLoading()\" (clicked)=\"loadEvents('activity', false)\">{{ text.loadMore }}</hm-button>\n }\n </hm-tab>\n </hm-tabs>\n </div>\n\n <!-- Direita: os campos da tarefa (cada mudan\u00E7a grava na hora). -->\n <aside class=\"as-task__side\" [attr.aria-label]=\"text.taskLabel\">\n <hm-select [label]=\"text.status\" [value]=\"t.status\" [disabled]=\"busy()\" (valueChange)=\"changeStatus($any($event))\">\n @for (st of statuses; track st) { <option [value]=\"st\">{{ text.statuses[st] }}</option> }\n </hm-select>\n\n <div class=\"as-field\">\n <div class=\"as-field__head\">\n <span class=\"as-field__label\">{{ text.assignees }}</span>\n @if (!isMine()) {\n <hm-button size=\"small\" emphasis=\"minimal\" icon=\"user-plus\" (clicked)=\"assignMe()\">{{ text.assignMe }}</hm-button>\n }\n </div>\n <ul class=\"as-people\">\n @for (p of t.assignees; track p.user_id) {\n <li class=\"as-people__item\">\n <hm-account-avatar size=\"small\" [name]=\"personName(p)\" [imageUrl]=\"p.picture\" />\n <span class=\"as-people__name\">{{ personName(p) }}</span>\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"trash\" [label]=\"removePersonLabel(p)\" (clicked)=\"removePerson(p, 'responsavel')\" />\n </li>\n } @empty {\n <li class=\"muted\">{{ text.nobody }}</li>\n }\n </ul>\n <hm-button size=\"small\" emphasis=\"minimal\" icon=\"plus\" [hmPopoverTrigger]=\"assigneePop\">{{ text.addAssignee }}</hm-button>\n <hm-popover #assigneePop [label]=\"text.addAssignee\">\n @if (assigneePop.open()) {\n <spine-suggestion-people-picker [clientId]=\"clientId()\" [exclude]=\"assigneeIds()\" (picked)=\"addPerson($event, 'responsavel', assigneePop)\" />\n }\n </hm-popover>\n </div>\n\n <div class=\"as-field\">\n <span class=\"as-field__label\">{{ text.watchers }}</span>\n <ul class=\"as-people\">\n @for (p of t.watchers; track p.user_id) {\n <li class=\"as-people__item\">\n <hm-account-avatar size=\"small\" [name]=\"personName(p)\" [imageUrl]=\"p.picture\" />\n <span class=\"as-people__name\">{{ personName(p) }}</span>\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"trash\" [label]=\"removePersonLabel(p)\" (clicked)=\"removePerson(p, 'envolvido')\" />\n </li>\n } @empty {\n <li class=\"muted\">{{ text.nobody }}</li>\n }\n </ul>\n <hm-button size=\"small\" emphasis=\"minimal\" icon=\"plus\" [hmPopoverTrigger]=\"watcherPop\">{{ text.addWatcher }}</hm-button>\n <hm-popover #watcherPop [label]=\"text.addWatcher\">\n @if (watcherPop.open()) {\n <spine-suggestion-people-picker [clientId]=\"clientId()\" [exclude]=\"watcherIds()\" (picked)=\"addPerson($event, 'envolvido', watcherPop)\" />\n }\n </hm-popover>\n </div>\n\n <!-- Quem pediu: o autor e, numa que recebeu agrupadas, os relatores que vieram delas. -->\n <div class=\"as-field\">\n <span class=\"as-field__label\">{{ (t.reporters?.length ?? 0) > 1 ? text.reporters : text.reporter }}</span>\n <ul class=\"as-people\">\n @for (p of reportersOf(t); track p.user_id || $index) {\n <li class=\"as-people__item\">\n <hm-account-avatar size=\"small\" [name]=\"personName(p.user_id ? p : null)\" [imageUrl]=\"p.picture\" />\n <span class=\"as-people__name\">{{ personName(p.user_id ? p : null) }}</span>\n @if ((t.reporters?.length ?? 0) > 1 && reporterHint(t, p); as hint) {\n <span class=\"as-field__hint\">{{ hint }}</span>\n }\n </li>\n }\n </ul>\n <span class=\"as-field__hint\">{{ t.origin === 'equipe' ? text.originTeam : text.originUser }}</span>\n </div>\n\n @if (t.merged?.length) {\n <div class=\"as-field\">\n <span class=\"as-field__label\">{{ text.mergedHere }}</span>\n <ul class=\"as-merged\">\n @for (m of t.merged; track m.id) {\n <li class=\"as-merged__item\">\n <button type=\"button\" class=\"as-merged__link\" (click)=\"openTask.emit(m.id)\">\n <span class=\"as-task__key\">{{ m.key }}</span> {{ m.title }}\n </button>\n <span class=\"as-merged__author\">\n <hm-account-avatar size=\"small\" [name]=\"personName(m.author)\" [imageUrl]=\"m.author?.picture\" />\n {{ personName(m.author) }}\n </span>\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"unlink\" [label]=\"unmergeLabel(m.key)\"\n [disabled]=\"busy()\" (clicked)=\"unmergeOrigin(m)\" />\n </li>\n }\n </ul>\n </div>\n }\n\n <hm-select [label]=\"text.priority\" [value]=\"t.priority\" [disabled]=\"busy()\" (valueChange)=\"changePriority($any($event))\">\n @for (p of priorities; track p) { <option [value]=\"p\">{{ text.priorities[p] }}</option> }\n </hm-select>\n <hm-select [label]=\"text.kind\" [value]=\"t.kind\" [disabled]=\"busy()\" (valueChange)=\"changeKind($any($event))\">\n @for (k of kinds; track k) { <option [value]=\"k\">{{ text.kinds[k] }}</option> }\n </hm-select>\n <div class=\"as-field__dates\">\n <hm-date-picker [label]=\"text.startDate\" [value]=\"t.start_date\" [max]=\"$any(t.due_date)\" (valueChange)=\"changeDate('start_date', $event)\" />\n <hm-date-picker [label]=\"text.dueDate\" [value]=\"t.due_date\" [min]=\"$any(t.start_date)\" (valueChange)=\"changeDate('due_date', $event)\" />\n </div>\n @if (t.due_date) {\n <span class=\"as-due\" [attr.data-due]=\"dueSituation()\"><hm-icon shape=\"calendar\" />{{ dueText(t) }}</span>\n }\n\n <div class=\"as-field\">\n <div class=\"as-field__head\">\n <span class=\"as-field__label\">{{ text.labels }}</span>\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"tag\" [label]=\"text.addLabel\" [hmPopoverTrigger]=\"labelPop\" />\n </div>\n <div class=\"as-field__chips\">\n @for (l of t.labels; track l.id) {\n <span class=\"as-label\" [class]=\"'as-tok--' + l.color\">{{ l.name }}</span>\n } @empty {\n <span class=\"muted\">{{ text.noLabels }}</span>\n }\n </div>\n <hm-popover #labelPop [label]=\"text.labels\">\n @if (labelPop.open()) {\n <spine-suggestion-label-picker [labels]=\"labels()\" [selected]=\"selectedLabels()\"\n (toggled)=\"toggleLabel($event.label, $event.linked)\" (created)=\"createLabel($event.name, $event.color)\" />\n }\n </hm-popover>\n </div>\n\n <div class=\"as-field\">\n <div class=\"as-field__head\">\n <span class=\"as-field__label\">{{ text.cover }}</span>\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"color-palette\" [label]=\"text.menuCover\" [hmPopoverTrigger]=\"coverPop\" />\n </div>\n @if (t.cover) {\n <span class=\"as-field__cover\" [class]=\"'as-tok--' + t.cover\" aria-hidden=\"true\"></span>\n } @else {\n <span class=\"muted\">\u2014</span>\n }\n <hm-popover #coverPop [label]=\"text.cover\">\n <spine-suggestion-cover-picker [value]=\"t.cover\" (picked)=\"changeCover($event, coverPop)\" />\n </hm-popover>\n </div>\n\n <dl class=\"as-facts\">\n <dt>{{ text.screenOrigin }}</dt>\n <dd>\n {{ t.screen.label || text.noScreen }}\n @if (t.screen.path && canOpenScreen()) {\n <a class=\"as-facts__link\" [href]=\"screenHref(t.screen.path)\" (click)=\"openScreen($event, t.screen.path)\">\n <hm-icon shape=\"pop-out\" />{{ text.openScreen }}\n </a>\n }\n </dd>\n <dt>{{ text.votesField }}</dt>\n <dd>{{ t.votes }}</dd>\n <dt>{{ text.createdAt }}</dt>\n <dd>{{ date(t.created_at, true) }}</dd>\n <dt>{{ text.updatedAt }}</dt>\n <dd>{{ date(t.updated_at, true) }}</dd>\n </dl>\n @if (lastChange(t); as change) {\n <p class=\"as-field__hint\">{{ change }}</p>\n }\n\n <div class=\"as-task__danger\">\n <!-- Sem cadeia: nem a agrupada nem a que recebeu outras v\u00E3o para dentro de outra. -->\n @if (!t.merged_into && !t.merged_count) {\n <hm-button size=\"small\" emphasis=\"medium\" icon=\"layers\" (clicked)=\"mergeRequested.emit(t)\">{{ text.menuMerge }}</hm-button>\n }\n <hm-button size=\"small\" emphasis=\"medium\" [icon]=\"t.archived_at ? 'unarchive' : 'archive'\" (clicked)=\"toggleArchived()\">\n {{ t.archived_at ? text.menuRestore : text.menuArchive }}\n </hm-button>\n <hm-button size=\"small\" emphasis=\"medium\" intent=\"danger\" icon=\"trash\" (clicked)=\"remove()\">{{ text.menuDelete }}</hm-button>\n </div>\n </aside>\n </div>\n } @else if (draft()) {\n <!-- Nova tarefa: o essencial; o resto (pessoas, checklist, conversa) assim que existir. -->\n <div class=\"as-task\">\n <div class=\"as-task__main\">\n <hm-input [label]=\"text.title\" [placeholder]=\"text.addTaskPlaceholder\" [maxLength]=\"limits.title\" required\n [error]=\"draftTitleError()\" [value]=\"draftTitle()\" (valueChange)=\"draftTitle.set($event)\" />\n <hm-rich-text [label]=\"text.description\" [rows]=\"6\" [maxRows]=\"20\" [value]=\"draftDescription()\" (valueChange)=\"draftDescription.set($event)\" />\n <p class=\"as-field__hint\">{{ text.draftHint }}</p>\n </div>\n <aside class=\"as-task__side\" [attr.aria-label]=\"text.taskLabel\">\n <hm-select [label]=\"text.status\" [value]=\"draftColumn()\" (valueChange)=\"draftColumn.set($any($event))\">\n @for (st of statuses; track st) { <option [value]=\"st\">{{ text.statuses[st] }}</option> }\n </hm-select>\n <hm-select [label]=\"text.kind\" [value]=\"draftKind()\" (valueChange)=\"draftKind.set($any($event))\">\n @for (k of kinds; track k) { <option [value]=\"k\">{{ text.kinds[k] }}</option> }\n </hm-select>\n <hm-select [label]=\"text.priority\" [value]=\"draftPriority()\" (valueChange)=\"draftPriority.set($any($event))\">\n @for (p of priorities; track p) { <option [value]=\"p\">{{ text.priorities[p] }}</option> }\n </hm-select>\n <div class=\"as-field__dates\">\n <hm-date-picker [label]=\"text.startDate\" [value]=\"draftStart()\" [max]=\"$any(draftDue())\" (valueChange)=\"draftStart.set($event)\" />\n <hm-date-picker [label]=\"text.dueDate\" [value]=\"draftDue()\" [min]=\"$any(draftStart())\" (valueChange)=\"draftDue.set($event)\" />\n </div>\n <div class=\"as-field\">\n <span class=\"as-field__label\">{{ text.labels }}</span>\n <spine-suggestion-label-picker [labels]=\"labels()\" [selected]=\"draftLabels()\"\n (toggled)=\"toggleLabel($event.label, $event.linked)\" (created)=\"createLabel($event.name, $event.color)\" />\n </div>\n <hm-checkbox [label]=\"text.assignMeOnCreate\" [checked]=\"draftAssignMe()\" (checkedChange)=\"draftAssignMe.set($event)\" />\n </aside>\n </div>\n }\n\n <div footer class=\"as-task__footer\">\n @if (!current() && draft()) {\n <hm-button emphasis=\"medium\" (clicked)=\"closed.emit()\">{{ text.cancel }}</hm-button>\n <hm-button icon=\"plus\" [loading]=\"busy()\" (clicked)=\"create()\">{{ text.createTask }}</hm-button>\n } @else {\n <hm-button emphasis=\"medium\" (clicked)=\"closed.emit()\">{{ text.close }}</hm-button>\n }\n </div>\n</hm-modal>\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}.as-tok--cat-1{--as-tok: var(--hm-data-categorical-1);background:var(--as-tok)}.as-tok--cat-2{--as-tok: var(--hm-data-categorical-2);background:var(--as-tok)}.as-tok--cat-3{--as-tok: var(--hm-data-categorical-3);background:var(--as-tok)}.as-tok--cat-4{--as-tok: var(--hm-data-categorical-4);background:var(--as-tok)}.as-tok--cat-5{--as-tok: var(--hm-data-categorical-5);background:var(--as-tok)}.as-tok--cat-6{--as-tok: var(--hm-data-categorical-6);background:var(--as-tok)}.as-tok--cat-7{--as-tok: var(--hm-data-categorical-7);background:var(--as-tok)}.as-tok--cat-8{--as-tok: var(--hm-data-categorical-8);background:var(--as-tok)}.as-tok--grad-1{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-2))}.as-tok--grad-2{--as-tok: var(--hm-data-categorical-3);background:linear-gradient(135deg,var(--hm-data-categorical-3),var(--hm-data-categorical-4))}.as-tok--grad-3{--as-tok: var(--hm-data-categorical-5);background:linear-gradient(135deg,var(--hm-data-categorical-5),var(--hm-data-categorical-6))}.as-tok--grad-4{--as-tok: var(--hm-data-categorical-7);background:linear-gradient(135deg,var(--hm-data-categorical-7),var(--hm-data-categorical-8))}.as-tok--grad-5{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-5))}.as-tok--grad-6{--as-tok: var(--hm-data-categorical-2);background:linear-gradient(135deg,var(--hm-data-categorical-2),var(--hm-data-categorical-6))}.as-label{display:inline-flex;align-items:center;gap:var(--hm-space-micro);max-inline-size:100%;padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:color-mix(in srgb,var(--as-tok, var(--hm-border)) 18%,var(--hm-surface-raised));color:var(--hm-text);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);line-height:1.6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.as-label:before{flex:none;inline-size:.5rem;block-size:.5rem;border-radius:50%;background:var(--as-tok, var(--hm-border));content:\"\"}@media(forced-colors:active){.as-label{border:var(--hm-border-hairline) solid CanvasText}}.as-task{display:grid;grid-template-columns:minmax(0,1fr) minmax(15rem,20rem);gap:var(--hm-space-lg);align-items:start}@media(max-width:48rem){.as-task{grid-template-columns:minmax(0,1fr)}}.as-task__cover{block-size:3rem;margin-block-end:var(--hm-space-sm);border-radius:var(--hm-radius-control)}.as-task__main{display:flex;flex-direction:column;gap:var(--hm-space-md);min-inline-size:0}.as-task__key-row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.as-task__key{color:var(--hm-muted);font-family:var(--hm-font-mono, monospace);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.as-task__title{margin:0;font-size:var(--hm-font-size-xl, 1.375rem)}.as-task__title-button{display:inline-flex;align-items:baseline;gap:var(--hm-space-xs);padding:0;border:0;background:none;color:var(--hm-text);font:inherit;font-weight:var(--hm-font-weight-bold);text-align:start;overflow-wrap:anywhere;cursor:text}.as-task__title-button:hover .as-task__title-pen,.as-task__title-button:focus-visible .as-task__title-pen{opacity:1}.as-task__title-button:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:2px}.as-task__title-pen{color:var(--hm-muted);opacity:0;font-size:var(--hm-font-size-md)}.as-task__section{display:flex;flex-direction:column;gap:var(--hm-space-xs);padding:var(--hm-space-sm);border-radius:var(--hm-radius-control);background:var(--hm-surface-muted)}.as-task__section-head{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs)}.as-task__heading{display:flex;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-task__count{color:var(--hm-muted);font-size:var(--hm-font-size-sm);font-variant-numeric:tabular-nums}.as-task__progress{block-size:.375rem;border-radius:var(--hm-radius-pill);background:var(--hm-border);overflow:hidden}.as-task__progress span{display:block;block-size:100%;background:var(--hm-success)}.as-task__reply{display:flex;flex-direction:column;gap:var(--hm-space-xs);margin-block:var(--hm-space-sm)}.as-task__reply-row{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs)}.as-task__side{display:flex;flex-direction:column;gap:var(--hm-space-sm);padding:var(--hm-space-sm);border-radius:var(--hm-radius-control);background:var(--hm-surface-muted)}.as-task__danger{display:flex;flex-wrap:wrap;gap:var(--hm-space-xs);padding-block-start:var(--hm-space-xs);border-block-start:var(--hm-border-hairline) solid var(--hm-border)}.as-task__footer{display:flex;justify-content:flex-end;gap:var(--hm-space-xs)}.as-field{display:flex;flex-direction:column;gap:var(--hm-space-micro)}.as-field__head{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-xs)}.as-field__label{color:var(--hm-muted);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.as-field__hint{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-field__chips{display:flex;flex-wrap:wrap;gap:var(--hm-space-micro)}.as-field__dates{display:grid;grid-template-columns:1fr 1fr;gap:var(--hm-space-xs)}.as-field__cover{display:block;block-size:1.5rem;border-radius:var(--hm-radius-control)}.as-people{display:flex;flex-direction:column;gap:var(--hm-space-micro);margin:0;padding:0;list-style:none}.as-people__item{display:flex;align-items:center;gap:var(--hm-space-xs);min-inline-size:0}.as-people__name{flex:1;min-inline-size:0;overflow:hidden;color:var(--hm-text);font-size:var(--hm-font-size-sm);text-overflow:ellipsis;white-space:nowrap}.as-task__merged{display:flex;flex-wrap:wrap;align-items:flex-start;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-xs) var(--hm-space-sm);border-inline-start:var(--hm-border-thin) solid var(--hm-info);border-radius:var(--hm-radius-control);background:var(--hm-info-bg);color:var(--hm-text)}.as-task__merged-icon{flex:none;color:var(--hm-info)}.as-task__merged-text{display:flex;flex:1 1 12rem;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0;overflow-wrap:anywhere}.as-merged{display:flex;flex-direction:column;gap:var(--hm-space-micro);margin:0;padding:0;list-style:none}.as-merged__item{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:var(--hm-space-micro) var(--hm-space-xs);padding:var(--hm-space-xxxs) var(--hm-space-xs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised)}.as-merged__item hm-icon-button{grid-row:1/span 2;grid-column:2}.as-merged__link{padding:0;border:0;background:none;color:var(--hm-accent);font:inherit;font-size:var(--hm-font-size-sm);text-align:start;overflow-wrap:anywhere;cursor:pointer}.as-merged__link:hover{text-decoration:underline}.as-merged__link:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:2px}.as-merged__author{display:inline-flex;align-items:center;gap:var(--hm-space-micro);color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-due{display:inline-flex;align-items:center;gap:var(--hm-space-micro);color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.as-due[data-due=overdue]{color:var(--hm-danger);font-weight:var(--hm-font-weight-semibold)}.as-due[data-due=today],.as-due[data-due=tomorrow]{color:var(--hm-text);font-weight:var(--hm-font-weight-semibold)}.as-facts{display:grid;grid-template-columns:max-content 1fr;gap:var(--hm-space-xxxs) var(--hm-space-sm);margin:0;font-size:var(--hm-font-size-sm)}.as-facts dt{color:var(--hm-muted)}.as-facts dd{margin:0;color:var(--hm-text);overflow-wrap:anywhere}.as-facts__link{display:inline-flex;align-items:center;gap:var(--hm-space-micro);margin-inline-start:var(--hm-space-xxxs);color:var(--hm-accent)}.as-checklist{display:flex;flex-direction:column;gap:var(--hm-space-micro);margin:0;padding:0;list-style:none}.as-checklist__item{display:flex;align-items:center;gap:var(--hm-space-xs);min-block-size:var(--hm-control-height-dense)}.as-checklist__item--done .as-checklist__check{color:var(--hm-muted);text-decoration:line-through}.as-checklist__item:hover .as-checklist__tools,.as-checklist__item:focus-within .as-checklist__tools{opacity:1}.as-checklist__check{flex:1;min-inline-size:0}.as-checklist__rename{flex:1}.as-checklist__tools{display:inline-flex;opacity:0}.as-checklist__add{display:flex;gap:var(--hm-space-xs);align-items:flex-start}.as-checklist__new{flex:1}@media(hover:none){.as-checklist__tools{opacity:1}}.as-timeline{display:flex;flex-direction:column;gap:var(--hm-space-sm);margin:var(--hm-space-sm) 0 0;padding:0;list-style:none}.as-timeline__item{position:relative;display:flex;align-items:flex-start;gap:var(--hm-space-xs);font-size:var(--hm-font-size-sm)}.as-timeline__item:not(:last-child):before{position:absolute;inset-block:var(--hm-space-lg) calc(var(--hm-space-sm) * -1);inset-inline-start:calc(var(--hm-space-sm) - var(--hm-border-hairline));border-inline-start:var(--hm-border-thin) solid var(--hm-border);content:\"\"}.as-timeline__face{position:relative;flex:none}.as-timeline__body{display:flex;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.as-timeline__line{display:inline-flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-micro);color:var(--hm-text)}.as-timeline__comment{margin:0;padding:var(--hm-space-xs) var(--hm-space-sm);border-radius:var(--hm-radius-control);background:var(--hm-surface-muted);color:var(--hm-text);white-space:pre-line;overflow-wrap:anywhere}.as-timeline__comment--internal{border-inline-start:var(--hm-border-thin) solid var(--hm-warning);background:var(--hm-warning-bg)}\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: CheckboxComponent, selector: "hm-checkbox", inputs: ["label", "labelHidden", "checked", "indeterminate"], outputs: ["checkedChange"] }, { kind: "component", type: DatePickerComponent, selector: "hm-date-picker", inputs: ["value", "label", "labelHidden", "hint", "placeholder", "min", "max", "valueFormat"], outputs: ["valueChange"] }, { kind: "component", type: IconButtonComponent, selector: "hm-icon-button", inputs: ["label", "icon", "solid", "iconDirection", "emphasis", "intent", "size", "disabled", "toggle", "pressed", "keyShortcuts"], outputs: ["pressedChange", "clicked"] }, { kind: "component", type: IconComponent, selector: "hm-icon", inputs: ["shape", "direction", "size", "solid", "badge", "status", "flip", "inverse", "label"] }, { kind: "component", type: InputComponent, selector: "hm-input", inputs: ["value", "label", "type", "placeholder", "icon", "labelHidden", "hint", "prefix", "suffix", "prefixIcon", "mask", "maskValue", "min", "max", "step", "stepButtons", "maxLength", "autocomplete", "inputMode"], outputs: ["valueChange"] }, { kind: "component", type: ModalComponent, selector: "hm-modal", inputs: ["open", "heading", "size", "closeLabel", "closable", "beforeClose"], outputs: ["openChange", "closed"] }, { kind: "component", type: PopoverComponent, selector: "hm-popover", inputs: ["open", "heading", "headingLevel", "label", "side", "align", "arrow", "closable", "closeLabel", "closeOnOutsideClick", "openOn", "openDelay", "closeDelay"], outputs: ["openChange"] }, { kind: "directive", type: PopoverTriggerDirective, selector: "[hmPopoverTrigger]", inputs: ["hmPopoverTrigger"] }, { kind: "component", type: RichTextComponent, selector: "hm-rich-text", inputs: ["value", "label", "labelHidden", "hint", "placeholder", "readonly", "loading", "tools", "headingLevels", "hideToolbar", "rows", "maxRows", "maxLength", "showCounter", "pasteMode", "linkProtocols", "allowRelativeLinks", "spellcheck"], outputs: ["valueChange", "focusChange"] }, { kind: "component", type: SelectComponent, selector: "hm-select", inputs: ["value", "label", "labelHidden", "hint"], outputs: ["valueChange"] }, { kind: "component", type: SuggestionCoverPickerComponent, selector: "spine-suggestion-cover-picker", inputs: ["value"], outputs: ["picked"] }, { kind: "component", type: SuggestionLabelPickerComponent, selector: "spine-suggestion-label-picker", inputs: ["labels", "selected"], outputs: ["toggled", "created"] }, { kind: "component", type: SuggestionPeoplePickerComponent, selector: "spine-suggestion-people-picker", inputs: ["clientId", "exclude"], outputs: ["picked"] }, { 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: "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: "component", type: TextareaComponent, selector: "hm-textarea", inputs: ["value", "label", "hint", "placeholder", "maxLength", "rows", "autoGrow", "labelHidden", "loading", "maxRows", "submitKey"], outputs: ["valueChange", "submitted"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1074
|
+
}
|
|
1075
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionTaskDetailComponent, decorators: [{
|
|
1076
|
+
type: Component,
|
|
1077
|
+
args: [{ selector: 'spine-suggestion-task-detail', imports: [
|
|
1078
|
+
AccountAvatarComponent, AlertComponent, ButtonComponent, CheckboxComponent, DatePickerComponent, IconButtonComponent,
|
|
1079
|
+
IconComponent, InputComponent, ModalComponent, PopoverComponent, PopoverTriggerDirective, RichTextComponent,
|
|
1080
|
+
SelectComponent, SuggestionCoverPickerComponent, SuggestionLabelPickerComponent, SuggestionPeoplePickerComponent,
|
|
1081
|
+
SwitchComponent, TabComponent, TabsComponent, TagComponent, TextareaComponent,
|
|
1082
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<hm-modal [heading]=\"heading()\" [open]=\"open()\" (openChange)=\"setOpen($event)\" size=\"xlarge\">\n @if (current(); as t) {\n @if (t.cover) {\n <div class=\"as-task__cover\" [class]=\"'as-tok--' + t.cover\" aria-hidden=\"true\"></div>\n }\n @if (loadError()) {\n <hm-alert variant=\"error\">{{ loadError() }}</hm-alert>\n }\n <div class=\"as-task\">\n <!-- Esquerda: o trabalho (t\u00EDtulo, descri\u00E7\u00E3o, checklist, conversa). -->\n <div class=\"as-task__main\">\n <div class=\"as-task__key-row\">\n <span class=\"as-task__key\">{{ t.key }}</span>\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"copy\" [label]=\"text.copyKey\" (clicked)=\"copyKey()\" />\n <hm-tag [name]=\"text.kinds[t.kind]\" [color]=\"kindColors[t.kind]\" />\n @if (t.archived_at) { <hm-tag [name]=\"text.archivedTag\" /> }\n @if (t.merged_into) { <hm-tag [name]=\"text.mergedTag\" color=\"info\" /> }\n </div>\n\n <!-- Agrupada: o destino tem os relatores e os apoios; os coment\u00E1rios ficam aqui. -->\n @if (t.merged_into; as into) {\n <div class=\"as-task__merged\">\n <hm-icon class=\"as-task__merged-icon\" shape=\"layers\" />\n <div class=\"as-task__merged-text\">\n <strong>{{ mergedIntoText(into.key) }}</strong>\n <span>{{ into.title }}</span>\n <span class=\"as-field__hint\">{{ text.mergedIntoHint }}</span>\n </div>\n <div class=\"as-actions\">\n <hm-button size=\"small\" emphasis=\"medium\" (clicked)=\"openTask.emit(into.id)\">{{ openMergedText(into.key) }}</hm-button>\n <hm-button size=\"small\" emphasis=\"minimal\" icon=\"unlink\" [disabled]=\"busy()\" (clicked)=\"unmergeSelf()\">{{ text.unmerge }}</hm-button>\n </div>\n </div>\n }\n\n @if (editingTitle()) {\n <hm-input [label]=\"text.title\" labelHidden [maxLength]=\"limits.title\" [value]=\"titleDraft()\"\n (valueChange)=\"titleDraft.set($event)\" (keydown)=\"onTitleKey($event)\" (focusout)=\"saveTitle()\" />\n } @else {\n <h2 class=\"as-task__title\">\n <button type=\"button\" class=\"as-task__title-button\" [attr.aria-label]=\"text.editTitle + ': ' + t.title\" (click)=\"startTitle()\">\n {{ t.title }}<hm-icon class=\"as-task__title-pen\" shape=\"pencil\" />\n </button>\n </h2>\n }\n\n <section class=\"as-task__section\" aria-labelledby=\"as-task-description\">\n <div class=\"as-task__section-head\">\n <h3 id=\"as-task-description\" class=\"as-task__heading\">{{ text.description }}</h3>\n @if (!editingDescription()) {\n <hm-button size=\"small\" emphasis=\"minimal\" icon=\"pencil\" (clicked)=\"startDescription()\">{{ text.editDescription }}</hm-button>\n }\n </div>\n @if (editingDescription()) {\n <hm-rich-text [label]=\"text.description\" labelHidden [rows]=\"6\" [maxRows]=\"20\" [value]=\"descriptionDraft()\"\n (valueChange)=\"descriptionDraft.set($event)\" />\n <div class=\"as-actions\">\n <hm-button emphasis=\"medium\" (clicked)=\"editingDescription.set(false)\">{{ text.cancel }}</hm-button>\n <hm-button [loading]=\"busy()\" (clicked)=\"saveDescription()\">{{ text.save }}</hm-button>\n </div>\n } @else if (t.description_html) {\n <hm-rich-text class=\"as-task__description\" [label]=\"text.description\" labelHidden readonly hideToolbar [value]=\"t.description_html\" />\n } @else {\n <p class=\"muted\">{{ text.noDescription }}</p>\n }\n </section>\n\n <section class=\"as-task__section\" aria-labelledby=\"as-task-checklist\">\n <div class=\"as-task__section-head\">\n <h3 id=\"as-task-checklist\" class=\"as-task__heading\">\n {{ text.checklist }}\n @if (t.checklist.total) { <span class=\"as-task__count\">{{ t.checklist.done }}/{{ t.checklist.total }}</span> }\n </h3>\n <hm-switch [label]=\"text.checklistOnCard\" [value]=\"t.checklist_on_card\" (valueChange)=\"toggleChecklistOnCard($event)\" />\n </div>\n @if (t.checklist.total) {\n <div class=\"as-task__progress\" role=\"progressbar\" aria-valuemin=\"0\" [attr.aria-valuemax]=\"t.checklist.total\"\n [attr.aria-valuenow]=\"t.checklist.done\" [attr.aria-label]=\"text.checklist\">\n <span [style.inline-size.%]=\"(t.checklist.done / t.checklist.total) * 100\"></span>\n </div>\n }\n <ul class=\"as-checklist\">\n @for (item of checklistItems(); track item.id; let first = $first; let last = $last) {\n <li class=\"as-checklist__item\" [class.as-checklist__item--done]=\"item.done\">\n @if (renaming() === item.id) {\n <hm-input class=\"as-checklist__rename\" [label]=\"text.checklist\" labelHidden [maxLength]=\"500\" [value]=\"renameDraft()\"\n (valueChange)=\"renameDraft.set($event)\" (keydown)=\"onRenameKey($event, item)\" (focusout)=\"saveRename(item)\" />\n } @else {\n <hm-checkbox class=\"as-checklist__check\" [label]=\"item.text\" [checked]=\"item.done\" (checkedChange)=\"toggleItem(item, $event)\" />\n <span class=\"as-checklist__tools\">\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"pencil\" [label]=\"text.editTitle + ': ' + item.text\" (clicked)=\"startRename(item)\" />\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"angle-up\" [label]=\"itemLabel(text.checklistUp, item)\" [disabled]=\"first\" (clicked)=\"moveItem(item, -1)\" />\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"angle-down\" [label]=\"itemLabel(text.checklistDown, item)\" [disabled]=\"last\" (clicked)=\"moveItem(item, 1)\" />\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"trash\" [label]=\"itemLabel(text.checklistRemove, item)\" (clicked)=\"removeItem(item)\" />\n </span>\n }\n </li>\n } @empty {\n <li class=\"muted\">{{ text.checklistEmpty }}</li>\n }\n </ul>\n <div class=\"as-checklist__add\">\n <hm-input class=\"as-checklist__new\" [label]=\"text.checklistAdd\" labelHidden [placeholder]=\"text.checklistPlaceholder\"\n [maxLength]=\"500\" [value]=\"newItem()\" (valueChange)=\"newItem.set($event)\" (keydown)=\"onNewItemKey($event)\" />\n <hm-button emphasis=\"medium\" icon=\"plus\" [disabled]=\"!newItem().trim()\" (clicked)=\"addItem()\">{{ text.checklistAdd }}</hm-button>\n </div>\n </section>\n\n <hm-tabs [label]=\"text.taskLabel\" [active]=\"tab()\" (activeChange)=\"selectTab($event)\">\n <hm-tab id=\"comments\" [label]=\"text.tabComments\" [count]=\"commentsTotal() || null\">\n <div class=\"as-task__reply\">\n <hm-textarea [label]=\"text.comment\" [hint]=\"commentInternal() ? '' : text.commentHint\" [maxLength]=\"limits.description\"\n [rows]=\"2\" autoGrow [maxRows]=\"8\" [error]=\"commentError()\" [value]=\"comment()\" (valueChange)=\"comment.set($event)\" />\n <div class=\"as-task__reply-row\">\n <hm-checkbox [label]=\"text.commentInternal\" [checked]=\"commentInternal()\" (checkedChange)=\"commentInternal.set($event)\" />\n <hm-button icon=\"comment-message\" [loading]=\"commenting()\" (clicked)=\"sendComment()\">{{ text.commentSend }}</hm-button>\n </div>\n </div>\n <ol class=\"as-timeline\">\n @for (e of comments(); track e.id) {\n <li class=\"as-timeline__item\">\n <hm-account-avatar class=\"as-timeline__face\" size=\"small\" [name]=\"e.actor ? personName(e.actor) : text.someone\" [imageUrl]=\"e.actor?.picture\" />\n <div class=\"as-timeline__body\">\n <span class=\"as-timeline__line\">\n <strong>{{ e.actor ? personName(e.actor) : text.someone }}</strong> \u00B7\n <time class=\"muted\" [attr.datetime]=\"e.created_at\" [attr.title]=\"date(e.created_at, true)\">{{ age(e.created_at) }}</time>\n @if (e.kind === 'status') { \u00B7 <span class=\"muted\">{{ eventText(e) }}</span> }\n @if (e.internal) { <hm-tag [name]=\"text.internalTag\" color=\"warning\" /> }\n </span>\n <p class=\"as-timeline__comment\" [class.as-timeline__comment--internal]=\"e.internal\">{{ e.comment }}</p>\n </div>\n </li>\n } @empty {\n @if (!eventsLoading()) { <li class=\"muted\">{{ text.commentsEmpty }}</li> }\n }\n </ol>\n @if (comments().length < commentsTotal()) {\n <hm-button emphasis=\"medium\" [loading]=\"eventsLoading()\" (clicked)=\"loadEvents('comments', false)\">{{ text.loadMore }}</hm-button>\n }\n </hm-tab>\n <hm-tab id=\"activity\" [label]=\"text.tabActivity\">\n <ol class=\"as-timeline\">\n @for (e of activity(); track e.id) {\n <li class=\"as-timeline__item\">\n <hm-account-avatar class=\"as-timeline__face\" size=\"small\" [name]=\"e.actor ? personName(e.actor) : text.someone\" [imageUrl]=\"e.actor?.picture\" />\n <div class=\"as-timeline__body\">\n <span class=\"as-timeline__line\">{{ eventText(e) }} \u00B7\n <time class=\"muted\" [attr.datetime]=\"e.created_at\" [attr.title]=\"date(e.created_at, true)\">{{ age(e.created_at) }}</time>\n </span>\n @if (e.comment) {\n <p class=\"as-timeline__comment\" [class.as-timeline__comment--internal]=\"e.internal\">{{ e.comment }}</p>\n }\n </div>\n </li>\n } @empty {\n @if (!eventsLoading()) { <li class=\"muted\">{{ text.activityEmpty }}</li> }\n }\n </ol>\n @if (activity().length < activityTotal()) {\n <hm-button emphasis=\"medium\" [loading]=\"eventsLoading()\" (clicked)=\"loadEvents('activity', false)\">{{ text.loadMore }}</hm-button>\n }\n </hm-tab>\n </hm-tabs>\n </div>\n\n <!-- Direita: os campos da tarefa (cada mudan\u00E7a grava na hora). -->\n <aside class=\"as-task__side\" [attr.aria-label]=\"text.taskLabel\">\n <hm-select [label]=\"text.status\" [value]=\"t.status\" [disabled]=\"busy()\" (valueChange)=\"changeStatus($any($event))\">\n @for (st of statuses; track st) { <option [value]=\"st\">{{ text.statuses[st] }}</option> }\n </hm-select>\n\n <div class=\"as-field\">\n <div class=\"as-field__head\">\n <span class=\"as-field__label\">{{ text.assignees }}</span>\n @if (!isMine()) {\n <hm-button size=\"small\" emphasis=\"minimal\" icon=\"user-plus\" (clicked)=\"assignMe()\">{{ text.assignMe }}</hm-button>\n }\n </div>\n <ul class=\"as-people\">\n @for (p of t.assignees; track p.user_id) {\n <li class=\"as-people__item\">\n <hm-account-avatar size=\"small\" [name]=\"personName(p)\" [imageUrl]=\"p.picture\" />\n <span class=\"as-people__name\">{{ personName(p) }}</span>\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"trash\" [label]=\"removePersonLabel(p)\" (clicked)=\"removePerson(p, 'responsavel')\" />\n </li>\n } @empty {\n <li class=\"muted\">{{ text.nobody }}</li>\n }\n </ul>\n <hm-button size=\"small\" emphasis=\"minimal\" icon=\"plus\" [hmPopoverTrigger]=\"assigneePop\">{{ text.addAssignee }}</hm-button>\n <hm-popover #assigneePop [label]=\"text.addAssignee\">\n @if (assigneePop.open()) {\n <spine-suggestion-people-picker [clientId]=\"clientId()\" [exclude]=\"assigneeIds()\" (picked)=\"addPerson($event, 'responsavel', assigneePop)\" />\n }\n </hm-popover>\n </div>\n\n <div class=\"as-field\">\n <span class=\"as-field__label\">{{ text.watchers }}</span>\n <ul class=\"as-people\">\n @for (p of t.watchers; track p.user_id) {\n <li class=\"as-people__item\">\n <hm-account-avatar size=\"small\" [name]=\"personName(p)\" [imageUrl]=\"p.picture\" />\n <span class=\"as-people__name\">{{ personName(p) }}</span>\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"trash\" [label]=\"removePersonLabel(p)\" (clicked)=\"removePerson(p, 'envolvido')\" />\n </li>\n } @empty {\n <li class=\"muted\">{{ text.nobody }}</li>\n }\n </ul>\n <hm-button size=\"small\" emphasis=\"minimal\" icon=\"plus\" [hmPopoverTrigger]=\"watcherPop\">{{ text.addWatcher }}</hm-button>\n <hm-popover #watcherPop [label]=\"text.addWatcher\">\n @if (watcherPop.open()) {\n <spine-suggestion-people-picker [clientId]=\"clientId()\" [exclude]=\"watcherIds()\" (picked)=\"addPerson($event, 'envolvido', watcherPop)\" />\n }\n </hm-popover>\n </div>\n\n <!-- Quem pediu: o autor e, numa que recebeu agrupadas, os relatores que vieram delas. -->\n <div class=\"as-field\">\n <span class=\"as-field__label\">{{ (t.reporters?.length ?? 0) > 1 ? text.reporters : text.reporter }}</span>\n <ul class=\"as-people\">\n @for (p of reportersOf(t); track p.user_id || $index) {\n <li class=\"as-people__item\">\n <hm-account-avatar size=\"small\" [name]=\"personName(p.user_id ? p : null)\" [imageUrl]=\"p.picture\" />\n <span class=\"as-people__name\">{{ personName(p.user_id ? p : null) }}</span>\n @if ((t.reporters?.length ?? 0) > 1 && reporterHint(t, p); as hint) {\n <span class=\"as-field__hint\">{{ hint }}</span>\n }\n </li>\n }\n </ul>\n <span class=\"as-field__hint\">{{ t.origin === 'equipe' ? text.originTeam : text.originUser }}</span>\n </div>\n\n @if (t.merged?.length) {\n <div class=\"as-field\">\n <span class=\"as-field__label\">{{ text.mergedHere }}</span>\n <ul class=\"as-merged\">\n @for (m of t.merged; track m.id) {\n <li class=\"as-merged__item\">\n <button type=\"button\" class=\"as-merged__link\" (click)=\"openTask.emit(m.id)\">\n <span class=\"as-task__key\">{{ m.key }}</span> {{ m.title }}\n </button>\n <span class=\"as-merged__author\">\n <hm-account-avatar size=\"small\" [name]=\"personName(m.author)\" [imageUrl]=\"m.author?.picture\" />\n {{ personName(m.author) }}\n </span>\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"unlink\" [label]=\"unmergeLabel(m.key)\"\n [disabled]=\"busy()\" (clicked)=\"unmergeOrigin(m)\" />\n </li>\n }\n </ul>\n </div>\n }\n\n <hm-select [label]=\"text.priority\" [value]=\"t.priority\" [disabled]=\"busy()\" (valueChange)=\"changePriority($any($event))\">\n @for (p of priorities; track p) { <option [value]=\"p\">{{ text.priorities[p] }}</option> }\n </hm-select>\n <hm-select [label]=\"text.kind\" [value]=\"t.kind\" [disabled]=\"busy()\" (valueChange)=\"changeKind($any($event))\">\n @for (k of kinds; track k) { <option [value]=\"k\">{{ text.kinds[k] }}</option> }\n </hm-select>\n <div class=\"as-field__dates\">\n <hm-date-picker [label]=\"text.startDate\" [value]=\"t.start_date\" [max]=\"$any(t.due_date)\" (valueChange)=\"changeDate('start_date', $event)\" />\n <hm-date-picker [label]=\"text.dueDate\" [value]=\"t.due_date\" [min]=\"$any(t.start_date)\" (valueChange)=\"changeDate('due_date', $event)\" />\n </div>\n @if (t.due_date) {\n <span class=\"as-due\" [attr.data-due]=\"dueSituation()\"><hm-icon shape=\"calendar\" />{{ dueText(t) }}</span>\n }\n\n <div class=\"as-field\">\n <div class=\"as-field__head\">\n <span class=\"as-field__label\">{{ text.labels }}</span>\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"tag\" [label]=\"text.addLabel\" [hmPopoverTrigger]=\"labelPop\" />\n </div>\n <div class=\"as-field__chips\">\n @for (l of t.labels; track l.id) {\n <span class=\"as-label\" [class]=\"'as-tok--' + l.color\">{{ l.name }}</span>\n } @empty {\n <span class=\"muted\">{{ text.noLabels }}</span>\n }\n </div>\n <hm-popover #labelPop [label]=\"text.labels\">\n @if (labelPop.open()) {\n <spine-suggestion-label-picker [labels]=\"labels()\" [selected]=\"selectedLabels()\"\n (toggled)=\"toggleLabel($event.label, $event.linked)\" (created)=\"createLabel($event.name, $event.color)\" />\n }\n </hm-popover>\n </div>\n\n <div class=\"as-field\">\n <div class=\"as-field__head\">\n <span class=\"as-field__label\">{{ text.cover }}</span>\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"color-palette\" [label]=\"text.menuCover\" [hmPopoverTrigger]=\"coverPop\" />\n </div>\n @if (t.cover) {\n <span class=\"as-field__cover\" [class]=\"'as-tok--' + t.cover\" aria-hidden=\"true\"></span>\n } @else {\n <span class=\"muted\">\u2014</span>\n }\n <hm-popover #coverPop [label]=\"text.cover\">\n <spine-suggestion-cover-picker [value]=\"t.cover\" (picked)=\"changeCover($event, coverPop)\" />\n </hm-popover>\n </div>\n\n <dl class=\"as-facts\">\n <dt>{{ text.screenOrigin }}</dt>\n <dd>\n {{ t.screen.label || text.noScreen }}\n @if (t.screen.path && canOpenScreen()) {\n <a class=\"as-facts__link\" [href]=\"screenHref(t.screen.path)\" (click)=\"openScreen($event, t.screen.path)\">\n <hm-icon shape=\"pop-out\" />{{ text.openScreen }}\n </a>\n }\n </dd>\n <dt>{{ text.votesField }}</dt>\n <dd>{{ t.votes }}</dd>\n <dt>{{ text.createdAt }}</dt>\n <dd>{{ date(t.created_at, true) }}</dd>\n <dt>{{ text.updatedAt }}</dt>\n <dd>{{ date(t.updated_at, true) }}</dd>\n </dl>\n @if (lastChange(t); as change) {\n <p class=\"as-field__hint\">{{ change }}</p>\n }\n\n <div class=\"as-task__danger\">\n <!-- Sem cadeia: nem a agrupada nem a que recebeu outras v\u00E3o para dentro de outra. -->\n @if (!t.merged_into && !t.merged_count) {\n <hm-button size=\"small\" emphasis=\"medium\" icon=\"layers\" (clicked)=\"mergeRequested.emit(t)\">{{ text.menuMerge }}</hm-button>\n }\n <hm-button size=\"small\" emphasis=\"medium\" [icon]=\"t.archived_at ? 'unarchive' : 'archive'\" (clicked)=\"toggleArchived()\">\n {{ t.archived_at ? text.menuRestore : text.menuArchive }}\n </hm-button>\n <hm-button size=\"small\" emphasis=\"medium\" intent=\"danger\" icon=\"trash\" (clicked)=\"remove()\">{{ text.menuDelete }}</hm-button>\n </div>\n </aside>\n </div>\n } @else if (draft()) {\n <!-- Nova tarefa: o essencial; o resto (pessoas, checklist, conversa) assim que existir. -->\n <div class=\"as-task\">\n <div class=\"as-task__main\">\n <hm-input [label]=\"text.title\" [placeholder]=\"text.addTaskPlaceholder\" [maxLength]=\"limits.title\" required\n [error]=\"draftTitleError()\" [value]=\"draftTitle()\" (valueChange)=\"draftTitle.set($event)\" />\n <hm-rich-text [label]=\"text.description\" [rows]=\"6\" [maxRows]=\"20\" [value]=\"draftDescription()\" (valueChange)=\"draftDescription.set($event)\" />\n <p class=\"as-field__hint\">{{ text.draftHint }}</p>\n </div>\n <aside class=\"as-task__side\" [attr.aria-label]=\"text.taskLabel\">\n <hm-select [label]=\"text.status\" [value]=\"draftColumn()\" (valueChange)=\"draftColumn.set($any($event))\">\n @for (st of statuses; track st) { <option [value]=\"st\">{{ text.statuses[st] }}</option> }\n </hm-select>\n <hm-select [label]=\"text.kind\" [value]=\"draftKind()\" (valueChange)=\"draftKind.set($any($event))\">\n @for (k of kinds; track k) { <option [value]=\"k\">{{ text.kinds[k] }}</option> }\n </hm-select>\n <hm-select [label]=\"text.priority\" [value]=\"draftPriority()\" (valueChange)=\"draftPriority.set($any($event))\">\n @for (p of priorities; track p) { <option [value]=\"p\">{{ text.priorities[p] }}</option> }\n </hm-select>\n <div class=\"as-field__dates\">\n <hm-date-picker [label]=\"text.startDate\" [value]=\"draftStart()\" [max]=\"$any(draftDue())\" (valueChange)=\"draftStart.set($event)\" />\n <hm-date-picker [label]=\"text.dueDate\" [value]=\"draftDue()\" [min]=\"$any(draftStart())\" (valueChange)=\"draftDue.set($event)\" />\n </div>\n <div class=\"as-field\">\n <span class=\"as-field__label\">{{ text.labels }}</span>\n <spine-suggestion-label-picker [labels]=\"labels()\" [selected]=\"draftLabels()\"\n (toggled)=\"toggleLabel($event.label, $event.linked)\" (created)=\"createLabel($event.name, $event.color)\" />\n </div>\n <hm-checkbox [label]=\"text.assignMeOnCreate\" [checked]=\"draftAssignMe()\" (checkedChange)=\"draftAssignMe.set($event)\" />\n </aside>\n </div>\n }\n\n <div footer class=\"as-task__footer\">\n @if (!current() && draft()) {\n <hm-button emphasis=\"medium\" (clicked)=\"closed.emit()\">{{ text.cancel }}</hm-button>\n <hm-button icon=\"plus\" [loading]=\"busy()\" (clicked)=\"create()\">{{ text.createTask }}</hm-button>\n } @else {\n <hm-button emphasis=\"medium\" (clicked)=\"closed.emit()\">{{ text.close }}</hm-button>\n }\n </div>\n</hm-modal>\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}.as-tok--cat-1{--as-tok: var(--hm-data-categorical-1);background:var(--as-tok)}.as-tok--cat-2{--as-tok: var(--hm-data-categorical-2);background:var(--as-tok)}.as-tok--cat-3{--as-tok: var(--hm-data-categorical-3);background:var(--as-tok)}.as-tok--cat-4{--as-tok: var(--hm-data-categorical-4);background:var(--as-tok)}.as-tok--cat-5{--as-tok: var(--hm-data-categorical-5);background:var(--as-tok)}.as-tok--cat-6{--as-tok: var(--hm-data-categorical-6);background:var(--as-tok)}.as-tok--cat-7{--as-tok: var(--hm-data-categorical-7);background:var(--as-tok)}.as-tok--cat-8{--as-tok: var(--hm-data-categorical-8);background:var(--as-tok)}.as-tok--grad-1{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-2))}.as-tok--grad-2{--as-tok: var(--hm-data-categorical-3);background:linear-gradient(135deg,var(--hm-data-categorical-3),var(--hm-data-categorical-4))}.as-tok--grad-3{--as-tok: var(--hm-data-categorical-5);background:linear-gradient(135deg,var(--hm-data-categorical-5),var(--hm-data-categorical-6))}.as-tok--grad-4{--as-tok: var(--hm-data-categorical-7);background:linear-gradient(135deg,var(--hm-data-categorical-7),var(--hm-data-categorical-8))}.as-tok--grad-5{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-5))}.as-tok--grad-6{--as-tok: var(--hm-data-categorical-2);background:linear-gradient(135deg,var(--hm-data-categorical-2),var(--hm-data-categorical-6))}.as-label{display:inline-flex;align-items:center;gap:var(--hm-space-micro);max-inline-size:100%;padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:color-mix(in srgb,var(--as-tok, var(--hm-border)) 18%,var(--hm-surface-raised));color:var(--hm-text);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);line-height:1.6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.as-label:before{flex:none;inline-size:.5rem;block-size:.5rem;border-radius:50%;background:var(--as-tok, var(--hm-border));content:\"\"}@media(forced-colors:active){.as-label{border:var(--hm-border-hairline) solid CanvasText}}.as-task{display:grid;grid-template-columns:minmax(0,1fr) minmax(15rem,20rem);gap:var(--hm-space-lg);align-items:start}@media(max-width:48rem){.as-task{grid-template-columns:minmax(0,1fr)}}.as-task__cover{block-size:3rem;margin-block-end:var(--hm-space-sm);border-radius:var(--hm-radius-control)}.as-task__main{display:flex;flex-direction:column;gap:var(--hm-space-md);min-inline-size:0}.as-task__key-row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.as-task__key{color:var(--hm-muted);font-family:var(--hm-font-mono, monospace);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.as-task__title{margin:0;font-size:var(--hm-font-size-xl, 1.375rem)}.as-task__title-button{display:inline-flex;align-items:baseline;gap:var(--hm-space-xs);padding:0;border:0;background:none;color:var(--hm-text);font:inherit;font-weight:var(--hm-font-weight-bold);text-align:start;overflow-wrap:anywhere;cursor:text}.as-task__title-button:hover .as-task__title-pen,.as-task__title-button:focus-visible .as-task__title-pen{opacity:1}.as-task__title-button:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:2px}.as-task__title-pen{color:var(--hm-muted);opacity:0;font-size:var(--hm-font-size-md)}.as-task__section{display:flex;flex-direction:column;gap:var(--hm-space-xs);padding:var(--hm-space-sm);border-radius:var(--hm-radius-control);background:var(--hm-surface-muted)}.as-task__section-head{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs)}.as-task__heading{display:flex;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-task__count{color:var(--hm-muted);font-size:var(--hm-font-size-sm);font-variant-numeric:tabular-nums}.as-task__progress{block-size:.375rem;border-radius:var(--hm-radius-pill);background:var(--hm-border);overflow:hidden}.as-task__progress span{display:block;block-size:100%;background:var(--hm-success)}.as-task__reply{display:flex;flex-direction:column;gap:var(--hm-space-xs);margin-block:var(--hm-space-sm)}.as-task__reply-row{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs)}.as-task__side{display:flex;flex-direction:column;gap:var(--hm-space-sm);padding:var(--hm-space-sm);border-radius:var(--hm-radius-control);background:var(--hm-surface-muted)}.as-task__danger{display:flex;flex-wrap:wrap;gap:var(--hm-space-xs);padding-block-start:var(--hm-space-xs);border-block-start:var(--hm-border-hairline) solid var(--hm-border)}.as-task__footer{display:flex;justify-content:flex-end;gap:var(--hm-space-xs)}.as-field{display:flex;flex-direction:column;gap:var(--hm-space-micro)}.as-field__head{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-xs)}.as-field__label{color:var(--hm-muted);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.as-field__hint{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-field__chips{display:flex;flex-wrap:wrap;gap:var(--hm-space-micro)}.as-field__dates{display:grid;grid-template-columns:1fr 1fr;gap:var(--hm-space-xs)}.as-field__cover{display:block;block-size:1.5rem;border-radius:var(--hm-radius-control)}.as-people{display:flex;flex-direction:column;gap:var(--hm-space-micro);margin:0;padding:0;list-style:none}.as-people__item{display:flex;align-items:center;gap:var(--hm-space-xs);min-inline-size:0}.as-people__name{flex:1;min-inline-size:0;overflow:hidden;color:var(--hm-text);font-size:var(--hm-font-size-sm);text-overflow:ellipsis;white-space:nowrap}.as-task__merged{display:flex;flex-wrap:wrap;align-items:flex-start;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-xs) var(--hm-space-sm);border-inline-start:var(--hm-border-thin) solid var(--hm-info);border-radius:var(--hm-radius-control);background:var(--hm-info-bg);color:var(--hm-text)}.as-task__merged-icon{flex:none;color:var(--hm-info)}.as-task__merged-text{display:flex;flex:1 1 12rem;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0;overflow-wrap:anywhere}.as-merged{display:flex;flex-direction:column;gap:var(--hm-space-micro);margin:0;padding:0;list-style:none}.as-merged__item{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:var(--hm-space-micro) var(--hm-space-xs);padding:var(--hm-space-xxxs) var(--hm-space-xs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised)}.as-merged__item hm-icon-button{grid-row:1/span 2;grid-column:2}.as-merged__link{padding:0;border:0;background:none;color:var(--hm-accent);font:inherit;font-size:var(--hm-font-size-sm);text-align:start;overflow-wrap:anywhere;cursor:pointer}.as-merged__link:hover{text-decoration:underline}.as-merged__link:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:2px}.as-merged__author{display:inline-flex;align-items:center;gap:var(--hm-space-micro);color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-due{display:inline-flex;align-items:center;gap:var(--hm-space-micro);color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.as-due[data-due=overdue]{color:var(--hm-danger);font-weight:var(--hm-font-weight-semibold)}.as-due[data-due=today],.as-due[data-due=tomorrow]{color:var(--hm-text);font-weight:var(--hm-font-weight-semibold)}.as-facts{display:grid;grid-template-columns:max-content 1fr;gap:var(--hm-space-xxxs) var(--hm-space-sm);margin:0;font-size:var(--hm-font-size-sm)}.as-facts dt{color:var(--hm-muted)}.as-facts dd{margin:0;color:var(--hm-text);overflow-wrap:anywhere}.as-facts__link{display:inline-flex;align-items:center;gap:var(--hm-space-micro);margin-inline-start:var(--hm-space-xxxs);color:var(--hm-accent)}.as-checklist{display:flex;flex-direction:column;gap:var(--hm-space-micro);margin:0;padding:0;list-style:none}.as-checklist__item{display:flex;align-items:center;gap:var(--hm-space-xs);min-block-size:var(--hm-control-height-dense)}.as-checklist__item--done .as-checklist__check{color:var(--hm-muted);text-decoration:line-through}.as-checklist__item:hover .as-checklist__tools,.as-checklist__item:focus-within .as-checklist__tools{opacity:1}.as-checklist__check{flex:1;min-inline-size:0}.as-checklist__rename{flex:1}.as-checklist__tools{display:inline-flex;opacity:0}.as-checklist__add{display:flex;gap:var(--hm-space-xs);align-items:flex-start}.as-checklist__new{flex:1}@media(hover:none){.as-checklist__tools{opacity:1}}.as-timeline{display:flex;flex-direction:column;gap:var(--hm-space-sm);margin:var(--hm-space-sm) 0 0;padding:0;list-style:none}.as-timeline__item{position:relative;display:flex;align-items:flex-start;gap:var(--hm-space-xs);font-size:var(--hm-font-size-sm)}.as-timeline__item:not(:last-child):before{position:absolute;inset-block:var(--hm-space-lg) calc(var(--hm-space-sm) * -1);inset-inline-start:calc(var(--hm-space-sm) - var(--hm-border-hairline));border-inline-start:var(--hm-border-thin) solid var(--hm-border);content:\"\"}.as-timeline__face{position:relative;flex:none}.as-timeline__body{display:flex;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.as-timeline__line{display:inline-flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-micro);color:var(--hm-text)}.as-timeline__comment{margin:0;padding:var(--hm-space-xs) var(--hm-space-sm);border-radius:var(--hm-radius-control);background:var(--hm-surface-muted);color:var(--hm-text);white-space:pre-line;overflow-wrap:anywhere}.as-timeline__comment--internal{border-inline-start:var(--hm-border-thin) solid var(--hm-warning);background:var(--hm-warning-bg)}\n"] }]
|
|
1083
|
+
}], ctorParameters: () => [], propDecorators: { clientId: [{ type: i0.Input, args: [{ isSignal: true, alias: "clientId", required: true }] }], task: [{ type: i0.Input, args: [{ isSignal: true, alias: "task", required: false }] }], draft: [{ type: i0.Input, args: [{ isSignal: true, alias: "draft", required: false }] }], draftStatus: [{ type: i0.Input, args: [{ isSignal: true, alias: "draftStatus", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], canOpenScreen: [{ type: i0.Input, args: [{ isSignal: true, alias: "canOpenScreen", required: false }] }], changed: [{ type: i0.Output, args: ["changed"] }], created: [{ type: i0.Output, args: ["created"] }], removed: [{ type: i0.Output, args: ["removed"] }], closed: [{ type: i0.Output, args: ["closed"] }], labelCreated: [{ type: i0.Output, args: ["labelCreated"] }], mergeRequested: [{ type: i0.Output, args: ["mergeRequested"] }], openTask: [{ type: i0.Output, args: ["openTask"] }], unmerged: [{ type: i0.Output, args: ["unmerged"] }] } });
|
|
1084
|
+
|
|
1085
|
+
/** Sem acento e sem caixa: "relatório" acha "RELATORIO". */
|
|
1086
|
+
const fold$1 = (s) => s.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
|
|
1087
|
+
/** Teto da lista do seletor: a busca afunila o resto. */
|
|
1088
|
+
const MAX_OPTIONS = 50;
|
|
1089
|
+
/**
|
|
1090
|
+
* "Agrupar em…": escolhe a sugestão principal para uma repetida (`source`), entre as abertas do quadro (`candidates`,
|
|
1091
|
+
* menos ela e as já agrupadas), com busca pela chave e pelo título. Escolher pede a prévia ao servidor ("3 apoios e 1
|
|
1092
|
+
* relator passam para SUG-12"); "Agrupar" confirma (`HermesDialogService.confirm`) e grava. Uma sugestão que já recebeu
|
|
1093
|
+
* outras não pode ser agrupada (sem cadeia): o seletor avisa em vez de listar.
|
|
1094
|
+
*/
|
|
1095
|
+
class SuggestionMergePickerComponent {
|
|
1096
|
+
constructor() {
|
|
1097
|
+
this.messages = inject(SPINE_MESSAGES);
|
|
1098
|
+
this.text = this.messages.suggestions;
|
|
1099
|
+
this.service = inject(AutoServiceSuggestionsService);
|
|
1100
|
+
this.dialogs = inject(HermesDialogService);
|
|
1101
|
+
this.toast = inject(HermesToastService);
|
|
1102
|
+
this.clientId = input.required(...(ngDevMode ? [{ debugName: "clientId" }] : /* istanbul ignore next */ []));
|
|
1103
|
+
/** A que vai ser agrupada; null = fechado. */
|
|
1104
|
+
this.source = input(null, ...(ngDevMode ? [{ debugName: "source" }] : /* istanbul ignore next */ []));
|
|
1105
|
+
/** As do quadro (o seletor tira a própria e as agrupadas). */
|
|
1106
|
+
this.candidates = input([], ...(ngDevMode ? [{ debugName: "candidates" }] : /* istanbul ignore next */ []));
|
|
1107
|
+
/** Agrupou: o destino inteiro, como o servidor devolveu. */
|
|
1108
|
+
this.merged = output();
|
|
1109
|
+
this.closed = output();
|
|
1110
|
+
this.statusColors = SPINE_SUGGESTION_STATUS_COLORS;
|
|
1111
|
+
this.term = signal('', ...(ngDevMode ? [{ debugName: "term" }] : /* istanbul ignore next */ []));
|
|
1112
|
+
this.picked = signal(null, ...(ngDevMode ? [{ debugName: "picked" }] : /* istanbul ignore next */ []));
|
|
1113
|
+
this.preview = signal(null, ...(ngDevMode ? [{ debugName: "preview" }] : /* istanbul ignore next */ []));
|
|
1114
|
+
this.previewError = signal(false, ...(ngDevMode ? [{ debugName: "previewError" }] : /* istanbul ignore next */ []));
|
|
1115
|
+
this.busy = signal(false, ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
|
|
1116
|
+
this.heading = computed(() => formatMessage(this.text.mergeHeading, { key: this.source()?.key ?? '' }), ...(ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []));
|
|
1117
|
+
this.options = computed(() => {
|
|
1118
|
+
const source = this.source();
|
|
1119
|
+
const term = fold$1(this.term().trim());
|
|
1120
|
+
return this.candidates()
|
|
1121
|
+
.filter((c) => c.id !== source?.id && !c.merged_into)
|
|
1122
|
+
.filter((c) => !term || fold$1(`${c.key} ${c.title}`).includes(term))
|
|
1123
|
+
.slice(0, MAX_OPTIONS);
|
|
1124
|
+
}, ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
|
|
1125
|
+
// Cada abertura começa do zero.
|
|
1126
|
+
effect(() => {
|
|
1127
|
+
this.source();
|
|
1128
|
+
untracked(() => {
|
|
1129
|
+
this.term.set('');
|
|
1130
|
+
this.picked.set(null);
|
|
1131
|
+
this.preview.set(null);
|
|
1132
|
+
this.previewError.set(false);
|
|
1133
|
+
});
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
onOpen(open) {
|
|
1137
|
+
if (!open)
|
|
1138
|
+
this.closed.emit();
|
|
1139
|
+
}
|
|
1140
|
+
async pick(target) {
|
|
1141
|
+
const source = this.source();
|
|
1142
|
+
if (!source || this.picked()?.id === target.id)
|
|
1143
|
+
return;
|
|
1144
|
+
this.picked.set(target);
|
|
1145
|
+
this.preview.set(null);
|
|
1146
|
+
this.previewError.set(false);
|
|
1147
|
+
try {
|
|
1148
|
+
const preview = await this.service.mergePreview(this.clientId(), source.id, target.id);
|
|
1149
|
+
if (this.picked()?.id === target.id)
|
|
1150
|
+
this.preview.set(preview);
|
|
1151
|
+
}
|
|
1152
|
+
catch {
|
|
1153
|
+
if (this.picked()?.id === target.id)
|
|
1154
|
+
this.previewError.set(true);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
previewText(p) {
|
|
1158
|
+
const locale = this.messages.meta.locale;
|
|
1159
|
+
return formatMessage(this.text.mergePreview, {
|
|
1160
|
+
votes: pluralMessage(this.text.votes, p.votes, locale),
|
|
1161
|
+
reporters: pluralMessage(this.text.reportersCount, p.reporters, locale),
|
|
1162
|
+
key: p.target.key,
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
votesText(count) {
|
|
1166
|
+
return pluralMessage(this.text.votes, count, this.messages.meta.locale);
|
|
1167
|
+
}
|
|
1168
|
+
async confirm() {
|
|
1169
|
+
const source = this.source();
|
|
1170
|
+
const preview = this.preview();
|
|
1171
|
+
const target = this.picked();
|
|
1172
|
+
if (!source || !preview || !target || this.busy())
|
|
1173
|
+
return;
|
|
1174
|
+
const ok = await this.dialogs.confirm({
|
|
1175
|
+
heading: formatMessage(this.text.mergeConfirmHeading, { key: source.key, target: target.key }),
|
|
1176
|
+
message: `${this.previewText(preview)} ${this.text.mergeConfirmMessage}`,
|
|
1177
|
+
confirmLabel: this.text.mergeConfirm,
|
|
1178
|
+
});
|
|
1179
|
+
if (!ok)
|
|
1180
|
+
return;
|
|
1181
|
+
this.busy.set(true);
|
|
1182
|
+
try {
|
|
1183
|
+
const saved = await this.service.merge(this.clientId(), source.id, target.id);
|
|
1184
|
+
this.toast.success(formatMessage(this.text.mergedToast, { key: source.key, target: target.key }));
|
|
1185
|
+
this.merged.emit({ source, target: saved });
|
|
1186
|
+
}
|
|
1187
|
+
catch {
|
|
1188
|
+
this.toast.error(this.text.mergeError);
|
|
1189
|
+
}
|
|
1190
|
+
finally {
|
|
1191
|
+
this.busy.set(false);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionMergePickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1195
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SuggestionMergePickerComponent, isStandalone: true, selector: "spine-suggestion-merge-picker", inputs: { clientId: { classPropertyName: "clientId", publicName: "clientId", isSignal: true, isRequired: true, transformFunction: null }, source: { classPropertyName: "source", publicName: "source", isSignal: true, isRequired: false, transformFunction: null }, candidates: { classPropertyName: "candidates", publicName: "candidates", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { merged: "merged", closed: "closed" }, ngImport: i0, template: `
|
|
1196
|
+
<hm-modal [heading]="heading()" [open]="!!source()" (openChange)="onOpen($event)">
|
|
1197
|
+
@if (source(); as s) {
|
|
1198
|
+
<div class="as-merge">
|
|
1199
|
+
<p class="as-merge__intro">{{ text.mergeIntro }}</p>
|
|
1200
|
+
@if (s.merged_count) {
|
|
1201
|
+
<hm-alert variant="warning">{{ text.mergeHasMerged }}</hm-alert>
|
|
1202
|
+
} @else {
|
|
1203
|
+
<hm-search-field [label]="text.mergeSearch" labelHidden [placeholder]="text.mergeSearch" [landmark]="false"
|
|
1204
|
+
[value]="term()" (search)="term.set($event)" />
|
|
1205
|
+
<ul class="as-merge__list" [attr.aria-label]="text.mergeSearch">
|
|
1206
|
+
@for (c of options(); track c.id) {
|
|
1207
|
+
<li>
|
|
1208
|
+
<button type="button" class="as-merge__option" [class.as-merge__option--on]="picked()?.id === c.id"
|
|
1209
|
+
[attr.aria-pressed]="picked()?.id === c.id" (click)="pick(c)">
|
|
1210
|
+
<span class="as-merge__key">{{ c.key }}</span>
|
|
1211
|
+
<span class="as-merge__title">{{ c.title }}</span>
|
|
1212
|
+
<span class="as-merge__meta">
|
|
1213
|
+
<hm-tag [name]="text.statuses[c.status]" [color]="statusColors[c.status]" />
|
|
1214
|
+
<span>{{ votesText(c.votes) }}</span>
|
|
1215
|
+
</span>
|
|
1216
|
+
</button>
|
|
1217
|
+
</li>
|
|
1218
|
+
} @empty {
|
|
1219
|
+
<li class="as-merge__empty">{{ text.mergeNone }}</li>
|
|
1220
|
+
}
|
|
1221
|
+
</ul>
|
|
1222
|
+
@if (picked()) {
|
|
1223
|
+
<p class="as-merge__preview" aria-live="polite">
|
|
1224
|
+
@if (previewError()) { {{ text.mergePreviewError }} }
|
|
1225
|
+
@else if (preview(); as p) { {{ previewText(p) }} }
|
|
1226
|
+
@else { {{ text.mergeCalculating }} }
|
|
1227
|
+
</p>
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
</div>
|
|
1231
|
+
}
|
|
1232
|
+
<div footer class="as-merge__footer">
|
|
1233
|
+
<hm-button emphasis="medium" (clicked)="closed.emit()">{{ text.cancel }}</hm-button>
|
|
1234
|
+
<hm-button icon="layers" [disabled]="!preview() || !!source()?.merged_count" [loading]="busy()" (clicked)="confirm()">
|
|
1235
|
+
{{ text.mergeConfirm }}
|
|
1236
|
+
</hm-button>
|
|
1237
|
+
</div>
|
|
1238
|
+
</hm-modal>
|
|
1239
|
+
`, isInline: true, styles: [".as-merge{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-inline-size:0}.as-merge__intro,.as-merge__empty{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.as-merge__list{display:flex;flex-direction:column;gap:var(--hm-space-xxxs);max-block-size:22rem;margin:0;padding:0;overflow:auto;list-style:none}.as-merge__option{display:grid;grid-template-columns:auto 1fr;gap:var(--hm-space-micro) var(--hm-space-xs);inline-size:100%;padding:var(--hm-space-xs) var(--hm-space-sm);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised);color:var(--hm-text);font:inherit;text-align:start;cursor:pointer}.as-merge__option:hover{background:var(--hm-surface-muted)}.as-merge__option:focus-visible{outline:var(--hm-border-thin) solid var(--hm-accent);outline-offset:var(--hm-border-thin)}.as-merge__option--on{border-color:var(--hm-accent);background:var(--hm-accent-subtle)}.as-merge__key{color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold)}.as-merge__title{font-weight:var(--hm-font-weight-semibold);overflow-wrap:anywhere}.as-merge__meta{display:inline-flex;grid-column:2;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs);color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.as-merge__preview{margin:0;padding:var(--hm-space-xs) var(--hm-space-sm);border-radius:var(--hm-radius-control);background:var(--hm-surface-muted);color:var(--hm-text)}.as-merge__footer{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs);inline-size:100%}\n"], dependencies: [{ 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: ModalComponent, selector: "hm-modal", inputs: ["open", "heading", "size", "closeLabel", "closable", "beforeClose"], outputs: ["openChange", "closed"] }, { 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: TagComponent, selector: "hm-tag", inputs: ["name", "color", "compact"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1240
|
+
}
|
|
1241
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionMergePickerComponent, decorators: [{
|
|
1242
|
+
type: Component,
|
|
1243
|
+
args: [{ selector: 'spine-suggestion-merge-picker', imports: [AlertComponent, ButtonComponent, ModalComponent, SearchFieldComponent, TagComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
1244
|
+
<hm-modal [heading]="heading()" [open]="!!source()" (openChange)="onOpen($event)">
|
|
1245
|
+
@if (source(); as s) {
|
|
1246
|
+
<div class="as-merge">
|
|
1247
|
+
<p class="as-merge__intro">{{ text.mergeIntro }}</p>
|
|
1248
|
+
@if (s.merged_count) {
|
|
1249
|
+
<hm-alert variant="warning">{{ text.mergeHasMerged }}</hm-alert>
|
|
1250
|
+
} @else {
|
|
1251
|
+
<hm-search-field [label]="text.mergeSearch" labelHidden [placeholder]="text.mergeSearch" [landmark]="false"
|
|
1252
|
+
[value]="term()" (search)="term.set($event)" />
|
|
1253
|
+
<ul class="as-merge__list" [attr.aria-label]="text.mergeSearch">
|
|
1254
|
+
@for (c of options(); track c.id) {
|
|
1255
|
+
<li>
|
|
1256
|
+
<button type="button" class="as-merge__option" [class.as-merge__option--on]="picked()?.id === c.id"
|
|
1257
|
+
[attr.aria-pressed]="picked()?.id === c.id" (click)="pick(c)">
|
|
1258
|
+
<span class="as-merge__key">{{ c.key }}</span>
|
|
1259
|
+
<span class="as-merge__title">{{ c.title }}</span>
|
|
1260
|
+
<span class="as-merge__meta">
|
|
1261
|
+
<hm-tag [name]="text.statuses[c.status]" [color]="statusColors[c.status]" />
|
|
1262
|
+
<span>{{ votesText(c.votes) }}</span>
|
|
1263
|
+
</span>
|
|
1264
|
+
</button>
|
|
1265
|
+
</li>
|
|
1266
|
+
} @empty {
|
|
1267
|
+
<li class="as-merge__empty">{{ text.mergeNone }}</li>
|
|
1268
|
+
}
|
|
1269
|
+
</ul>
|
|
1270
|
+
@if (picked()) {
|
|
1271
|
+
<p class="as-merge__preview" aria-live="polite">
|
|
1272
|
+
@if (previewError()) { {{ text.mergePreviewError }} }
|
|
1273
|
+
@else if (preview(); as p) { {{ previewText(p) }} }
|
|
1274
|
+
@else { {{ text.mergeCalculating }} }
|
|
1275
|
+
</p>
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
</div>
|
|
1279
|
+
}
|
|
1280
|
+
<div footer class="as-merge__footer">
|
|
1281
|
+
<hm-button emphasis="medium" (clicked)="closed.emit()">{{ text.cancel }}</hm-button>
|
|
1282
|
+
<hm-button icon="layers" [disabled]="!preview() || !!source()?.merged_count" [loading]="busy()" (clicked)="confirm()">
|
|
1283
|
+
{{ text.mergeConfirm }}
|
|
1284
|
+
</hm-button>
|
|
1285
|
+
</div>
|
|
1286
|
+
</hm-modal>
|
|
1287
|
+
`, styles: [".as-merge{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-inline-size:0}.as-merge__intro,.as-merge__empty{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.as-merge__list{display:flex;flex-direction:column;gap:var(--hm-space-xxxs);max-block-size:22rem;margin:0;padding:0;overflow:auto;list-style:none}.as-merge__option{display:grid;grid-template-columns:auto 1fr;gap:var(--hm-space-micro) var(--hm-space-xs);inline-size:100%;padding:var(--hm-space-xs) var(--hm-space-sm);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised);color:var(--hm-text);font:inherit;text-align:start;cursor:pointer}.as-merge__option:hover{background:var(--hm-surface-muted)}.as-merge__option:focus-visible{outline:var(--hm-border-thin) solid var(--hm-accent);outline-offset:var(--hm-border-thin)}.as-merge__option--on{border-color:var(--hm-accent);background:var(--hm-accent-subtle)}.as-merge__key{color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold)}.as-merge__title{font-weight:var(--hm-font-weight-semibold);overflow-wrap:anywhere}.as-merge__meta{display:inline-flex;grid-column:2;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs);color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.as-merge__preview{margin:0;padding:var(--hm-space-xs) var(--hm-space-sm);border-radius:var(--hm-radius-control);background:var(--hm-surface-muted);color:var(--hm-text)}.as-merge__footer{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs);inline-size:100%}\n"] }]
|
|
1288
|
+
}], ctorParameters: () => [], propDecorators: { clientId: [{ type: i0.Input, args: [{ isSignal: true, alias: "clientId", required: true }] }], source: [{ type: i0.Input, args: [{ isSignal: true, alias: "source", required: false }] }], candidates: [{ type: i0.Input, args: [{ isSignal: true, alias: "candidates", required: false }] }], merged: [{ type: i0.Output, args: ["merged"] }], closed: [{ type: i0.Output, args: ["closed"] }] } });
|
|
1289
|
+
|
|
1290
|
+
/** As colunas do quadro: recusada não tem coluna (sai na hora; fica no arquivo). */
|
|
1291
|
+
const BOARD_STATUSES = ['em_analise', 'planejada', 'em_desenvolvimento', 'concluida'];
|
|
1292
|
+
const OPEN_STATUSES = ['em_analise', 'planejada', 'em_desenvolvimento'];
|
|
1293
|
+
/** Quantas levam o selo "Mais votada" (das com algum apoio, no quadro inteiro, não só no filtrado). */
|
|
1294
|
+
const TOP_VOTED = 3;
|
|
1295
|
+
/** Responsáveis que aparecem no cartão; o resto vira "+N". */
|
|
1296
|
+
const FACES = 3;
|
|
1297
|
+
const ARCHIVE_PAGE = 20;
|
|
1298
|
+
const EXCERPT = 140;
|
|
1299
|
+
/** Parâmetro do link copiado ("?sugestao=SUG-12"): abrir a tela com ele abre a tarefa. */
|
|
1300
|
+
const SUGGESTION_LINK_PARAM = 'sugestao';
|
|
1301
|
+
const PRIORITY_RANK = { urgente: 0, alta: 1, media: 2, baixa: 3 };
|
|
1302
|
+
const byPosition = (a, b) => a.position - b.position || a.created_at.localeCompare(b.created_at);
|
|
1303
|
+
const SORTS = {
|
|
1304
|
+
position: byPosition,
|
|
1305
|
+
votes: (a, b) => b.votes - a.votes || byPosition(a, b),
|
|
1306
|
+
recent: (a, b) => b.created_at.localeCompare(a.created_at),
|
|
1307
|
+
priority: (a, b) => PRIORITY_RANK[a.priority] - PRIORITY_RANK[b.priority] || byPosition(a, b),
|
|
1308
|
+
due: (a, b) => (a.due_date ?? '9999').localeCompare(b.due_date ?? '9999') || byPosition(a, b),
|
|
1309
|
+
};
|
|
1310
|
+
/** Sem acento e minúsculo: "analise" acha "Análise". */
|
|
1311
|
+
const fold = (s) => s.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
|
|
1312
|
+
/** Intervalos do "há 3 dias" (segundos por unidade), do maior para o menor. */
|
|
1313
|
+
const AGES = [
|
|
1314
|
+
['year', 31_536_000], ['month', 2_592_000], ['week', 604_800], ['day', 86_400], ['hour', 3_600], ['minute', 60],
|
|
1315
|
+
];
|
|
1316
|
+
/**
|
|
1317
|
+
* O quadro de trabalho das sugestões do app (`hm-kanban`), montado na tela Sugestões do auto-serviço.
|
|
1318
|
+
*
|
|
1319
|
+
* - **Quadro**: o servidor manda o recorte (`board=true`): abertas e as concluídas dos últimos `done_days` dias;
|
|
1320
|
+
* recusadas e arquivadas saem. Indicadores do quadro inteiro, tudo calculado aqui com o que já vem (vencidas e as
|
|
1321
|
+
* que venceram ontem, vencendo em breve, em andamento por status, concluídas na janela e hoje, atribuídas a mim e as
|
|
1322
|
+
* abertas por prioridade; vencidas, vencendo, minhas e cada prioridade também filtram) e os filtros da tela — texto
|
|
1323
|
+
* (inclusive a chave "SUG-12"), responsável ("minhas", "sem responsável"), tipo, status (só aquela coluna),
|
|
1324
|
+
* prioridade, rótulo, prazo, e em "Mais filtros" tela e quem pediu, além da ordenação — sobre a lista inteira.
|
|
1325
|
+
* - **Coluna**: "Mais antiga há 4h 12min" (pelo `status_at`) e as vencidas abaixo do título, vazio explicado e
|
|
1326
|
+
* "+ Criar tarefa" no rodapé (os templates `hmKanbanColumnSummary`/`Empty`/`Footer` do `hm-kanban`).
|
|
1327
|
+
* - **Cartão**: capa (token da paleta), chave, selo de prioridade, título, contexto (tipo · tela), rótulos, trecho,
|
|
1328
|
+
* checklist (e os itens, com "Mostrar no cartão"), prazo por extenso (vencida em vermelho; hoje/amanhã em âmbar) e o
|
|
1329
|
+
* rodapé com comentários, checklist, apoios, a data e os responsáveis com "+N"; o "⋮": status e prioridade
|
|
1330
|
+
* (submenus), atribuir a mim, copiar link e chave, rótulos, capa (popover), arquivar e excluir (com confirmação).
|
|
1331
|
+
* - **Criar**: sempre o cadastro completo (a tarefa em branco no modal), já com o status da coluna: "Nova tarefa" no
|
|
1332
|
+
* topo (o "▾" escolhe a coluna), o "+" do cabeçalho e o "+ Criar tarefa" do rodapé de cada coluna.
|
|
1333
|
+
* - **Arrastar** (ponteiro, toque ou teclado; o `hm-kanban` cuida do gesto e dos anúncios) muda status e/ou posição: a
|
|
1334
|
+
* tela aplica na hora e grava; se o servidor recusar, avisa e relê. A posição gravada é sempre a da coluna INTEIRA
|
|
1335
|
+
* (pelo vizinho visível). Fora da "ordem do quadro", arrastar só troca de coluna (vai para o fim).
|
|
1336
|
+
* - **Arquivo**: arquivadas, recusadas, agrupadas (selo "Agrupada", com "Desagrupar") e concluídas antigas, paginado e
|
|
1337
|
+
* buscado no servidor, com "Reabrir"/"Restaurar".
|
|
1338
|
+
* - **Agrupar**: "Agrupar em…" no "⋯" do cartão e no detalhe abre o seletor (`spine-suggestion-merge-picker`) com as
|
|
1339
|
+
* abertas do quadro e a prévia do servidor; confirmar agrupa, tira a origem do quadro e abre o destino.
|
|
1340
|
+
* - **Tarefa**: `spine-suggestion-task-detail` (modal de duas colunas); o link "?sugestao=SUG-12" abre direto nela.
|
|
1341
|
+
*/
|
|
1342
|
+
class AutoServiceSuggestionsComponent {
|
|
1343
|
+
constructor() {
|
|
1344
|
+
this.messages = inject(SPINE_MESSAGES);
|
|
1345
|
+
this.text = this.messages.suggestions;
|
|
1346
|
+
this.service = inject(AutoServiceSuggestionsService);
|
|
1347
|
+
this.toast = inject(HermesToastService);
|
|
1348
|
+
this.dialogs = inject(HermesDialogService);
|
|
1349
|
+
this.preferences = inject(SpinePreferencesService);
|
|
1350
|
+
this.userStorage = inject(SpineUserStorage, { optional: true });
|
|
1351
|
+
this.router = inject(Router, { optional: true });
|
|
1352
|
+
this.location = inject(Location, { optional: true });
|
|
1353
|
+
/** O client da sessão: a tela da sugestão só abre daqui se o app administrado for este. */
|
|
1354
|
+
this.sessionClientId = inject(AUTH_CONFIG, { optional: true })?.clientId ?? null;
|
|
1355
|
+
this.clientId = input.required(...(ngDevMode ? [{ debugName: "clientId" }] : /* istanbul ignore next */ []));
|
|
1356
|
+
this.kinds = SPINE_SUGGESTION_KINDS;
|
|
1357
|
+
this.statuses = SPINE_SUGGESTION_STATUSES;
|
|
1358
|
+
this.statusColors = SPINE_SUGGESTION_STATUS_COLORS;
|
|
1359
|
+
this.statusIcons = SPINE_SUGGESTION_STATUS_ICONS;
|
|
1360
|
+
this.kindColors = SPINE_SUGGESTION_KIND_COLORS;
|
|
1361
|
+
this.priorities = TASK_PRIORITIES;
|
|
1362
|
+
this.sortKeys = ['position', 'votes', 'recent', 'priority', 'due'];
|
|
1363
|
+
this.today = todayIso(this.preferences.effectiveTimeZone());
|
|
1364
|
+
this.mode = signal('board', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
|
|
1365
|
+
this.modeOptions = [
|
|
1366
|
+
{ value: 'board', label: this.text.viewBoard, icon: 'view-columns' },
|
|
1367
|
+
{ value: 'archive', label: this.text.viewArchive, icon: 'archive' },
|
|
1368
|
+
];
|
|
1369
|
+
// --- quadro ---
|
|
1370
|
+
this.all = signal([], ...(ngDevMode ? [{ debugName: "all" }] : /* istanbul ignore next */ []));
|
|
1371
|
+
this.catalog = signal([], ...(ngDevMode ? [{ debugName: "catalog" }] : /* istanbul ignore next */ []));
|
|
1372
|
+
this.doneDays = signal(AUTO_SERVICE_SUGGESTION_DONE_DAYS, ...(ngDevMode ? [{ debugName: "doneDays" }] : /* istanbul ignore next */ []));
|
|
1373
|
+
this.loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
1374
|
+
this.loadError = signal('', ...(ngDevMode ? [{ debugName: "loadError" }] : /* istanbul ignore next */ []));
|
|
1375
|
+
this.search = signal('', ...(ngDevMode ? [{ debugName: "search" }] : /* istanbul ignore next */ []));
|
|
1376
|
+
this.kind = signal('', ...(ngDevMode ? [{ debugName: "kind" }] : /* istanbul ignore next */ []));
|
|
1377
|
+
this.screen = signal('', ...(ngDevMode ? [{ debugName: "screen" }] : /* istanbul ignore next */ []));
|
|
1378
|
+
this.author = signal('', ...(ngDevMode ? [{ debugName: "author" }] : /* istanbul ignore next */ []));
|
|
1379
|
+
/** '' | 'me' | 'none' | user_id */
|
|
1380
|
+
this.assignee = signal('', ...(ngDevMode ? [{ debugName: "assignee" }] : /* istanbul ignore next */ []));
|
|
1381
|
+
this.label = signal('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
1382
|
+
this.priority = signal('', ...(ngDevMode ? [{ debugName: "priority" }] : /* istanbul ignore next */ []));
|
|
1383
|
+
/** '' | 'overdue' | 'soon' */
|
|
1384
|
+
this.due = signal('', ...(ngDevMode ? [{ debugName: "due" }] : /* istanbul ignore next */ []));
|
|
1385
|
+
/** '' | um status do quadro: só essa coluna aparece. */
|
|
1386
|
+
this.status = signal('', ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
|
|
1387
|
+
this.sort = signal('position', ...(ngDevMode ? [{ debugName: "sort" }] : /* istanbul ignore next */ []));
|
|
1388
|
+
/** "Mais filtros" aberto (tela e quem pediu). */
|
|
1389
|
+
this.moreFilters = signal(false, ...(ngDevMode ? [{ debugName: "moreFilters" }] : /* istanbul ignore next */ []));
|
|
1390
|
+
this.boardStatuses = BOARD_STATUSES;
|
|
1391
|
+
/** Os filtros escondidos em "Mais filtros" que estão ligados (o botão mostra a conta). */
|
|
1392
|
+
this.extraFilters = computed(() => [this.screen(), this.author()].filter(Boolean).length, ...(ngDevMode ? [{ debugName: "extraFilters" }] : /* istanbul ignore next */ []));
|
|
1393
|
+
this.moreFiltersText = computed(() => (this.extraFilters()
|
|
1394
|
+
? formatMessage(this.text.moreFiltersActive, { count: this.extraFilters() }) : this.text.moreFilters), ...(ngDevMode ? [{ debugName: "moreFiltersText" }] : /* istanbul ignore next */ []));
|
|
1395
|
+
this.me = computed(() => this.userStorage?.userId() ?? '', ...(ngDevMode ? [{ debugName: "me" }] : /* istanbul ignore next */ []));
|
|
1396
|
+
this.filtered = computed(() => {
|
|
1397
|
+
const term = fold(this.search().trim());
|
|
1398
|
+
const keyMatch = this.search().trim().match(/^(?:sug-?)?(\d{1,9})$/i);
|
|
1399
|
+
const [kind, screen, author, assignee, label, priority, due] = [this.kind(), this.screen(), this.author(), this.assignee(), this.label(), this.priority(), this.due()];
|
|
1400
|
+
const me = this.me();
|
|
1401
|
+
return this.all().filter((s) => (!kind || s.kind === kind)
|
|
1402
|
+
&& (!screen || s.screen.path === screen)
|
|
1403
|
+
&& (!author || s.author?.user_id === author)
|
|
1404
|
+
&& (!assignee || (assignee === 'none' ? s.assignees.length === 0 : assignedTo(s, assignee === 'me' ? me : assignee)))
|
|
1405
|
+
&& (!label || s.labels.some((l) => l.id === label))
|
|
1406
|
+
&& (!priority || s.priority === priority)
|
|
1407
|
+
&& (!due || this.matchesDue(s, due))
|
|
1408
|
+
&& (!term || (keyMatch && s.key_number === Number(keyMatch[1]))
|
|
1409
|
+
|| fold(`${s.title} ${s.description} ${s.screen.label ?? ''} ${this.personName(s.author)} ${s.key}`).includes(term)));
|
|
1410
|
+
}, ...(ngDevMode ? [{ debugName: "filtered" }] : /* istanbul ignore next */ []));
|
|
1411
|
+
this.isFiltered = computed(() => !!(this.search().trim() || this.kind() || this.screen() || this.author()
|
|
1412
|
+
|| this.assignee() || this.label() || this.priority() || this.due() || this.status()), ...(ngDevMode ? [{ debugName: "isFiltered" }] : /* istanbul ignore next */ []));
|
|
1413
|
+
this.filteredText = computed(() => formatMessage(this.text.boardFiltered, {
|
|
1414
|
+
shown: this.columns().reduce((n, c) => n + c.items.length, 0), total: this.all().length,
|
|
1415
|
+
}), ...(ngDevMode ? [{ debugName: "filteredText" }] : /* istanbul ignore next */ []));
|
|
1416
|
+
this.columns = computed(() => {
|
|
1417
|
+
const list = this.filtered();
|
|
1418
|
+
const order = SORTS[this.sort()];
|
|
1419
|
+
const only = this.status();
|
|
1420
|
+
return BOARD_STATUSES.filter((status) => !only || status === only).map((status) => ({
|
|
1421
|
+
id: status, title: this.text.statuses[status], items: list.filter((s) => s.status === status).sort(order),
|
|
1422
|
+
}));
|
|
1423
|
+
}, ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
|
|
1424
|
+
/**
|
|
1425
|
+
* Resumo de cada coluna (o que está à vista nela): há quanto tempo a mais antiga está no status ("Mais antiga há
|
|
1426
|
+
* 4h 12min", pelo `status_at`) e quantas vencidas.
|
|
1427
|
+
*/
|
|
1428
|
+
this.columnSummaries = computed(() => {
|
|
1429
|
+
const now = Date.now();
|
|
1430
|
+
return new Map(this.columns().map((c) => {
|
|
1431
|
+
const since = c.items.map((s) => Date.parse(s.status_at ?? s.created_at)).filter((t) => !Number.isNaN(t));
|
|
1432
|
+
const oldest = since.length ? this.duration(now - Math.min(...since)) : '';
|
|
1433
|
+
const overdue = c.items.filter((s) => dueState(s, this.today) === 'overdue').length;
|
|
1434
|
+
return [c.id, {
|
|
1435
|
+
oldest: oldest ? formatMessage(this.text.columnOldest, { age: oldest }) : '',
|
|
1436
|
+
overdue: overdue ? pluralMessage(this.text.columnOverdue, overdue, this.messages.meta.locale) : '',
|
|
1437
|
+
}];
|
|
1438
|
+
}));
|
|
1439
|
+
}, ...(ngDevMode ? [{ debugName: "columnSummaries" }] : /* istanbul ignore next */ []));
|
|
1440
|
+
/** Telas, pessoas e responsáveis que aparecem no quadro, para os filtros (só o que existe). */
|
|
1441
|
+
this.screens = computed(() => {
|
|
1442
|
+
const byPath = new Map();
|
|
1443
|
+
for (const s of this.all())
|
|
1444
|
+
if (s.screen.path)
|
|
1445
|
+
byPath.set(s.screen.path, s.screen.label || s.screen.path);
|
|
1446
|
+
return [...byPath].map(([path, label]) => ({ path, label })).sort((a, b) => a.label.localeCompare(b.label));
|
|
1447
|
+
}, ...(ngDevMode ? [{ debugName: "screens" }] : /* istanbul ignore next */ []));
|
|
1448
|
+
this.authors = computed(() => this.people((s) => (s.author ? [s.author] : [])), ...(ngDevMode ? [{ debugName: "authors" }] : /* istanbul ignore next */ []));
|
|
1449
|
+
this.assigneeOptions = computed(() => this.people((s) => s.assignees), ...(ngDevMode ? [{ debugName: "assigneeOptions" }] : /* istanbul ignore next */ []));
|
|
1450
|
+
/** Indicadores do quadro inteiro (o filtro não muda: é o retrato do app). */
|
|
1451
|
+
this.stats = computed(() => {
|
|
1452
|
+
const list = this.all();
|
|
1453
|
+
const count = (status) => list.filter((s) => s.status === status).length;
|
|
1454
|
+
const open = list.filter((s) => OPEN_STATUSES.includes(s.status));
|
|
1455
|
+
const yesterday = addDays(this.today, -1);
|
|
1456
|
+
const zone = this.preferences.effectiveTimeZone();
|
|
1457
|
+
const overdue = open.filter((s) => dueState(s, this.today) === 'overdue');
|
|
1458
|
+
const done = list.filter((s) => s.status === 'concluida');
|
|
1459
|
+
return {
|
|
1460
|
+
open: open.length, em_analise: count('em_analise'), planejada: count('planejada'),
|
|
1461
|
+
em_desenvolvimento: count('em_desenvolvimento'), concluida: done.length,
|
|
1462
|
+
mine: open.filter((s) => assignedTo(s, this.me())).length,
|
|
1463
|
+
overdue: overdue.length,
|
|
1464
|
+
/** Venceram ontem: passaram a vencidas hoje (o "+N desde ontem"). */
|
|
1465
|
+
overdueNew: overdue.filter((s) => s.due_date === yesterday).length,
|
|
1466
|
+
soon: open.filter((s) => { const d = dueState(s, this.today); return d === 'today' || d === 'tomorrow'; }).length,
|
|
1467
|
+
doneToday: done.filter((s) => todayIso(zone, new Date(s.status_at ?? s.updated_at)) === this.today).length,
|
|
1468
|
+
byPriority: TASK_PRIORITIES.map((p) => ({ priority: p, count: open.filter((s) => s.priority === p).length })),
|
|
1469
|
+
};
|
|
1470
|
+
}, ...(ngDevMode ? [{ debugName: "stats" }] : /* istanbul ignore next */ []));
|
|
1471
|
+
this.doneLabel = computed(() => formatMessage(this.text.kpiDone, { days: this.doneDays() }), ...(ngDevMode ? [{ debugName: "doneLabel" }] : /* istanbul ignore next */ []));
|
|
1472
|
+
/** "Em análise 3 · Planejada 4 · Em desenvolvimento 5" no indicador das abertas. */
|
|
1473
|
+
this.openParts = computed(() => OPEN_STATUSES.map((st) => ({
|
|
1474
|
+
status: st,
|
|
1475
|
+
text: formatMessage(this.text.kpiStatusCount, { status: this.text.statuses[st], count: this.all().filter((s) => s.status === st).length }),
|
|
1476
|
+
})), ...(ngDevMode ? [{ debugName: "openParts" }] : /* istanbul ignore next */ []));
|
|
1477
|
+
this.overdueNote = computed(() => (this.stats().overdueNew
|
|
1478
|
+
? formatMessage(this.text.kpiOverdueTrend, { count: this.stats().overdueNew }) : this.text.kpiOverdueSteady), ...(ngDevMode ? [{ debugName: "overdueNote" }] : /* istanbul ignore next */ []));
|
|
1479
|
+
this.doneNote = computed(() => (this.stats().doneToday
|
|
1480
|
+
? formatMessage(this.text.kpiDoneToday, { count: this.stats().doneToday }) : this.text.kpiDoneNoneToday), ...(ngDevMode ? [{ debugName: "doneNote" }] : /* istanbul ignore next */ []));
|
|
1481
|
+
this.kpiLoading = computed(() => this.loading() && !this.all().length, ...(ngDevMode ? [{ debugName: "kpiLoading" }] : /* istanbul ignore next */ []));
|
|
1482
|
+
/** Itens do "▾" ao lado de "Nova tarefa": criar direto numa coluna. */
|
|
1483
|
+
this.newTaskItems = BOARD_STATUSES.map((st) => ({
|
|
1484
|
+
id: st, label: formatMessage(this.text.newTaskIn, { status: this.text.statuses[st] }), icon: SPINE_SUGGESTION_STATUS_ICONS[st],
|
|
1485
|
+
}));
|
|
1486
|
+
this.topIds = computed(() => new Set(this.all().filter((s) => s.votes > 0).sort(SORTS.votes).slice(0, TOP_VOTED).map((s) => s.id)), ...(ngDevMode ? [{ debugName: "topIds" }] : /* istanbul ignore next */ []));
|
|
1487
|
+
/** O menu "⋯" de cada cartão, montado uma vez por versão da tarefa (o menu aberto não perde o foco). */
|
|
1488
|
+
this.menus = computed(() => new Map(this.all().map((t) => [t.id, this.buildMenu(t)])), ...(ngDevMode ? [{ debugName: "menus" }] : /* istanbul ignore next */ []));
|
|
1489
|
+
this.itemKey = (s) => s.id;
|
|
1490
|
+
this.itemLabel = (s) => `${s.key} ${s.title}`;
|
|
1491
|
+
// --- capa pelo menu do cartão ---
|
|
1492
|
+
this.coverPop = viewChild('coverPop', ...(ngDevMode ? [{ debugName: "coverPop" }] : /* istanbul ignore next */ []));
|
|
1493
|
+
this.coverTarget = signal(null, ...(ngDevMode ? [{ debugName: "coverTarget" }] : /* istanbul ignore next */ []));
|
|
1494
|
+
// --- arquivo ---
|
|
1495
|
+
this.archived = signal([], ...(ngDevMode ? [{ debugName: "archived" }] : /* istanbul ignore next */ []));
|
|
1496
|
+
this.archiveTotal = signal(0, ...(ngDevMode ? [{ debugName: "archiveTotal" }] : /* istanbul ignore next */ []));
|
|
1497
|
+
this.archivePage = signal({ page: 1, size: ARCHIVE_PAGE }, ...(ngDevMode ? [{ debugName: "archivePage" }] : /* istanbul ignore next */ []));
|
|
1498
|
+
this.archiveSearch = signal('', ...(ngDevMode ? [{ debugName: "archiveSearch" }] : /* istanbul ignore next */ []));
|
|
1499
|
+
this.archiveKind = signal('', ...(ngDevMode ? [{ debugName: "archiveKind" }] : /* istanbul ignore next */ []));
|
|
1500
|
+
this.archiveStatus = signal('', ...(ngDevMode ? [{ debugName: "archiveStatus" }] : /* istanbul ignore next */ []));
|
|
1501
|
+
this.archiveLoading = signal(false, ...(ngDevMode ? [{ debugName: "archiveLoading" }] : /* istanbul ignore next */ []));
|
|
1502
|
+
this.archiveError = signal('', ...(ngDevMode ? [{ debugName: "archiveError" }] : /* istanbul ignore next */ []));
|
|
1503
|
+
this.reopening = signal(null, ...(ngDevMode ? [{ debugName: "reopening" }] : /* istanbul ignore next */ []));
|
|
1504
|
+
this.archiveCountText = computed(() => pluralMessage(this.text.archiveCount, this.archiveTotal(), this.messages.meta.locale), ...(ngDevMode ? [{ debugName: "archiveCountText" }] : /* istanbul ignore next */ []));
|
|
1505
|
+
this.archiveEmptyHint = computed(() => formatMessage(this.text.archiveEmptyHint, { days: this.doneDays() }), ...(ngDevMode ? [{ debugName: "archiveEmptyHint" }] : /* istanbul ignore next */ []));
|
|
1506
|
+
// --- tarefa aberta ---
|
|
1507
|
+
this.selected = signal(null, ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
|
|
1508
|
+
this.drafting = signal(false, ...(ngDevMode ? [{ debugName: "drafting" }] : /* istanbul ignore next */ []));
|
|
1509
|
+
this.draftStatus = signal('em_analise', ...(ngDevMode ? [{ debugName: "draftStatus" }] : /* istanbul ignore next */ []));
|
|
1510
|
+
this.canOpenScreen = computed(() => !!this.sessionClientId && this.sessionClientId.toLowerCase() === this.clientId().toLowerCase(), ...(ngDevMode ? [{ debugName: "canOpenScreen" }] : /* istanbul ignore next */ []));
|
|
1511
|
+
this.linkHandled = false;
|
|
1512
|
+
// --- agrupar ---
|
|
1513
|
+
/** A que está sendo agrupada (o seletor aberto); o detalhe fecha antes (um modal por vez). */
|
|
1514
|
+
this.mergeSource = signal(null, ...(ngDevMode ? [{ debugName: "mergeSource" }] : /* istanbul ignore next */ []));
|
|
1515
|
+
effect(() => {
|
|
1516
|
+
const clientId = this.clientId();
|
|
1517
|
+
untracked(() => {
|
|
1518
|
+
this.selected.set(null);
|
|
1519
|
+
this.drafting.set(false);
|
|
1520
|
+
void this.loadLabels(clientId);
|
|
1521
|
+
if (this.mode() === 'archive')
|
|
1522
|
+
void this.loadArchive(1, clientId);
|
|
1523
|
+
else
|
|
1524
|
+
void this.load(clientId);
|
|
1525
|
+
});
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
people(of) {
|
|
1529
|
+
const byId = new Map();
|
|
1530
|
+
for (const s of this.all())
|
|
1531
|
+
for (const p of of(s))
|
|
1532
|
+
byId.set(p.user_id, p);
|
|
1533
|
+
return [...byId.values()].sort((a, b) => this.personName(a).localeCompare(this.personName(b)));
|
|
1534
|
+
}
|
|
1535
|
+
matchesDue(s, due) {
|
|
1536
|
+
const state = dueState(s, this.today);
|
|
1537
|
+
return due === 'overdue' ? state === 'overdue' : state === 'today' || state === 'tomorrow';
|
|
1538
|
+
}
|
|
1539
|
+
setMode(value) {
|
|
1540
|
+
const mode = value === 'archive' ? 'archive' : 'board';
|
|
1541
|
+
if (mode === this.mode())
|
|
1542
|
+
return;
|
|
1543
|
+
this.mode.set(mode);
|
|
1544
|
+
// Cada troca relê: o que foi reaberto no arquivo aparece no quadro e vice-versa.
|
|
1545
|
+
if (mode === 'archive')
|
|
1546
|
+
void this.loadArchive(1);
|
|
1547
|
+
else
|
|
1548
|
+
void this.load();
|
|
1549
|
+
}
|
|
1550
|
+
refresh() {
|
|
1551
|
+
if (this.mode() === 'archive')
|
|
1552
|
+
void this.loadArchive(this.archivePage().page);
|
|
1553
|
+
else
|
|
1554
|
+
void this.load();
|
|
1555
|
+
}
|
|
1556
|
+
async load(clientId = this.clientId()) {
|
|
1557
|
+
this.loading.set(true);
|
|
1558
|
+
this.loadError.set('');
|
|
1559
|
+
try {
|
|
1560
|
+
const board = await this.service.board(clientId);
|
|
1561
|
+
this.all.set(board.suggestions);
|
|
1562
|
+
this.doneDays.set(board.doneDays);
|
|
1563
|
+
void this.openFromLink(clientId);
|
|
1564
|
+
}
|
|
1565
|
+
catch {
|
|
1566
|
+
this.loadError.set(this.text.loadError);
|
|
1567
|
+
}
|
|
1568
|
+
finally {
|
|
1569
|
+
this.loading.set(false);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
async loadLabels(clientId = this.clientId()) {
|
|
1573
|
+
try {
|
|
1574
|
+
this.catalog.set(await this.service.labels(clientId));
|
|
1575
|
+
}
|
|
1576
|
+
catch {
|
|
1577
|
+
this.catalog.set([]);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
/** "?sugestao=SUG-12" na URL: abre a tarefa (uma vez por tela). */
|
|
1581
|
+
async openFromLink(clientId) {
|
|
1582
|
+
if (this.linkHandled || !this.router)
|
|
1583
|
+
return;
|
|
1584
|
+
this.linkHandled = true;
|
|
1585
|
+
const key = this.router.parseUrl(this.router.url).queryParams[SUGGESTION_LINK_PARAM];
|
|
1586
|
+
if (typeof key !== 'string' || !key)
|
|
1587
|
+
return;
|
|
1588
|
+
const known = this.all().find((s) => s.key.toLowerCase() === key.toLowerCase());
|
|
1589
|
+
try {
|
|
1590
|
+
this.openDetail(known ?? (await this.service.lookup(clientId, key)));
|
|
1591
|
+
}
|
|
1592
|
+
catch {
|
|
1593
|
+
this.toast.error(this.text.loadError);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
clearFilters() {
|
|
1597
|
+
for (const s of [this.search, this.kind, this.screen, this.author, this.assignee, this.label, this.priority, this.due, this.status]) {
|
|
1598
|
+
s.set('');
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
/** Indicador que filtra ("atribuídas a mim", "vencidas", "vencendo em breve"): liga/desliga o filtro equivalente. */
|
|
1602
|
+
toggleKpi(which, on) {
|
|
1603
|
+
if (which === 'mine')
|
|
1604
|
+
this.assignee.set(on ? 'me' : '');
|
|
1605
|
+
else
|
|
1606
|
+
this.due.set(on ? which : '');
|
|
1607
|
+
}
|
|
1608
|
+
/** Uma prioridade da legenda do indicador: filtra por ela (de novo, desliga). */
|
|
1609
|
+
togglePriority(p) {
|
|
1610
|
+
this.priority.update((current) => (current === p ? '' : p));
|
|
1611
|
+
}
|
|
1612
|
+
/** Largura de cada prioridade na barra empilhada (% das abertas). */
|
|
1613
|
+
priorityShare(count) {
|
|
1614
|
+
const total = this.stats().open;
|
|
1615
|
+
return total ? (count / total) * 100 : 0;
|
|
1616
|
+
}
|
|
1617
|
+
priorityPart(p, count) {
|
|
1618
|
+
return formatMessage(this.text.kpiPriorityPart, { priority: this.text.priorities[p], count });
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* O quadro já moveu o cartão (na lista filtrada); aqui vira a posição na coluna INTEIRA — logo depois do vizinho de
|
|
1622
|
+
* cima que está visível (ou antes do de baixo; sozinho na coluna filtrada = fim) — aplica na lista e grava. Com
|
|
1623
|
+
* outra ordenação, só a coluna conta: vai para o fim dela, e reordenar na mesma coluna não grava.
|
|
1624
|
+
*/
|
|
1625
|
+
async onMoved(event) {
|
|
1626
|
+
const item = event.item;
|
|
1627
|
+
const to = event.to;
|
|
1628
|
+
const changed = event.from !== to;
|
|
1629
|
+
let change;
|
|
1630
|
+
if (this.sort() !== 'position') {
|
|
1631
|
+
if (!changed) {
|
|
1632
|
+
this.all.update((list) => [...list]); // devolve o cartão ao lugar da ordenação
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
const end = this.all().filter((s) => s.status === to && s.id !== item.id).length;
|
|
1636
|
+
this.all.update((list) => list.map((s) => (s.id === item.id ? { ...s, status: to, position: end } : s)));
|
|
1637
|
+
change = { status: to };
|
|
1638
|
+
}
|
|
1639
|
+
else {
|
|
1640
|
+
const shown = event.columns.find((c) => c.id === to)?.items ?? [];
|
|
1641
|
+
const above = shown[event.toIndex - 1];
|
|
1642
|
+
const below = shown[event.toIndex + 1];
|
|
1643
|
+
const column = this.all().filter((s) => s.status === to && s.id !== item.id).sort(byPosition);
|
|
1644
|
+
let index = column.length;
|
|
1645
|
+
if (above)
|
|
1646
|
+
index = column.findIndex((s) => s.id === above.id) + 1;
|
|
1647
|
+
else if (below)
|
|
1648
|
+
index = Math.max(0, column.findIndex((s) => s.id === below.id));
|
|
1649
|
+
const order = [...column.slice(0, index).map((s) => s.id), item.id, ...column.slice(index).map((s) => s.id)];
|
|
1650
|
+
const position = new Map(order.map((id, i) => [id, i]));
|
|
1651
|
+
this.all.update((list) => list.map((s) => (position.has(s.id)
|
|
1652
|
+
? { ...s, position: position.get(s.id), ...(s.id === item.id ? { status: to } : {}) } : s)));
|
|
1653
|
+
change = { ...(changed ? { status: to } : {}), position: index };
|
|
1654
|
+
}
|
|
1655
|
+
try {
|
|
1656
|
+
this.replace(await this.service.move(this.clientId(), item.id, change));
|
|
1657
|
+
}
|
|
1658
|
+
catch {
|
|
1659
|
+
this.toast.error(this.text.moveError);
|
|
1660
|
+
await this.load();
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
// --- criação ---
|
|
1664
|
+
/**
|
|
1665
|
+
* Abre o cadastro completo (a tarefa em branco no modal) já na coluna escolhida: "Nova tarefa" do topo (Em análise),
|
|
1666
|
+
* o "▾" ao lado dele, o "+" do cabeçalho e o "+ Criar tarefa" do rodapé de cada coluna.
|
|
1667
|
+
*/
|
|
1668
|
+
newTask(status = 'em_analise') {
|
|
1669
|
+
this.selected.set(null);
|
|
1670
|
+
this.draftStatus.set(BOARD_STATUSES.includes(status) ? status : 'em_analise');
|
|
1671
|
+
this.drafting.set(true);
|
|
1672
|
+
}
|
|
1673
|
+
/** Nome do "+" da coluna: "Nova tarefa em Planejada". */
|
|
1674
|
+
newTaskInLabel(status) {
|
|
1675
|
+
return formatMessage(this.text.newTaskIn, { status: this.text.statuses[status] ?? status });
|
|
1676
|
+
}
|
|
1677
|
+
// --- tarefa aberta ---
|
|
1678
|
+
openDetail(s) {
|
|
1679
|
+
this.drafting.set(false);
|
|
1680
|
+
this.selected.set(s);
|
|
1681
|
+
}
|
|
1682
|
+
closeDetail() {
|
|
1683
|
+
this.selected.set(null);
|
|
1684
|
+
this.drafting.set(false);
|
|
1685
|
+
if (this.mode() === 'archive')
|
|
1686
|
+
void this.loadArchive(this.archivePage().page);
|
|
1687
|
+
}
|
|
1688
|
+
onCreated(task) {
|
|
1689
|
+
if (this.mode() === 'board')
|
|
1690
|
+
this.all.update((list) => [...list, task]);
|
|
1691
|
+
this.drafting.set(false);
|
|
1692
|
+
this.selected.set(task);
|
|
1693
|
+
}
|
|
1694
|
+
onRemoved(task) {
|
|
1695
|
+
this.all.update((list) => list.filter((s) => s.id !== task.id));
|
|
1696
|
+
this.archived.update((list) => list.filter((s) => s.id !== task.id));
|
|
1697
|
+
this.archiveTotal.update((n) => Math.max(0, n - (this.mode() === 'archive' ? 1 : 0)));
|
|
1698
|
+
this.selected.set(null);
|
|
1699
|
+
}
|
|
1700
|
+
onLabelCreated(label) {
|
|
1701
|
+
this.catalog.update((list) => [...list, label].sort((a, b) => a.name.localeCompare(b.name)));
|
|
1702
|
+
}
|
|
1703
|
+
/**
|
|
1704
|
+
* Troca a tarefa nas listas (o que o servidor devolveu), mantendo o autor que já veio com a foto. Recusada,
|
|
1705
|
+
* arquivada e agrupada saem do quadro na hora (o servidor também as tira do próximo `board=true`).
|
|
1706
|
+
*/
|
|
1707
|
+
replace(saved) {
|
|
1708
|
+
const merge = (s) => ({ ...s, ...saved, author: saved.author ?? s.author });
|
|
1709
|
+
const leaves = saved.status === 'recusada' || !!saved.archived_at || !!saved.merged_into;
|
|
1710
|
+
this.all.update((list) => (leaves
|
|
1711
|
+
? list.filter((s) => s.id !== saved.id)
|
|
1712
|
+
: list.map((s) => (s.id === saved.id ? merge(s) : s))));
|
|
1713
|
+
this.archived.update((list) => list.map((s) => (s.id === saved.id ? merge(s) : s)));
|
|
1714
|
+
}
|
|
1715
|
+
// --- menu do cartão ---
|
|
1716
|
+
buildMenu(t) {
|
|
1717
|
+
const linked = new Set(t.labels.map((l) => l.id));
|
|
1718
|
+
const labels = this.catalog().length
|
|
1719
|
+
? this.catalog().map((l) => ({ id: `label:${l.id}`, label: l.name, icon: linked.has(l.id) ? 'check' : 'tag' }))
|
|
1720
|
+
: [{ id: 'label:none', label: this.text.menuNoLabels, disabled: true }];
|
|
1721
|
+
return [
|
|
1722
|
+
{
|
|
1723
|
+
id: 'status', label: this.text.menuStatus, icon: 'view-columns',
|
|
1724
|
+
children: SPINE_SUGGESTION_STATUSES.map((st) => ({
|
|
1725
|
+
id: `status:${st}`, label: this.text.statuses[st], icon: st === t.status ? 'check' : SPINE_SUGGESTION_STATUS_ICONS[st], disabled: st === t.status,
|
|
1726
|
+
})),
|
|
1727
|
+
},
|
|
1728
|
+
{
|
|
1729
|
+
id: 'priority', label: this.text.menuPriority, icon: TASK_PRIORITY_ICONS[t.priority],
|
|
1730
|
+
children: TASK_PRIORITIES.map((p) => ({ id: `priority:${p}`, label: this.text.priorities[p], icon: p === t.priority ? 'check' : TASK_PRIORITY_ICONS[p] })),
|
|
1731
|
+
},
|
|
1732
|
+
{ id: 'assign-me', label: this.text.menuAssignMe, icon: 'user-plus', disabled: assignedTo(t, this.me()) },
|
|
1733
|
+
{ id: 'labels', label: this.text.menuLabel, icon: 'tag', children: labels },
|
|
1734
|
+
{ id: 'cover', label: this.text.menuCover, icon: 'color-palette' },
|
|
1735
|
+
{ id: 'copy-link', label: this.text.menuCopyLink, icon: 'link', separatorAbove: true },
|
|
1736
|
+
{ id: 'copy-key', label: this.text.menuCopyKey, icon: 'copy' },
|
|
1737
|
+
// Sem cadeia: a que já recebeu outras não pode ser agrupada (o seletor explica; aqui fica desligado).
|
|
1738
|
+
{ id: 'merge', label: this.text.menuMerge, icon: 'layers', separatorAbove: true, disabled: !!t.merged_count },
|
|
1739
|
+
{ id: 'archive', label: this.text.menuArchive, icon: 'archive', separatorAbove: true },
|
|
1740
|
+
{ id: 'delete', label: this.text.menuDelete, icon: 'trash', danger: true },
|
|
1741
|
+
];
|
|
1742
|
+
}
|
|
1743
|
+
menuOf(t) {
|
|
1744
|
+
return this.menus().get(t.id) ?? [];
|
|
1745
|
+
}
|
|
1746
|
+
menuLabel(t) {
|
|
1747
|
+
return formatMessage(this.text.cardMenu, { key: t.key });
|
|
1748
|
+
}
|
|
1749
|
+
/** Uma escolha do "⋯": cada uma grava e devolve a tarefa nova (ou copia, ou abre a capa ancorada no botão). */
|
|
1750
|
+
async onMenu(t, id, anchor) {
|
|
1751
|
+
const clientId = this.clientId();
|
|
1752
|
+
const [action, value] = id.split(':');
|
|
1753
|
+
try {
|
|
1754
|
+
if (action === 'status')
|
|
1755
|
+
this.replace(await this.service.move(clientId, t.id, { status: value }));
|
|
1756
|
+
else if (action === 'priority')
|
|
1757
|
+
this.replace(await this.service.update(clientId, t.id, { priority: value }));
|
|
1758
|
+
else if (action === 'assign-me') {
|
|
1759
|
+
this.replace(await this.service.addPerson(clientId, t.id, 'me', 'responsavel'));
|
|
1760
|
+
this.toast.success(this.text.assignedToMe);
|
|
1761
|
+
}
|
|
1762
|
+
else if (action === 'label') {
|
|
1763
|
+
this.replace(await this.service.setLabel(clientId, t.id, value, !t.labels.some((l) => l.id === value)));
|
|
1764
|
+
}
|
|
1765
|
+
else if (action === 'cover') {
|
|
1766
|
+
this.coverTarget.set(t);
|
|
1767
|
+
this.coverPop()?.show(anchor);
|
|
1768
|
+
}
|
|
1769
|
+
else if (action === 'copy-link') {
|
|
1770
|
+
if (await copyText(this.linkOf(t)))
|
|
1771
|
+
this.toast.success(this.text.linkCopied);
|
|
1772
|
+
else
|
|
1773
|
+
this.toast.error(this.text.copyError);
|
|
1774
|
+
}
|
|
1775
|
+
else if (action === 'copy-key') {
|
|
1776
|
+
if (await copyText(t.key))
|
|
1777
|
+
this.toast.success(this.text.keyCopied);
|
|
1778
|
+
else
|
|
1779
|
+
this.toast.error(this.text.copyError);
|
|
1780
|
+
}
|
|
1781
|
+
else if (action === 'merge') {
|
|
1782
|
+
this.startMerge(t);
|
|
1783
|
+
}
|
|
1784
|
+
else if (action === 'archive') {
|
|
1785
|
+
this.replace(await this.service.setArchived(clientId, t.id, true));
|
|
1786
|
+
this.toast.success(this.text.archivedToast);
|
|
1787
|
+
}
|
|
1788
|
+
else if (action === 'delete')
|
|
1789
|
+
await this.confirmDelete(t);
|
|
1790
|
+
}
|
|
1791
|
+
catch {
|
|
1792
|
+
this.toast.error(this.text.saveError);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
startMerge(t) {
|
|
1796
|
+
this.selected.set(null);
|
|
1797
|
+
this.drafting.set(false);
|
|
1798
|
+
this.mergeSource.set(t);
|
|
1799
|
+
}
|
|
1800
|
+
/** Agrupou: a origem sai do quadro, o destino é trocado pela versão do servidor e abre (mostra "Agrupadas aqui"). */
|
|
1801
|
+
onMerged(event) {
|
|
1802
|
+
this.mergeSource.set(null);
|
|
1803
|
+
this.all.update((list) => list.filter((s) => s.id !== event.source.id));
|
|
1804
|
+
this.replace(event.target);
|
|
1805
|
+
this.selected.set(event.target);
|
|
1806
|
+
}
|
|
1807
|
+
/** O detalhe pediu outra tarefa (o destino de uma agrupada, uma das agrupadas): abre a completa. */
|
|
1808
|
+
async openTask(id) {
|
|
1809
|
+
try {
|
|
1810
|
+
const task = await this.service.task(this.clientId(), id);
|
|
1811
|
+
this.openDetail(task);
|
|
1812
|
+
}
|
|
1813
|
+
catch {
|
|
1814
|
+
this.toast.error(this.text.loadError);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
/** Desagrupou: a origem volta ao quadro (relê o que estiver à vista). */
|
|
1818
|
+
onUnmerged() {
|
|
1819
|
+
if (this.mode() === 'archive')
|
|
1820
|
+
void this.loadArchive(this.archivePage().page);
|
|
1821
|
+
else
|
|
1822
|
+
void this.load();
|
|
1823
|
+
}
|
|
1824
|
+
mergedIntoText(key) {
|
|
1825
|
+
return formatMessage(this.text.mergedInto, { key });
|
|
1826
|
+
}
|
|
1827
|
+
/** "Desagrupar" no arquivo. */
|
|
1828
|
+
async unmergeFromArchive(t) {
|
|
1829
|
+
this.reopening.set(t.id);
|
|
1830
|
+
try {
|
|
1831
|
+
await this.service.unmerge(this.clientId(), t.id);
|
|
1832
|
+
this.toast.success(formatMessage(this.text.unmergedToast, { key: t.key }));
|
|
1833
|
+
void this.loadArchive(this.archivePage().page);
|
|
1834
|
+
}
|
|
1835
|
+
catch {
|
|
1836
|
+
this.toast.error(this.text.saveError);
|
|
1837
|
+
}
|
|
1838
|
+
finally {
|
|
1839
|
+
this.reopening.set(null);
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
async pickCover(cover) {
|
|
1843
|
+
const t = this.coverTarget();
|
|
1844
|
+
this.coverPop()?.close();
|
|
1845
|
+
if (!t || cover === t.cover)
|
|
1846
|
+
return;
|
|
1847
|
+
try {
|
|
1848
|
+
this.replace(await this.service.update(this.clientId(), t.id, { cover }));
|
|
1849
|
+
}
|
|
1850
|
+
catch {
|
|
1851
|
+
this.toast.error(this.text.saveError);
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
async confirmDelete(t) {
|
|
1855
|
+
const ok = await this.dialogs.confirm({
|
|
1856
|
+
heading: formatMessage(this.text.deleteHeading, { key: t.key }), message: this.text.deleteMessage,
|
|
1857
|
+
confirmLabel: this.text.deleteConfirm, danger: true,
|
|
1858
|
+
});
|
|
1859
|
+
if (!ok)
|
|
1860
|
+
return;
|
|
1861
|
+
await this.service.remove(this.clientId(), t.id);
|
|
1862
|
+
this.onRemoved(t);
|
|
1863
|
+
this.toast.success(this.text.deleted);
|
|
1864
|
+
}
|
|
1865
|
+
/** O link da tela com a tarefa: o caminho atual (sem a query) + "?sugestao=SUG-12". */
|
|
1866
|
+
linkOf(t) {
|
|
1867
|
+
const path = (this.location?.path() ?? '').split('?')[0];
|
|
1868
|
+
const base = this.location?.prepareExternalUrl(path) ?? path;
|
|
1869
|
+
const origin = typeof window !== 'undefined' ? window.location.origin : '';
|
|
1870
|
+
return `${origin}${base}?${SUGGESTION_LINK_PARAM}=${encodeURIComponent(t.key)}`;
|
|
1871
|
+
}
|
|
1872
|
+
/** Item do checklist marcado no próprio cartão. */
|
|
1873
|
+
async toggleCardItem(t, item) {
|
|
1874
|
+
try {
|
|
1875
|
+
this.replace(await this.service.updateChecklistItem(this.clientId(), t.id, item.id, { done: !item.done }));
|
|
1876
|
+
}
|
|
1877
|
+
catch {
|
|
1878
|
+
this.toast.error(this.text.saveError);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
// --- arquivo ---
|
|
1882
|
+
async loadArchive(page = 1, clientId = this.clientId()) {
|
|
1883
|
+
const size = this.archivePage().size;
|
|
1884
|
+
this.archiveLoading.set(true);
|
|
1885
|
+
this.archiveError.set('');
|
|
1886
|
+
try {
|
|
1887
|
+
const res = await this.service.archive(clientId, {
|
|
1888
|
+
limit: size, offset: (page - 1) * size, search: this.archiveSearch().trim(),
|
|
1889
|
+
kind: this.archiveKind(), status: this.archiveStatus(),
|
|
1890
|
+
});
|
|
1891
|
+
this.archived.set(res.suggestions);
|
|
1892
|
+
this.archiveTotal.set(res.total);
|
|
1893
|
+
this.archivePage.set({ page, size });
|
|
1894
|
+
}
|
|
1895
|
+
catch {
|
|
1896
|
+
this.archiveError.set(this.text.loadError);
|
|
1897
|
+
}
|
|
1898
|
+
finally {
|
|
1899
|
+
this.archiveLoading.set(false);
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
onArchiveSearch(term) {
|
|
1903
|
+
this.archiveSearch.set(term);
|
|
1904
|
+
void this.loadArchive(1);
|
|
1905
|
+
}
|
|
1906
|
+
onArchiveFilter(which, value) {
|
|
1907
|
+
(which === 'kind' ? this.archiveKind : this.archiveStatus).set(value);
|
|
1908
|
+
void this.loadArchive(1);
|
|
1909
|
+
}
|
|
1910
|
+
onArchivePage(state) {
|
|
1911
|
+
this.archivePage.set({ page: state.page, size: state.size });
|
|
1912
|
+
void this.loadArchive(state.page);
|
|
1913
|
+
}
|
|
1914
|
+
/** Recusada volta para Em análise (fim da coluna); arquivada volta ao quadro com o status que tinha. */
|
|
1915
|
+
async reopen(s) {
|
|
1916
|
+
if (this.reopening())
|
|
1917
|
+
return;
|
|
1918
|
+
this.reopening.set(s.id);
|
|
1919
|
+
try {
|
|
1920
|
+
if (s.archived_at)
|
|
1921
|
+
await this.service.setArchived(this.clientId(), s.id, false);
|
|
1922
|
+
if (s.status === 'recusada')
|
|
1923
|
+
await this.service.move(this.clientId(), s.id, { status: 'em_analise' });
|
|
1924
|
+
this.toast.success(s.status === 'recusada' ? this.text.reopened : this.text.restoredToast);
|
|
1925
|
+
const page = this.archivePage().page;
|
|
1926
|
+
// Última da página: volta uma, para não cair numa página vazia.
|
|
1927
|
+
await this.loadArchive(this.archived().length === 1 && page > 1 ? page - 1 : page);
|
|
1928
|
+
}
|
|
1929
|
+
catch {
|
|
1930
|
+
this.toast.error(this.text.reopenError);
|
|
1931
|
+
}
|
|
1932
|
+
finally {
|
|
1933
|
+
this.reopening.set(null);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
// --- textos do cartão ---
|
|
1937
|
+
sortLabel(k) {
|
|
1938
|
+
if (k === 'priority')
|
|
1939
|
+
return this.text.priority;
|
|
1940
|
+
if (k === 'due')
|
|
1941
|
+
return this.text.dueDate;
|
|
1942
|
+
return this.text.sorts[k];
|
|
1943
|
+
}
|
|
1944
|
+
kindLabel(s) {
|
|
1945
|
+
return this.text.kinds[s.kind];
|
|
1946
|
+
}
|
|
1947
|
+
kindIcon(s) {
|
|
1948
|
+
return SPINE_SUGGESTION_KIND_ICONS[s.kind];
|
|
1949
|
+
}
|
|
1950
|
+
priorityIcon(s) {
|
|
1951
|
+
return TASK_PRIORITY_ICONS[s.priority];
|
|
1952
|
+
}
|
|
1953
|
+
priorityName(s) {
|
|
1954
|
+
return this.text.priorities[s.priority];
|
|
1955
|
+
}
|
|
1956
|
+
isTop(s) {
|
|
1957
|
+
return this.topIds().has(s.id);
|
|
1958
|
+
}
|
|
1959
|
+
excerpt(s) {
|
|
1960
|
+
const d = s.description.replace(/\s+/g, ' ').trim();
|
|
1961
|
+
return d.length > EXCERPT ? `${d.slice(0, EXCERPT - 1).trimEnd()}…` : d;
|
|
1962
|
+
}
|
|
1963
|
+
faces(s) {
|
|
1964
|
+
return s.assignees.slice(0, FACES);
|
|
1965
|
+
}
|
|
1966
|
+
moreFaces(s) {
|
|
1967
|
+
const rest = s.assignees.length - FACES;
|
|
1968
|
+
return rest > 0 ? formatMessage(this.text.moreAssignees, { count: rest }) : '';
|
|
1969
|
+
}
|
|
1970
|
+
dueOf(s) {
|
|
1971
|
+
return dueState(s, this.today);
|
|
1972
|
+
}
|
|
1973
|
+
/** "Vencida há 3 dias", "Vence hoje", "Vence amanhã", "No prazo · até 12 de nov." (concluída: "Até 12 de nov."). */
|
|
1974
|
+
dueText(s) {
|
|
1975
|
+
if (!s.due_date)
|
|
1976
|
+
return '';
|
|
1977
|
+
const state = this.dueOf(s);
|
|
1978
|
+
if (state === 'overdue') {
|
|
1979
|
+
return pluralMessage(this.text.dueLateDays, daysBetween(s.due_date, this.today), this.messages.meta.locale);
|
|
1980
|
+
}
|
|
1981
|
+
if (state === 'today')
|
|
1982
|
+
return this.text.dueToday;
|
|
1983
|
+
if (state === 'tomorrow')
|
|
1984
|
+
return this.text.dueTomorrow;
|
|
1985
|
+
if (state === 'later')
|
|
1986
|
+
return formatMessage(this.text.dueWithin, { date: this.day(s.due_date) });
|
|
1987
|
+
return formatMessage(this.text.dueOn, { date: this.day(s.due_date) });
|
|
1988
|
+
}
|
|
1989
|
+
dueIcon(s) {
|
|
1990
|
+
const state = this.dueOf(s);
|
|
1991
|
+
if (state === 'overdue')
|
|
1992
|
+
return 'alarm-clock';
|
|
1993
|
+
if (state === 'today' || state === 'tomorrow')
|
|
1994
|
+
return 'hourglass';
|
|
1995
|
+
return 'calendar';
|
|
1996
|
+
}
|
|
1997
|
+
/** Contexto do cartão (na cor de link): o tipo e a tela de origem. */
|
|
1998
|
+
contextOf(s) {
|
|
1999
|
+
return [this.kindLabel(s), s.screen.label].filter(Boolean).join(' · ');
|
|
2000
|
+
}
|
|
2001
|
+
createdText(s) {
|
|
2002
|
+
return formatMessage(this.text.createdOn, { date: this.date(s.created_at, true) });
|
|
2003
|
+
}
|
|
2004
|
+
/** Explicação da coluna vazia: com filtro, "nenhuma com estes filtros"; sem, o que costuma cair nela. */
|
|
2005
|
+
emptyHint(status) {
|
|
2006
|
+
if (this.isFiltered())
|
|
2007
|
+
return this.text.columnEmptyFiltered;
|
|
2008
|
+
const hint = this.text.columnEmptyHints[status] ?? '';
|
|
2009
|
+
return formatMessage(hint, { days: this.doneDays() });
|
|
2010
|
+
}
|
|
2011
|
+
emptyIcon(status) {
|
|
2012
|
+
return this.isFiltered() ? 'filter' : (SPINE_SUGGESTION_STATUS_ICONS[status] ?? 'inbox');
|
|
2013
|
+
}
|
|
2014
|
+
/** "3d 4h", "4h 12min", "12min" (o catálogo dá o formato). */
|
|
2015
|
+
duration(ms) {
|
|
2016
|
+
const { d, h, m } = splitDuration(ms);
|
|
2017
|
+
if (d)
|
|
2018
|
+
return formatMessage(this.text.duration.days, { d, h });
|
|
2019
|
+
if (h)
|
|
2020
|
+
return formatMessage(this.text.duration.hours, { h, m });
|
|
2021
|
+
return formatMessage(this.text.duration.minutes, { m });
|
|
2022
|
+
}
|
|
2023
|
+
checklistText(s) {
|
|
2024
|
+
return formatMessage(this.text.checklistSpoken, { done: s.checklist.done, total: s.checklist.total });
|
|
2025
|
+
}
|
|
2026
|
+
cardItems(s) {
|
|
2027
|
+
return s.checklist_on_card ? [...(s.checklist.items ?? [])].sort((a, b) => a.position - b.position) : [];
|
|
2028
|
+
}
|
|
2029
|
+
personName(p) {
|
|
2030
|
+
return p?.name || p?.username || this.text.removedAuthor;
|
|
2031
|
+
}
|
|
2032
|
+
votesText(count) {
|
|
2033
|
+
return pluralMessage(this.text.votes, count, this.messages.meta.locale);
|
|
2034
|
+
}
|
|
2035
|
+
commentsText(count) {
|
|
2036
|
+
return pluralMessage(this.text.comments, count, this.messages.meta.locale);
|
|
2037
|
+
}
|
|
2038
|
+
closedOn(s) {
|
|
2039
|
+
if (s.archived_at)
|
|
2040
|
+
return `${this.text.archivedTag} · ${this.date(s.archived_at)}`;
|
|
2041
|
+
const template = s.status === 'recusada' ? this.text.closedOn.recusada : this.text.closedOn.concluida;
|
|
2042
|
+
return formatMessage(template, { date: this.date(s.status_at ?? s.updated_at) });
|
|
2043
|
+
}
|
|
2044
|
+
/** "há 3 dias", "ontem", "há 2 horas" (Intl.RelativeTimeFormat no idioma do catálogo). */
|
|
2045
|
+
age(iso) {
|
|
2046
|
+
const seconds = Math.round((Date.now() - new Date(iso).getTime()) / 1000);
|
|
2047
|
+
try {
|
|
2048
|
+
const rtf = new Intl.RelativeTimeFormat(this.messages.meta.locale, { numeric: 'auto' });
|
|
2049
|
+
for (const [unit, size] of AGES)
|
|
2050
|
+
if (Math.abs(seconds) >= size)
|
|
2051
|
+
return rtf.format(-Math.floor(seconds / size), unit);
|
|
2052
|
+
return rtf.format(0, 'minute');
|
|
2053
|
+
}
|
|
2054
|
+
catch {
|
|
2055
|
+
return this.date(iso);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
date(iso, withTime = false) {
|
|
2059
|
+
try {
|
|
2060
|
+
return new Intl.DateTimeFormat(this.messages.meta.locale, {
|
|
2061
|
+
dateStyle: 'short', ...(withTime ? { timeStyle: 'short' } : {}), timeZone: this.preferences.effectiveTimeZone(),
|
|
2062
|
+
}).format(new Date(iso));
|
|
2063
|
+
}
|
|
2064
|
+
catch {
|
|
2065
|
+
return iso.slice(0, 10);
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
/** Data sem hora (AAAA-MM-DD): o dia é o dia, sem fuso. */
|
|
2069
|
+
day(iso) {
|
|
2070
|
+
try {
|
|
2071
|
+
return new Intl.DateTimeFormat(this.messages.meta.locale, { day: '2-digit', month: 'short', timeZone: 'UTC' }).format(new Date(`${iso}T00:00:00Z`));
|
|
2072
|
+
}
|
|
2073
|
+
catch {
|
|
2074
|
+
return iso;
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceSuggestionsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2078
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: AutoServiceSuggestionsComponent, isStandalone: true, selector: "spine-auto-service-suggestions", inputs: { clientId: { classPropertyName: "clientId", publicName: "clientId", isSignal: true, isRequired: true, transformFunction: null } }, viewQueries: [{ propertyName: "coverPop", first: true, predicate: ["coverPop"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"as-stack as-suggestions\">\n <!-- Quadro x Arquivo, o atualizar e a nova tarefa (com o \"\u25BE\" para criar direto numa coluna). -->\n <div class=\"as-suggestions__bar\">\n <hm-segmented-control [label]=\"text.viewLabel\" [options]=\"modeOptions\" [value]=\"mode()\" (valueChange)=\"setMode($event)\" />\n <span class=\"as-suggestions__spacer\"></span>\n <hm-icon-button emphasis=\"subtle\" icon=\"refresh\" [label]=\"text.refresh\" (clicked)=\"refresh()\" />\n <span class=\"as-split\" role=\"group\" [attr.aria-label]=\"text.newTask\">\n <hm-button icon=\"plus\" (clicked)=\"newTask()\">{{ text.newTask }}</hm-button>\n <hm-menu-button class=\"as-split__more\" icon=\"chevron-down\" [showArrow]=\"false\" align=\"end\" [label]=\"text.newTaskOptions\"\n [items]=\"newTaskItems\" (chosen)=\"newTask($any($event))\" />\n </span>\n </div>\n\n @if (mode() === 'board') {\n @if (loadError()) {\n <hm-alert variant=\"error\">{{ loadError() }}</hm-alert>\n } @else {\n <!-- Indicadores do quadro inteiro (o filtro n\u00E3o mexe neles). Os bot\u00F5es filtram: vencidas, vencendo em breve,\n atribu\u00EDdas a mim e cada prioridade da legenda. Cor sempre com texto junto. -->\n <section class=\"as-kpis\" [attr.aria-label]=\"text.kpis\" [attr.aria-busy]=\"kpiLoading() ? 'true' : null\">\n <button type=\"button\" class=\"as-kpi\" data-tone=\"danger\" [attr.aria-pressed]=\"due() === 'overdue'\"\n (click)=\"toggleKpi('overdue', due() !== 'overdue')\">\n <span class=\"as-kpi__icon\" aria-hidden=\"true\"><hm-icon shape=\"alarm-clock\" /></span>\n <span class=\"as-kpi__body\">\n <span class=\"as-kpi__label\">{{ text.kpiOverdue }}</span>\n <span class=\"as-kpi__value\">{{ kpiLoading() ? '\u2014' : stats().overdue }}</span>\n <span class=\"as-kpi__note\" [class.as-kpi__note--up]=\"stats().overdueNew > 0\">\n @if (stats().overdueNew) { <hm-icon shape=\"arrow-up\" /> }{{ overdueNote() }}\n </span>\n </span>\n </button>\n\n <button type=\"button\" class=\"as-kpi\" data-tone=\"warning\" [attr.aria-pressed]=\"due() === 'soon'\"\n (click)=\"toggleKpi('soon', due() !== 'soon')\">\n <span class=\"as-kpi__icon\" aria-hidden=\"true\"><hm-icon shape=\"hourglass\" /></span>\n <span class=\"as-kpi__body\">\n <span class=\"as-kpi__label\">{{ text.kpiSoon }}</span>\n <span class=\"as-kpi__value\">{{ kpiLoading() ? '\u2014' : stats().soon }}</span>\n <span class=\"as-kpi__note\">{{ text.dueSoonFilter }}</span>\n </span>\n </button>\n\n <div class=\"as-kpi\" data-tone=\"info\">\n <span class=\"as-kpi__icon\" aria-hidden=\"true\"><hm-icon [shape]=\"statusIcons.em_desenvolvimento\" /></span>\n <span class=\"as-kpi__body\">\n <span class=\"as-kpi__label\">{{ text.kpiOpen }}</span>\n <span class=\"as-kpi__value\">{{ kpiLoading() ? '\u2014' : stats().open }}</span>\n <span class=\"as-kpi__parts\">\n @for (part of openParts(); track part.status) {\n <span class=\"as-kpi__part\" [attr.data-status]=\"part.status\"><span class=\"as-kpi__dot\" aria-hidden=\"true\"></span>{{ part.text }}</span>\n }\n </span>\n </span>\n </div>\n\n <div class=\"as-kpi\" data-tone=\"success\">\n <span class=\"as-kpi__icon\" aria-hidden=\"true\"><hm-icon [shape]=\"statusIcons.concluida\" /></span>\n <span class=\"as-kpi__body\">\n <span class=\"as-kpi__label\">{{ doneLabel() }}</span>\n <span class=\"as-kpi__value\">{{ kpiLoading() ? '\u2014' : stats().concluida }}</span>\n <span class=\"as-kpi__note\" [class.as-kpi__note--good]=\"stats().doneToday > 0\">\n @if (stats().doneToday) { <hm-icon shape=\"arrow-up\" /> }{{ doneNote() }}\n </span>\n </span>\n </div>\n\n <button type=\"button\" class=\"as-kpi\" data-tone=\"accent\" [attr.aria-pressed]=\"assignee() === 'me'\"\n (click)=\"toggleKpi('mine', assignee() !== 'me')\">\n <span class=\"as-kpi__icon\" aria-hidden=\"true\"><hm-icon shape=\"user-checkmark\" /></span>\n <span class=\"as-kpi__body\">\n <span class=\"as-kpi__label\">{{ text.kpiMine }}</span>\n <span class=\"as-kpi__value\">{{ kpiLoading() ? '\u2014' : stats().mine }}</span>\n <span class=\"as-kpi__note\">{{ text.kpiMineHint }}</span>\n </span>\n </button>\n\n <div class=\"as-kpi as-kpi--wide\" data-tone=\"neutral\">\n <span class=\"as-kpi__icon\" aria-hidden=\"true\"><hm-icon shape=\"bar-chart\" /></span>\n <span class=\"as-kpi__body\">\n <span class=\"as-kpi__label\">{{ text.kpiPriority }}</span>\n @if (stats().open) {\n <span class=\"as-kpi__bar\" aria-hidden=\"true\">\n @for (p of stats().byPriority; track p.priority) {\n @if (p.count) {\n <span class=\"as-kpi__seg\" [attr.data-priority]=\"p.priority\" [style.inline-size.%]=\"priorityShare(p.count)\"></span>\n }\n }\n </span>\n <span class=\"as-kpi__legend\">\n @for (p of stats().byPriority; track p.priority) {\n <button type=\"button\" class=\"as-kpi__chip\" [attr.data-priority]=\"p.priority\" [attr.aria-pressed]=\"priority() === p.priority\"\n [disabled]=\"!p.count && priority() !== p.priority\" (click)=\"togglePriority(p.priority)\">\n <span class=\"as-kpi__dot\" aria-hidden=\"true\"></span>{{ priorityPart(p.priority, p.count) }}\n </button>\n }\n </span>\n } @else {\n <span class=\"as-kpi__note\">{{ text.kpiPriorityNone }}</span>\n }\n </span>\n </div>\n </section>\n\n <!-- Filtros numa linha: busca larga, os mais usados e \"Mais filtros\" (tela, quem pediu); \u00E0 direita, a ordena\u00E7\u00E3o.\n Valem sobre a lista inteira: a posi\u00E7\u00E3o gravada ao arrastar \u00E9 sempre a da coluna inteira. -->\n <div class=\"as-filters\" role=\"group\" [attr.aria-label]=\"text.filters\">\n <hm-search-field class=\"as-filters__search\" [label]=\"text.search\" labelHidden [placeholder]=\"text.search\"\n [landmark]=\"false\" [debounce]=\"150\" [value]=\"search()\" (search)=\"search.set($event)\" />\n <hm-select class=\"as-filters__select\" [label]=\"text.assigneeFilter\" labelHidden [(value)]=\"assignee\">\n <option value=\"\">{{ text.allAssignees }}</option>\n <option value=\"me\">{{ text.assigneeMine }}</option>\n <option value=\"none\">{{ text.assigneeNone }}</option>\n @for (p of assigneeOptions(); track p.user_id) {\n <option [value]=\"p.user_id\">{{ personName(p) }}</option>\n }\n </hm-select>\n <hm-select class=\"as-filters__select\" [label]=\"text.kindFilter\" labelHidden [(value)]=\"kind\">\n <option value=\"\">{{ text.allKinds }}</option>\n @for (k of kinds; track k) {\n <option [value]=\"k\">{{ text.kinds[k] }}</option>\n }\n </hm-select>\n <hm-select class=\"as-filters__select\" [label]=\"text.statusFilter\" labelHidden [(value)]=\"status\">\n <option value=\"\">{{ text.allBoardStatuses }}</option>\n @for (st of boardStatuses; track st) {\n <option [value]=\"st\">{{ text.statuses[st] }}</option>\n }\n </hm-select>\n <hm-select class=\"as-filters__select\" [label]=\"text.priorityFilter\" labelHidden [(value)]=\"priority\">\n <option value=\"\">{{ text.allPriorities }}</option>\n @for (p of priorities; track p) {\n <option [value]=\"p\">{{ text.priorities[p] }}</option>\n }\n </hm-select>\n <hm-select class=\"as-filters__select\" [label]=\"text.labelFilter\" labelHidden [(value)]=\"label\">\n <option value=\"\">{{ text.allLabels }}</option>\n @for (l of catalog(); track l.id) {\n <option [value]=\"l.id\">{{ l.name }}</option>\n }\n </hm-select>\n <hm-select class=\"as-filters__select\" [label]=\"text.dueFilter\" labelHidden [(value)]=\"due\">\n <option value=\"\">{{ text.dueAny }}</option>\n <option value=\"overdue\">{{ text.dueOverdueFilter }}</option>\n <option value=\"soon\">{{ text.dueSoonFilter }}</option>\n </hm-select>\n <button type=\"button\" class=\"as-filters__more\" [class.as-filters__more--on]=\"extraFilters() > 0\"\n [attr.aria-expanded]=\"moreFilters()\" [attr.aria-controls]=\"moreFilters() ? 'as-more-filters' : null\"\n (click)=\"moreFilters.set(!moreFilters())\">\n <hm-icon shape=\"filter\" />{{ moreFiltersText() }}\n </button>\n <span class=\"as-filters__spacer\"></span>\n <hm-select class=\"as-filters__select as-filters__sort\" [label]=\"text.sort\" labelHidden [value]=\"sort()\" (valueChange)=\"sort.set($any($event))\">\n @for (k of sortKeys; track k) {\n <option [value]=\"k\">{{ text.sort }}: {{ sortLabel(k) }}</option>\n }\n </hm-select>\n </div>\n @if (moreFilters()) {\n <div class=\"as-filters as-filters--more\" id=\"as-more-filters\">\n <hm-select class=\"as-filters__select\" [label]=\"text.screenFilter\" labelHidden [(value)]=\"screen\">\n <option value=\"\">{{ text.allScreens }}</option>\n @for (sc of screens(); track sc.path) {\n <option [value]=\"sc.path\">{{ sc.label }}</option>\n }\n </hm-select>\n <hm-select class=\"as-filters__select\" [label]=\"text.authorFilter\" labelHidden [(value)]=\"author\">\n <option value=\"\">{{ text.allAuthors }}</option>\n @for (p of authors(); track p.user_id) {\n <option [value]=\"p.user_id\">{{ personName(p) }}</option>\n }\n </hm-select>\n </div>\n }\n <div class=\"as-suggestions__status\">\n @if (sort() !== 'position') {\n <p class=\"as-suggestions__hint\">{{ text.sortHint }}</p>\n }\n @if (isFiltered()) {\n <span class=\"as-suggestions__count\" aria-live=\"polite\">{{ filteredText() }}</span>\n <hm-button size=\"small\" emphasis=\"minimal\" icon=\"filter-close\" (clicked)=\"clearFilters()\">{{ text.clearFilters }}</hm-button>\n }\n </div>\n\n @if (!loading() && all().length === 0) {\n <hm-empty icon=\"lightbulb\" [heading]=\"text.boardEmpty\" [description]=\"text.boardEmptyHint\" />\n }\n\n <hm-kanban class=\"as-board\" [label]=\"text.boardLabel\" [columns]=\"columns()\" [itemKey]=\"itemKey\" [itemLabel]=\"itemLabel\"\n (moved)=\"onMoved($event)\">\n <!-- \"+\" no cabe\u00E7alho: abre o cadastro completo (o modal da \"Nova tarefa\") j\u00E1 nesta coluna. -->\n <ng-template hmKanbanColumnActions let-column>\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"plus\" [label]=\"newTaskInLabel(column.id)\" (clicked)=\"newTask(column.id)\" />\n </ng-template>\n\n <!-- Logo abaixo do cabe\u00E7alho: h\u00E1 quanto tempo a mais antiga est\u00E1 no status e quantas venceram. -->\n <ng-template hmKanbanColumnSummary let-column>\n @if (columnSummaries().get(column.id); as sum) {\n @if (sum.oldest) {\n <span class=\"as-colsum\"><hm-icon shape=\"clock\" />{{ sum.oldest }}</span>\n }\n @if (sum.overdue) {\n <span class=\"as-colsum as-colsum--late\"><hm-icon shape=\"alarm-clock\" />{{ sum.overdue }}</span>\n }\n }\n </ng-template>\n\n <!-- Coluna vazia: \u00EDcone grande, a frase e o que costuma cair nela (com filtro, \"nenhuma com estes filtros\"). -->\n <ng-template hmKanbanColumnEmpty let-column>\n <span class=\"as-colempty\">\n <hm-icon class=\"as-colempty__icon\" [shape]=\"emptyIcon(column.id)\" />\n <span class=\"as-colempty__title\">{{ text.columnEmpty }}</span>\n <span class=\"as-colempty__hint\">{{ emptyHint(column.id) }}</span>\n </span>\n </ng-template>\n\n <!-- Rodap\u00E9: \"+ Criar tarefa\" abre o mesmo cadastro, com o status da coluna j\u00E1 escolhido. -->\n <ng-template hmKanbanColumnFooter let-column>\n <button type=\"button\" class=\"as-colfoot\" (click)=\"newTask(column.id)\">\n <hm-icon shape=\"plus\" />{{ text.createTask }}<span class=\"as-sr-only\">: {{ column.title }}</span>\n </button>\n </ng-template>\n\n <ng-template hmKanbanCard let-item>\n <div class=\"as-card\" [class.as-card--top]=\"isTop(item)\">\n @if (item.cover) {\n <div class=\"as-card__cover\" [class]=\"'as-tok--' + item.cover\" aria-hidden=\"true\"></div>\n }\n <div class=\"as-card__body\">\n <div class=\"as-card__top\">\n <span class=\"as-card__key\">{{ item.key }}</span>\n <span class=\"as-card__priority\" [attr.data-priority]=\"item.priority\">\n <hm-icon [shape]=\"priorityIcon(item)\" /><span class=\"as-sr-only\">{{ text.priority }}: </span>{{ priorityName(item) }}\n </span>\n @if (isTop(item)) {\n <span class=\"as-card__badge\"><hm-icon shape=\"flame\" />{{ text.topVoted }}</span>\n }\n <span #menuAnchor class=\"as-card__menu\">\n <hm-menu-button size=\"small\" icon=\"ellipsis-vertical\" [showArrow]=\"false\" align=\"end\" [label]=\"menuLabel(item)\"\n [items]=\"menuOf(item)\" (chosen)=\"onMenu(item, $event, menuAnchor)\" />\n </span>\n </div>\n <button type=\"button\" class=\"as-card__title\" (click)=\"openDetail(item)\">{{ item.title }}</button>\n <span class=\"as-card__context\" [attr.data-kind]=\"item.kind\"><hm-icon [shape]=\"kindIcon(item)\" />{{ contextOf(item) }}</span>\n @if (item.labels.length) {\n <div class=\"as-card__labels\">\n @for (l of item.labels; track l.id) {\n <span class=\"as-label\" [class]=\"'as-tok--' + l.color\">{{ l.name }}</span>\n }\n </div>\n }\n @if (excerpt(item); as ex) {\n <p class=\"as-card__excerpt\">{{ ex }}</p>\n }\n @if (cardItems(item).length) {\n <ul class=\"as-card__checklist\">\n @for (c of cardItems(item); track c.id) {\n <li>\n <button type=\"button\" class=\"as-card__check\" [attr.aria-pressed]=\"c.done\" (click)=\"toggleCardItem(item, c)\">\n <hm-icon [shape]=\"c.done ? 'check-circle' : 'circle-empty'\" /><span [class.as-card__done]=\"c.done\">{{ c.text }}</span>\n </button>\n </li>\n }\n </ul>\n }\n @if (item.due_date) {\n <span class=\"as-card__due\" [attr.data-due]=\"dueOf(item) ?? 'closed'\"><hm-icon [shape]=\"dueIcon(item)\" />{{ dueText(item) }}</span>\n }\n <div class=\"as-card__foot\">\n <span class=\"as-card__count\" [class.as-card__count--zero]=\"!item.comments_count\" [attr.title]=\"commentsText(item.comments_count)\">\n <hm-icon shape=\"comment-message\" /><span aria-hidden=\"true\">{{ item.comments_count }}</span><span class=\"as-sr-only\">{{ commentsText(item.comments_count) }}</span>\n </span>\n @if (item.checklist.total) {\n <span class=\"as-card__count\" [class.as-card__count--full]=\"item.checklist.done === item.checklist.total\" [attr.title]=\"checklistText(item)\">\n <hm-icon shape=\"list-check\" /><span aria-hidden=\"true\">{{ item.checklist.done }}/{{ item.checklist.total }}</span><span class=\"as-sr-only\">{{ checklistText(item) }}</span>\n </span>\n }\n <span class=\"as-card__count\" [class.as-card__count--on]=\"item.votes > 0\" [class.as-card__count--zero]=\"!item.votes\" [attr.title]=\"votesText(item.votes)\">\n <hm-icon shape=\"thumbs-up\" /><span aria-hidden=\"true\">{{ item.votes }}</span><span class=\"as-sr-only\">{{ votesText(item.votes) }}</span>\n </span>\n <span class=\"as-card__date\" [attr.title]=\"createdText(item)\">\n <hm-icon shape=\"clock\" /><span aria-hidden=\"true\">{{ date(item.created_at) }}</span><span class=\"as-sr-only\">{{ createdText(item) }}</span>\n </span>\n <span class=\"as-card__faces\">\n @for (p of faces(item); track p.user_id) {\n <hm-account-avatar size=\"small\" [name]=\"personName(p)\" [imageUrl]=\"p.picture\" [attr.title]=\"personName(p)\" />\n } @empty {\n <span class=\"as-card__nobody\" [attr.title]=\"text.assigneeNone\">\n <hm-icon shape=\"user-circle\" /><span class=\"as-sr-only\">{{ text.assigneeNone }}</span>\n </span>\n }\n @if (moreFaces(item); as more) { <span class=\"as-card__more\">{{ more }}</span> }\n </span>\n </div>\n </div>\n </div>\n </ng-template>\n </hm-kanban>\n }\n } @else {\n <!-- Arquivo: arquivadas, recusadas e conclu\u00EDdas fora da janela do quadro, paginado no servidor. -->\n <div class=\"as-suggestions__filters\">\n <hm-search-field class=\"as-suggestions__search\" [label]=\"text.archiveSearch\" labelHidden [placeholder]=\"text.archiveSearch\"\n [landmark]=\"false\" [debounce]=\"300\" [value]=\"archiveSearch()\" (search)=\"onArchiveSearch($event)\" />\n <hm-select class=\"as-suggestions__select\" [label]=\"text.statusFilter\" labelHidden [value]=\"archiveStatus()\"\n (valueChange)=\"onArchiveFilter('status', $any($event))\">\n <option value=\"\">{{ text.statusFilter }}: {{ text.allStatuses }}</option>\n @for (st of statuses; track st) {\n <option [value]=\"st\">{{ text.statuses[st] }}</option>\n }\n </hm-select>\n <hm-select class=\"as-suggestions__select\" [label]=\"text.kindFilter\" labelHidden [value]=\"archiveKind()\"\n (valueChange)=\"onArchiveFilter('kind', $any($event))\">\n <option value=\"\">{{ text.allKinds }}</option>\n @for (k of kinds; track k) {\n <option [value]=\"k\">{{ text.kinds[k] }}</option>\n }\n </hm-select>\n @if (archiveTotal()) {\n <span class=\"as-suggestions__count\" aria-live=\"polite\">{{ archiveCountText() }}</span>\n }\n </div>\n\n @if (archiveError()) {\n <hm-alert variant=\"error\">{{ archiveError() }}</hm-alert>\n } @else if (!archiveLoading() && archived().length === 0) {\n <hm-empty icon=\"archive\" [heading]=\"text.archiveEmpty\" [description]=\"archiveEmptyHint()\" />\n } @else {\n <ul class=\"as-archive\" [attr.aria-label]=\"text.archiveLabel\" [attr.aria-busy]=\"archiveLoading() ? 'true' : null\">\n @for (s of archived(); track s.id) {\n <li class=\"as-archive__item\">\n <div class=\"as-archive__main\">\n <div class=\"as-archive__tags\">\n <span class=\"as-card__key\">{{ s.key }}</span>\n <hm-tag [name]=\"text.statuses[s.status]\" [color]=\"statusColors[s.status]\" />\n <hm-tag [name]=\"text.kinds[s.kind]\" [color]=\"kindColors[s.kind]\" />\n @if (s.merged_into; as into) {\n <!-- Agrupada: no arquivo, com o destino; os coment\u00E1rios ficam nela. -->\n <hm-tag [name]=\"text.mergedTag\" color=\"info\" />\n <button type=\"button\" class=\"as-archive__link\" (click)=\"openTask(into.id)\">{{ mergedIntoText(into.key) }}</button>\n } @else {\n <span class=\"as-archive__date\">{{ closedOn(s) }}</span>\n }\n </div>\n <button type=\"button\" class=\"as-card__title\" (click)=\"openDetail(s)\">{{ s.title }}</button>\n <div class=\"as-archive__meta\">\n <hm-account-avatar size=\"small\" [name]=\"personName(s.author)\" [imageUrl]=\"s.author?.picture\" />\n <span>{{ personName(s.author) }}</span>\n <span aria-hidden=\"true\">\u00B7</span>\n <span>{{ s.screen.label || text.noScreen }}</span>\n @if (s.votes) {\n <span aria-hidden=\"true\">\u00B7</span>\n <span>{{ votesText(s.votes) }}</span>\n }\n </div>\n </div>\n @if (s.merged_into) {\n <hm-button size=\"small\" emphasis=\"medium\" icon=\"unlink\" [loading]=\"reopening() === s.id\" (clicked)=\"unmergeFromArchive(s)\">\n {{ text.unmerge }}\n </hm-button>\n } @else if (s.archived_at) {\n <hm-button size=\"small\" emphasis=\"medium\" icon=\"unarchive\" [loading]=\"reopening() === s.id\" (clicked)=\"reopen(s)\">\n {{ text.menuRestore }}\n </hm-button>\n } @else if (s.status === 'recusada') {\n <hm-button size=\"small\" emphasis=\"medium\" icon=\"unarchive\" [loading]=\"reopening() === s.id\" (clicked)=\"reopen(s)\">\n {{ text.reopen }}\n </hm-button>\n }\n </li>\n }\n </ul>\n @if (archiveTotal() > archivePage().size) {\n <hm-paginator variant=\"simple\" [label]=\"text.pagination\" [state]=\"archivePage()\" [totalItems]=\"archiveTotal()\"\n [sizes]=\"[20, 50]\" (stateChanged)=\"onArchivePage($event)\" />\n }\n }\n }\n</div>\n\n<!-- Capa pelo \"\u22EF\" do cart\u00E3o: o popover abre ancorado no bot\u00E3o do cart\u00E3o escolhido. -->\n<hm-popover #coverPop [label]=\"text.cover\">\n <spine-suggestion-cover-picker [value]=\"coverTarget()?.cover ?? null\" (picked)=\"pickCover($event)\" />\n</hm-popover>\n\n<spine-suggestion-task-detail [clientId]=\"clientId()\" [task]=\"selected()\" [draft]=\"drafting()\" [draftStatus]=\"draftStatus()\"\n [labels]=\"catalog()\" [canOpenScreen]=\"canOpenScreen()\"\n (changed)=\"replace($event)\" (created)=\"onCreated($event)\" (removed)=\"onRemoved($event)\"\n (closed)=\"closeDetail()\" (labelCreated)=\"onLabelCreated($event)\"\n (mergeRequested)=\"startMerge($event)\" (openTask)=\"openTask($event)\" (unmerged)=\"onUnmerged()\" />\n\n<!-- \"Agrupar em\u2026\" (do \"\u22EF\" do cart\u00E3o ou do detalhe): as abertas do quadro, com a pr\u00E9via do servidor. -->\n<spine-suggestion-merge-picker [clientId]=\"clientId()\" [source]=\"mergeSource()\" [candidates]=\"all()\"\n (merged)=\"onMerged($event)\" (closed)=\"mergeSource.set(null)\" />\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}.as-tok--cat-1{--as-tok: var(--hm-data-categorical-1);background:var(--as-tok)}.as-tok--cat-2{--as-tok: var(--hm-data-categorical-2);background:var(--as-tok)}.as-tok--cat-3{--as-tok: var(--hm-data-categorical-3);background:var(--as-tok)}.as-tok--cat-4{--as-tok: var(--hm-data-categorical-4);background:var(--as-tok)}.as-tok--cat-5{--as-tok: var(--hm-data-categorical-5);background:var(--as-tok)}.as-tok--cat-6{--as-tok: var(--hm-data-categorical-6);background:var(--as-tok)}.as-tok--cat-7{--as-tok: var(--hm-data-categorical-7);background:var(--as-tok)}.as-tok--cat-8{--as-tok: var(--hm-data-categorical-8);background:var(--as-tok)}.as-tok--grad-1{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-2))}.as-tok--grad-2{--as-tok: var(--hm-data-categorical-3);background:linear-gradient(135deg,var(--hm-data-categorical-3),var(--hm-data-categorical-4))}.as-tok--grad-3{--as-tok: var(--hm-data-categorical-5);background:linear-gradient(135deg,var(--hm-data-categorical-5),var(--hm-data-categorical-6))}.as-tok--grad-4{--as-tok: var(--hm-data-categorical-7);background:linear-gradient(135deg,var(--hm-data-categorical-7),var(--hm-data-categorical-8))}.as-tok--grad-5{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-5))}.as-tok--grad-6{--as-tok: var(--hm-data-categorical-2);background:linear-gradient(135deg,var(--hm-data-categorical-2),var(--hm-data-categorical-6))}.as-label{display:inline-flex;align-items:center;gap:var(--hm-space-micro);max-inline-size:100%;padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:color-mix(in srgb,var(--as-tok, var(--hm-border)) 18%,var(--hm-surface-raised));color:var(--hm-text);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);line-height:1.6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.as-label:before{flex:none;inline-size:.5rem;block-size:.5rem;border-radius:50%;background:var(--as-tok, var(--hm-border));content:\"\"}@media(forced-colors:active){.as-label{border:var(--hm-border-hairline) solid CanvasText}}:host{display:block;container-type:inline-size}.as-sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}.as-suggestions__bar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.as-suggestions__spacer{flex:1}.as-split{display:inline-flex;align-items:stretch}.as-split ::ng-deep hm-button .hm-btn{border-start-end-radius:0;border-end-end-radius:0}.as-split ::ng-deep .as-split__more .hm-menu-button{--hm-icon-btn-color: var(--hm-on-action);--hm-icon-btn-hover: var(--hm-action-hover);block-size:var(--hm-control-height);border-inline-start:var(--hm-border-hairline) solid color-mix(in srgb,var(--hm-on-action) 40%,transparent);border-start-start-radius:0;border-end-start-radius:0;background:var(--hm-action)}.as-kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(12rem,1fr));gap:var(--hm-space-sm)}.as-kpis[aria-busy=true] .as-kpi__value{color:var(--hm-muted)}.as-kpi{--as-tone: var(--hm-muted);--as-tone-bg: var(--hm-hover);--as-tint: 0%;display:flex;align-items:flex-start;gap:var(--hm-space-sm);box-sizing:border-box;min-inline-size:0;margin:0;padding:var(--hm-space-sm);border:var(--hm-border-hairline) solid color-mix(in srgb,var(--as-tone) 35%,var(--hm-border));border-radius:var(--hm-radius);background:color-mix(in srgb,var(--as-tone) var(--as-tint),var(--hm-surface-raised));color:var(--hm-text);font:inherit;text-align:start}.as-kpi[data-tone=danger]{--as-tone: var(--hm-danger);--as-tone-bg: var(--hm-danger-bg);--as-tint: 7%}.as-kpi[data-tone=warning]{--as-tone: var(--hm-warning);--as-tone-bg: var(--hm-warning-bg);--as-tint: 7%}.as-kpi[data-tone=info]{--as-tone: var(--hm-info);--as-tone-bg: var(--hm-info-bg);--as-tint: 7%}.as-kpi[data-tone=success]{--as-tone: var(--hm-success);--as-tone-bg: var(--hm-success-bg);--as-tint: 7%}.as-kpi[data-tone=accent]{--as-tone: var(--hm-accent);--as-tone-bg: var(--hm-accent-subtle);--as-tint: 7%}.as-kpi--wide{grid-column:span 2}.as-kpi__icon{display:inline-flex;flex:none;align-items:center;justify-content:center;inline-size:var(--hm-control-height-large);block-size:var(--hm-control-height-large);border-radius:var(--hm-radius);background:var(--as-tone-bg);color:var(--as-tone)}.as-kpi__icon hm-icon{--hm-icon-size: calc(var(--hm-control-height-large) * .55)}.as-kpi__body{display:flex;flex:1;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.as-kpi__label{color:var(--hm-muted);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-medium)}.as-kpi__value{color:var(--hm-text);font-size:var(--hm-font-size-xxl);font-weight:var(--hm-font-weight-bold);font-variant-numeric:tabular-nums;line-height:var(--hm-line-height-tight)}.as-kpi__note{display:inline-flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-micro);color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-kpi__note--up{color:var(--hm-danger);font-weight:var(--hm-font-weight-semibold)}.as-kpi__note--good{color:var(--hm-success);font-weight:var(--hm-font-weight-semibold)}.as-kpi__parts,.as-kpi__legend{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-micro) var(--hm-space-xs);font-size:var(--hm-font-size-xs)}.as-kpi__part{display:inline-flex;align-items:center;gap:var(--hm-space-micro);color:var(--hm-muted);white-space:nowrap}.as-kpi__dot{flex:none;inline-size:.5rem;block-size:.5rem;border-radius:50%;background:var(--as-dot, var(--hm-muted))}.as-kpi__part[data-status=em_analise]{--as-dot: var(--hm-info)}.as-kpi__part[data-status=planejada]{--as-dot: var(--hm-accent)}.as-kpi__part[data-status=em_desenvolvimento]{--as-dot: var(--hm-warning)}.as-kpi__bar{display:flex;gap:var(--hm-border-thin);block-size:var(--hm-space-xs);margin-block:var(--hm-space-micro);overflow:hidden;border-radius:var(--hm-radius-pill);background:var(--hm-hover)}.as-kpi__seg{flex:none;min-inline-size:var(--hm-space-micro);background:var(--as-dot, var(--hm-muted))}.as-kpi [data-priority=urgente]{--as-dot: var(--hm-danger)}.as-kpi [data-priority=alta]{--as-dot: var(--hm-warning)}.as-kpi [data-priority=media]{--as-dot: var(--hm-info)}.as-kpi [data-priority=baixa]{--as-dot: var(--hm-muted)}.as-kpi__chip{display:inline-flex;align-items:center;gap:var(--hm-space-micro);padding:0 var(--hm-space-xxxs);border:var(--hm-border-hairline) solid transparent;border-radius:var(--hm-radius-pill);background:none;color:var(--hm-text);font:inherit;font-variant-numeric:tabular-nums;white-space:nowrap;cursor:pointer}.as-kpi__chip:hover:not(:disabled){background:var(--hm-hover)}.as-kpi__chip:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color)}.as-kpi__chip[aria-pressed=true]{border-color:var(--hm-text);background:var(--hm-hover);font-weight:var(--hm-font-weight-semibold)}.as-kpi__chip:disabled{color:var(--hm-muted);cursor:default}button.as-kpi{cursor:pointer;transition:border-color var(--hm-duration-quick) var(--hm-ease-standard),background var(--hm-duration-quick) var(--hm-ease-standard)}button.as-kpi:hover{border-color:var(--as-tone)}button.as-kpi:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:var(--hm-border-hairline)}button.as-kpi[aria-pressed=true]{--as-tint: 16%;border-color:var(--as-tone);box-shadow:inset 0 0 0 var(--hm-border-thin) var(--as-tone)}.as-filters{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.as-filters__search{flex:3 1 16rem;min-inline-size:0}.as-filters__select{flex:1 1 9rem;min-inline-size:8rem;max-inline-size:13rem}.as-filters__spacer{flex:1 0 0}.as-filters__more{display:inline-flex;flex:none;align-items:center;gap:var(--hm-space-nano);box-sizing:border-box;block-size:var(--hm-control-height);padding-inline:var(--hm-space-xs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised);color:var(--hm-text);font:inherit;font-size:var(--hm-font-size-sm);white-space:nowrap;cursor:pointer}.as-filters__more:hover{background:var(--hm-hover)}.as-filters__more:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color)}.as-filters__more[aria-expanded=true],.as-filters__more--on{border-color:var(--hm-action);color:var(--hm-action);font-weight:var(--hm-font-weight-semibold)}.as-suggestions__status{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs) var(--hm-space-sm)}.as-suggestions__hint{flex:1 1 20rem;margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-suggestions__count{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.as-suggestions__filters{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs) var(--hm-space-sm)}.as-suggestions__search{flex:1 1 16rem;min-inline-size:0}.as-suggestions__select{flex:0 1 11rem;min-inline-size:9rem}.as-board{--hm-kanban-column-width: 17.5rem}.as-board ::ng-deep .hm-kanban__board{min-block-size:max(26rem,100dvh - 24rem)}.as-board ::ng-deep .hm-kanban-column{--as-status: var(--hm-border);flex:1 0 var(--hm-kanban-column-width);max-inline-size:36rem;gap:var(--hm-space-xs);border-block-start:var(--hm-border-thin) solid var(--as-status);background:color-mix(in srgb,var(--as-status) 9%,var(--hm-kanban-column-bg))}.as-board ::ng-deep .hm-kanban-column--collapsed{flex:0 0 var(--hm-kanban-collapsed-width)}.as-board ::ng-deep .hm-kanban-column[data-column=em_analise]{--as-status: var(--hm-info)}.as-board ::ng-deep .hm-kanban-column[data-column=planejada]{--as-status: var(--hm-accent)}.as-board ::ng-deep .hm-kanban-column[data-column=em_desenvolvimento]{--as-status: var(--hm-warning)}.as-board ::ng-deep .hm-kanban-column[data-column=concluida]{--as-status: var(--hm-success)}.as-board ::ng-deep .hm-kanban-column__title:before{display:inline-block;inline-size:.625rem;block-size:.625rem;margin-inline-end:var(--hm-space-xs);border-radius:50%;background:var(--as-status);content:\"\"}.as-board ::ng-deep .hm-kanban-column__count{padding-inline:var(--hm-space-xs);font-weight:var(--hm-font-weight-semibold)}.as-board ::ng-deep .hm-kanban-column__summary{padding-inline:var(--hm-space-micro)}.as-board ::ng-deep .hm-kanban-column__list{gap:var(--hm-space-xs)}.as-board ::ng-deep .hm-kanban-card{padding:0;overflow:hidden}.as-board ::ng-deep .hm-kanban-card:has(.as-card--top){border-color:var(--hm-warning)}.as-colsum{display:inline-flex;align-items:center;gap:var(--hm-space-micro);white-space:nowrap}.as-colsum--late{color:var(--hm-danger);font-weight:var(--hm-font-weight-semibold)}.as-colempty{display:flex;flex-direction:column;align-items:center;gap:var(--hm-space-xxxs);padding:var(--hm-space-lg) var(--hm-space-sm);text-align:center}.as-colempty__icon{--hm-icon-size: var(--hm-control-height-large);color:var(--hm-muted);opacity:.6}.as-colempty__title{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.as-colempty__hint{max-inline-size:16rem;color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-colfoot{display:flex;align-items:center;gap:var(--hm-space-nano);box-sizing:border-box;inline-size:100%;block-size:var(--hm-control-height-dense);padding-inline:var(--hm-space-xs);border:0;border-radius:var(--hm-radius-control);background:transparent;color:var(--hm-muted);font:inherit;font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold);cursor:pointer}.as-colfoot:hover{background:var(--hm-hover);color:var(--hm-text)}.as-colfoot:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color)}.as-card{display:flex;flex-direction:column;min-inline-size:0}.as-card__cover{block-size:2.5rem}.as-card__body{display:flex;flex-direction:column;gap:var(--hm-space-xxxs);padding:var(--hm-space-sm);min-inline-size:0}.as-card__top{display:flex;align-items:center;gap:var(--hm-space-xxxs) var(--hm-space-xs);min-inline-size:0}.as-card__key{flex:none;color:var(--hm-muted);font-family:var(--hm-font-mono, monospace);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold)}.as-card__menu{margin-inline-start:auto}.as-card__priority{display:inline-flex;align-items:center;gap:var(--hm-space-micro);padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:var(--hm-hover);color:var(--hm-text);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);line-height:1.6;white-space:nowrap}.as-card__priority hm-icon{--hm-icon-size: .875rem;color:var(--hm-muted)}.as-card__priority[data-priority=urgente]{background:var(--hm-danger-bg);color:var(--hm-danger)}.as-card__priority[data-priority=urgente] hm-icon{color:var(--hm-danger)}.as-card__priority[data-priority=alta]{background:var(--hm-warning-bg)}.as-card__priority[data-priority=alta] hm-icon{color:var(--hm-warning)}.as-card__priority[data-priority=media]{background:var(--hm-info-bg)}.as-card__priority[data-priority=media] hm-icon{color:var(--hm-info)}.as-card__badge{display:inline-flex;align-items:center;gap:var(--hm-space-micro);padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:var(--hm-warning-bg);color:var(--hm-text);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);white-space:nowrap}.as-card__title{margin-block-start:var(--hm-space-micro);padding:0;border:0;background:none;color:var(--hm-text);font:inherit;font-weight:var(--hm-font-weight-bold);line-height:var(--hm-line-height-snug);text-align:start;overflow-wrap:anywhere;cursor:pointer}.as-card__title:hover,.as-card__title:focus-visible{color:var(--hm-accent);text-decoration:underline}.as-card__context{display:inline-flex;align-items:center;gap:var(--hm-space-micro);min-inline-size:0;color:var(--hm-action);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-medium);overflow-wrap:anywhere}.as-card__context hm-icon{flex:none}.as-card__context[data-kind=melhoria] hm-icon{color:var(--hm-accent)}.as-card__context[data-kind=nova_funcionalidade] hm-icon{color:var(--hm-info)}.as-card__context[data-kind=problema] hm-icon{color:var(--hm-danger)}.as-card__labels{display:flex;flex-wrap:wrap;gap:var(--hm-space-micro)}.as-card__excerpt{display:-webkit-box;margin:0;overflow:hidden;color:var(--hm-muted);font-size:var(--hm-font-size-sm);-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2}.as-card__checklist{display:flex;flex-direction:column;gap:1px;margin:var(--hm-space-micro) 0 0;padding:var(--hm-space-micro);border-radius:var(--hm-radius-control);background:var(--hm-hover);list-style:none}.as-card__check{display:flex;align-items:flex-start;gap:var(--hm-space-micro);inline-size:100%;padding:1px var(--hm-space-micro);border:0;border-radius:var(--hm-radius-control);background:none;color:var(--hm-text);font:inherit;font-size:var(--hm-font-size-sm);text-align:start;cursor:pointer}.as-card__check[aria-pressed=true] hm-icon{color:var(--hm-success)}.as-card__check:hover{background:var(--hm-hover)}.as-card__check:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color)}.as-card__done{color:var(--hm-muted);text-decoration:line-through}.as-card__due{display:inline-flex;align-self:flex-start;align-items:center;gap:var(--hm-space-micro);margin-block-start:var(--hm-space-micro);padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:var(--hm-hover);color:var(--hm-muted);font-size:var(--hm-font-size-xs);line-height:1.6;white-space:nowrap}.as-card__due[data-due=overdue]{background:var(--hm-danger-bg);color:var(--hm-danger);font-weight:var(--hm-font-weight-semibold)}.as-card__due[data-due=today],.as-card__due[data-due=tomorrow]{background:var(--hm-warning-bg);color:var(--hm-text);font-weight:var(--hm-font-weight-semibold)}.as-card__due[data-due=today] hm-icon,.as-card__due[data-due=tomorrow] hm-icon{color:var(--hm-warning)}.as-card__foot{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs) var(--hm-space-sm);margin-block-start:var(--hm-space-xxxs);padding-block-start:var(--hm-space-xs);border-block-start:var(--hm-border-hairline) solid var(--hm-border)}.as-card__count,.as-card__date{display:inline-flex;align-items:center;gap:var(--hm-space-micro);color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-variant-numeric:tabular-nums;white-space:nowrap}.as-card__count--on{color:var(--hm-text);font-weight:var(--hm-font-weight-semibold)}.as-card__count--full{color:var(--hm-success)}.as-card__count--zero{opacity:.75}.as-card__faces{display:inline-flex;align-items:center;margin-inline-start:auto}.as-card__faces>hm-account-avatar{border-radius:50%;box-shadow:0 0 0 var(--hm-border-thin) var(--hm-kanban-card-bg, var(--hm-surface-raised))}.as-card__faces>hm-account-avatar+hm-account-avatar{margin-inline-start:calc(var(--hm-space-xs) * -1)}.as-card__more{margin-inline-start:var(--hm-space-micro);color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold)}.as-card__nobody{display:inline-flex;color:var(--hm-muted);opacity:.7}.as-archive{display:flex;flex-direction:column;gap:var(--hm-space-xs);margin:0;padding:0;list-style:none}.as-archive[aria-busy=true]{opacity:.6}.as-archive__item{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm);padding:var(--hm-space-sm);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.as-archive__main{display:flex;flex:1 1 20rem;flex-direction:column;gap:var(--hm-space-xxxs);min-inline-size:0}.as-archive__tags,.as-archive__meta{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs) var(--hm-space-xs)}.as-archive__meta,.as-archive__date{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.as-archive__link{padding:0;border:0;background:none;color:var(--hm-accent);font:inherit;font-size:var(--hm-font-size-sm);cursor:pointer}.as-archive__link:hover{text-decoration:underline}.as-archive__link:focus-visible{outline:var(--hm-border-thin) solid var(--hm-accent);outline-offset:var(--hm-border-thin)}@media(forced-colors:active){.as-kpi,.as-card__priority,.as-card__due{border:var(--hm-border-hairline) solid CanvasText}button.as-kpi[aria-pressed=true],.as-kpi__chip[aria-pressed=true]{outline:var(--hm-border-thin) solid Highlight}}@container (max-width: 52rem){.as-kpi--wide{grid-column:1/-1}}@container (max-width: 40rem){.as-kpis{grid-template-columns:repeat(auto-fit,minmax(9.5rem,1fr))}.as-kpi{gap:var(--hm-space-xs);padding:var(--hm-space-xs)}.as-kpi__icon{inline-size:var(--hm-control-height);block-size:var(--hm-control-height)}.as-kpi__icon hm-icon{--hm-icon-size: calc(var(--hm-control-height) * .55)}.as-kpi__value{font-size:var(--hm-font-size-xl)}.as-board{--hm-kanban-column-width: min(18rem, 85cqi)}.as-board ::ng-deep .hm-kanban__board{min-block-size:0;scroll-snap-type:x proximity;scroll-padding-inline:var(--hm-space-xs)}.as-board ::ng-deep .hm-kanban-column{flex:0 0 var(--hm-kanban-column-width);scroll-snap-align:start}.as-board ::ng-deep .hm-kanban-column--collapsed{flex:0 0 var(--hm-kanban-collapsed-width)}.as-filters__select,.as-suggestions__select{flex:1 1 9rem;max-inline-size:none}.as-filters__spacer{display: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: EmptyComponent, selector: "hm-empty", inputs: ["heading", "description", "icon", "appearance", "size", "headingLevel"] }, { kind: "component", type: IconButtonComponent, selector: "hm-icon-button", inputs: ["label", "icon", "solid", "iconDirection", "emphasis", "intent", "size", "disabled", "toggle", "pressed", "keyShortcuts"], outputs: ["pressedChange", "clicked"] }, { kind: "component", type: IconComponent, selector: "hm-icon", inputs: ["shape", "direction", "size", "solid", "badge", "status", "flip", "inverse", "label"] }, { kind: "directive", type: KanbanCardDirective, selector: "ng-template[hmKanbanCard]" }, { kind: "directive", type: KanbanColumnActionsDirective, selector: "ng-template[hmKanbanColumnActions]" }, { kind: "directive", type: KanbanColumnEmptyDirective, selector: "ng-template[hmKanbanColumnEmpty]" }, { kind: "directive", type: KanbanColumnFooterDirective, selector: "ng-template[hmKanbanColumnFooter]" }, { kind: "directive", type: KanbanColumnSummaryDirective, selector: "ng-template[hmKanbanColumnSummary]" }, { kind: "component", type: KanbanComponent, selector: "hm-kanban", inputs: ["columns", "collapsed", "canMove", "itemKey", "itemLabel", "label", "collapsible", "headingLevel", "disabled"], outputs: ["columnsChange", "collapsedChange", "moved"] }, { 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: PopoverComponent, selector: "hm-popover", inputs: ["open", "heading", "headingLevel", "label", "side", "align", "arrow", "closable", "closeLabel", "closeOnOutsideClick", "openOn", "openDelay", "closeDelay"], outputs: ["openChange"] }, { 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: SelectComponent, selector: "hm-select", inputs: ["value", "label", "labelHidden", "hint"], outputs: ["valueChange"] }, { kind: "component", type: SuggestionCoverPickerComponent, selector: "spine-suggestion-cover-picker", inputs: ["value"], outputs: ["picked"] }, { kind: "component", type: SuggestionMergePickerComponent, selector: "spine-suggestion-merge-picker", inputs: ["clientId", "source", "candidates"], outputs: ["merged", "closed"] }, { kind: "component", type: SuggestionTaskDetailComponent, selector: "spine-suggestion-task-detail", inputs: ["clientId", "task", "draft", "draftStatus", "labels", "canOpenScreen"], outputs: ["changed", "created", "removed", "closed", "labelCreated", "mergeRequested", "openTask", "unmerged"] }, { kind: "component", type: TagComponent, selector: "hm-tag", inputs: ["name", "color", "compact"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2079
|
+
}
|
|
2080
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceSuggestionsComponent, decorators: [{
|
|
2081
|
+
type: Component,
|
|
2082
|
+
args: [{ selector: 'spine-auto-service-suggestions', imports: [
|
|
2083
|
+
AccountAvatarComponent, AlertComponent, ButtonComponent, EmptyComponent, IconButtonComponent, IconComponent,
|
|
2084
|
+
KanbanCardDirective, KanbanColumnActionsDirective, KanbanColumnEmptyDirective, KanbanColumnFooterDirective,
|
|
2085
|
+
KanbanColumnSummaryDirective, KanbanComponent, MenuButtonComponent, PaginatorComponent, PopoverComponent,
|
|
2086
|
+
SearchFieldComponent, SegmentedControlComponent, SelectComponent, SuggestionCoverPickerComponent,
|
|
2087
|
+
SuggestionMergePickerComponent, SuggestionTaskDetailComponent, TagComponent,
|
|
2088
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"as-stack as-suggestions\">\n <!-- Quadro x Arquivo, o atualizar e a nova tarefa (com o \"\u25BE\" para criar direto numa coluna). -->\n <div class=\"as-suggestions__bar\">\n <hm-segmented-control [label]=\"text.viewLabel\" [options]=\"modeOptions\" [value]=\"mode()\" (valueChange)=\"setMode($event)\" />\n <span class=\"as-suggestions__spacer\"></span>\n <hm-icon-button emphasis=\"subtle\" icon=\"refresh\" [label]=\"text.refresh\" (clicked)=\"refresh()\" />\n <span class=\"as-split\" role=\"group\" [attr.aria-label]=\"text.newTask\">\n <hm-button icon=\"plus\" (clicked)=\"newTask()\">{{ text.newTask }}</hm-button>\n <hm-menu-button class=\"as-split__more\" icon=\"chevron-down\" [showArrow]=\"false\" align=\"end\" [label]=\"text.newTaskOptions\"\n [items]=\"newTaskItems\" (chosen)=\"newTask($any($event))\" />\n </span>\n </div>\n\n @if (mode() === 'board') {\n @if (loadError()) {\n <hm-alert variant=\"error\">{{ loadError() }}</hm-alert>\n } @else {\n <!-- Indicadores do quadro inteiro (o filtro n\u00E3o mexe neles). Os bot\u00F5es filtram: vencidas, vencendo em breve,\n atribu\u00EDdas a mim e cada prioridade da legenda. Cor sempre com texto junto. -->\n <section class=\"as-kpis\" [attr.aria-label]=\"text.kpis\" [attr.aria-busy]=\"kpiLoading() ? 'true' : null\">\n <button type=\"button\" class=\"as-kpi\" data-tone=\"danger\" [attr.aria-pressed]=\"due() === 'overdue'\"\n (click)=\"toggleKpi('overdue', due() !== 'overdue')\">\n <span class=\"as-kpi__icon\" aria-hidden=\"true\"><hm-icon shape=\"alarm-clock\" /></span>\n <span class=\"as-kpi__body\">\n <span class=\"as-kpi__label\">{{ text.kpiOverdue }}</span>\n <span class=\"as-kpi__value\">{{ kpiLoading() ? '\u2014' : stats().overdue }}</span>\n <span class=\"as-kpi__note\" [class.as-kpi__note--up]=\"stats().overdueNew > 0\">\n @if (stats().overdueNew) { <hm-icon shape=\"arrow-up\" /> }{{ overdueNote() }}\n </span>\n </span>\n </button>\n\n <button type=\"button\" class=\"as-kpi\" data-tone=\"warning\" [attr.aria-pressed]=\"due() === 'soon'\"\n (click)=\"toggleKpi('soon', due() !== 'soon')\">\n <span class=\"as-kpi__icon\" aria-hidden=\"true\"><hm-icon shape=\"hourglass\" /></span>\n <span class=\"as-kpi__body\">\n <span class=\"as-kpi__label\">{{ text.kpiSoon }}</span>\n <span class=\"as-kpi__value\">{{ kpiLoading() ? '\u2014' : stats().soon }}</span>\n <span class=\"as-kpi__note\">{{ text.dueSoonFilter }}</span>\n </span>\n </button>\n\n <div class=\"as-kpi\" data-tone=\"info\">\n <span class=\"as-kpi__icon\" aria-hidden=\"true\"><hm-icon [shape]=\"statusIcons.em_desenvolvimento\" /></span>\n <span class=\"as-kpi__body\">\n <span class=\"as-kpi__label\">{{ text.kpiOpen }}</span>\n <span class=\"as-kpi__value\">{{ kpiLoading() ? '\u2014' : stats().open }}</span>\n <span class=\"as-kpi__parts\">\n @for (part of openParts(); track part.status) {\n <span class=\"as-kpi__part\" [attr.data-status]=\"part.status\"><span class=\"as-kpi__dot\" aria-hidden=\"true\"></span>{{ part.text }}</span>\n }\n </span>\n </span>\n </div>\n\n <div class=\"as-kpi\" data-tone=\"success\">\n <span class=\"as-kpi__icon\" aria-hidden=\"true\"><hm-icon [shape]=\"statusIcons.concluida\" /></span>\n <span class=\"as-kpi__body\">\n <span class=\"as-kpi__label\">{{ doneLabel() }}</span>\n <span class=\"as-kpi__value\">{{ kpiLoading() ? '\u2014' : stats().concluida }}</span>\n <span class=\"as-kpi__note\" [class.as-kpi__note--good]=\"stats().doneToday > 0\">\n @if (stats().doneToday) { <hm-icon shape=\"arrow-up\" /> }{{ doneNote() }}\n </span>\n </span>\n </div>\n\n <button type=\"button\" class=\"as-kpi\" data-tone=\"accent\" [attr.aria-pressed]=\"assignee() === 'me'\"\n (click)=\"toggleKpi('mine', assignee() !== 'me')\">\n <span class=\"as-kpi__icon\" aria-hidden=\"true\"><hm-icon shape=\"user-checkmark\" /></span>\n <span class=\"as-kpi__body\">\n <span class=\"as-kpi__label\">{{ text.kpiMine }}</span>\n <span class=\"as-kpi__value\">{{ kpiLoading() ? '\u2014' : stats().mine }}</span>\n <span class=\"as-kpi__note\">{{ text.kpiMineHint }}</span>\n </span>\n </button>\n\n <div class=\"as-kpi as-kpi--wide\" data-tone=\"neutral\">\n <span class=\"as-kpi__icon\" aria-hidden=\"true\"><hm-icon shape=\"bar-chart\" /></span>\n <span class=\"as-kpi__body\">\n <span class=\"as-kpi__label\">{{ text.kpiPriority }}</span>\n @if (stats().open) {\n <span class=\"as-kpi__bar\" aria-hidden=\"true\">\n @for (p of stats().byPriority; track p.priority) {\n @if (p.count) {\n <span class=\"as-kpi__seg\" [attr.data-priority]=\"p.priority\" [style.inline-size.%]=\"priorityShare(p.count)\"></span>\n }\n }\n </span>\n <span class=\"as-kpi__legend\">\n @for (p of stats().byPriority; track p.priority) {\n <button type=\"button\" class=\"as-kpi__chip\" [attr.data-priority]=\"p.priority\" [attr.aria-pressed]=\"priority() === p.priority\"\n [disabled]=\"!p.count && priority() !== p.priority\" (click)=\"togglePriority(p.priority)\">\n <span class=\"as-kpi__dot\" aria-hidden=\"true\"></span>{{ priorityPart(p.priority, p.count) }}\n </button>\n }\n </span>\n } @else {\n <span class=\"as-kpi__note\">{{ text.kpiPriorityNone }}</span>\n }\n </span>\n </div>\n </section>\n\n <!-- Filtros numa linha: busca larga, os mais usados e \"Mais filtros\" (tela, quem pediu); \u00E0 direita, a ordena\u00E7\u00E3o.\n Valem sobre a lista inteira: a posi\u00E7\u00E3o gravada ao arrastar \u00E9 sempre a da coluna inteira. -->\n <div class=\"as-filters\" role=\"group\" [attr.aria-label]=\"text.filters\">\n <hm-search-field class=\"as-filters__search\" [label]=\"text.search\" labelHidden [placeholder]=\"text.search\"\n [landmark]=\"false\" [debounce]=\"150\" [value]=\"search()\" (search)=\"search.set($event)\" />\n <hm-select class=\"as-filters__select\" [label]=\"text.assigneeFilter\" labelHidden [(value)]=\"assignee\">\n <option value=\"\">{{ text.allAssignees }}</option>\n <option value=\"me\">{{ text.assigneeMine }}</option>\n <option value=\"none\">{{ text.assigneeNone }}</option>\n @for (p of assigneeOptions(); track p.user_id) {\n <option [value]=\"p.user_id\">{{ personName(p) }}</option>\n }\n </hm-select>\n <hm-select class=\"as-filters__select\" [label]=\"text.kindFilter\" labelHidden [(value)]=\"kind\">\n <option value=\"\">{{ text.allKinds }}</option>\n @for (k of kinds; track k) {\n <option [value]=\"k\">{{ text.kinds[k] }}</option>\n }\n </hm-select>\n <hm-select class=\"as-filters__select\" [label]=\"text.statusFilter\" labelHidden [(value)]=\"status\">\n <option value=\"\">{{ text.allBoardStatuses }}</option>\n @for (st of boardStatuses; track st) {\n <option [value]=\"st\">{{ text.statuses[st] }}</option>\n }\n </hm-select>\n <hm-select class=\"as-filters__select\" [label]=\"text.priorityFilter\" labelHidden [(value)]=\"priority\">\n <option value=\"\">{{ text.allPriorities }}</option>\n @for (p of priorities; track p) {\n <option [value]=\"p\">{{ text.priorities[p] }}</option>\n }\n </hm-select>\n <hm-select class=\"as-filters__select\" [label]=\"text.labelFilter\" labelHidden [(value)]=\"label\">\n <option value=\"\">{{ text.allLabels }}</option>\n @for (l of catalog(); track l.id) {\n <option [value]=\"l.id\">{{ l.name }}</option>\n }\n </hm-select>\n <hm-select class=\"as-filters__select\" [label]=\"text.dueFilter\" labelHidden [(value)]=\"due\">\n <option value=\"\">{{ text.dueAny }}</option>\n <option value=\"overdue\">{{ text.dueOverdueFilter }}</option>\n <option value=\"soon\">{{ text.dueSoonFilter }}</option>\n </hm-select>\n <button type=\"button\" class=\"as-filters__more\" [class.as-filters__more--on]=\"extraFilters() > 0\"\n [attr.aria-expanded]=\"moreFilters()\" [attr.aria-controls]=\"moreFilters() ? 'as-more-filters' : null\"\n (click)=\"moreFilters.set(!moreFilters())\">\n <hm-icon shape=\"filter\" />{{ moreFiltersText() }}\n </button>\n <span class=\"as-filters__spacer\"></span>\n <hm-select class=\"as-filters__select as-filters__sort\" [label]=\"text.sort\" labelHidden [value]=\"sort()\" (valueChange)=\"sort.set($any($event))\">\n @for (k of sortKeys; track k) {\n <option [value]=\"k\">{{ text.sort }}: {{ sortLabel(k) }}</option>\n }\n </hm-select>\n </div>\n @if (moreFilters()) {\n <div class=\"as-filters as-filters--more\" id=\"as-more-filters\">\n <hm-select class=\"as-filters__select\" [label]=\"text.screenFilter\" labelHidden [(value)]=\"screen\">\n <option value=\"\">{{ text.allScreens }}</option>\n @for (sc of screens(); track sc.path) {\n <option [value]=\"sc.path\">{{ sc.label }}</option>\n }\n </hm-select>\n <hm-select class=\"as-filters__select\" [label]=\"text.authorFilter\" labelHidden [(value)]=\"author\">\n <option value=\"\">{{ text.allAuthors }}</option>\n @for (p of authors(); track p.user_id) {\n <option [value]=\"p.user_id\">{{ personName(p) }}</option>\n }\n </hm-select>\n </div>\n }\n <div class=\"as-suggestions__status\">\n @if (sort() !== 'position') {\n <p class=\"as-suggestions__hint\">{{ text.sortHint }}</p>\n }\n @if (isFiltered()) {\n <span class=\"as-suggestions__count\" aria-live=\"polite\">{{ filteredText() }}</span>\n <hm-button size=\"small\" emphasis=\"minimal\" icon=\"filter-close\" (clicked)=\"clearFilters()\">{{ text.clearFilters }}</hm-button>\n }\n </div>\n\n @if (!loading() && all().length === 0) {\n <hm-empty icon=\"lightbulb\" [heading]=\"text.boardEmpty\" [description]=\"text.boardEmptyHint\" />\n }\n\n <hm-kanban class=\"as-board\" [label]=\"text.boardLabel\" [columns]=\"columns()\" [itemKey]=\"itemKey\" [itemLabel]=\"itemLabel\"\n (moved)=\"onMoved($event)\">\n <!-- \"+\" no cabe\u00E7alho: abre o cadastro completo (o modal da \"Nova tarefa\") j\u00E1 nesta coluna. -->\n <ng-template hmKanbanColumnActions let-column>\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"plus\" [label]=\"newTaskInLabel(column.id)\" (clicked)=\"newTask(column.id)\" />\n </ng-template>\n\n <!-- Logo abaixo do cabe\u00E7alho: h\u00E1 quanto tempo a mais antiga est\u00E1 no status e quantas venceram. -->\n <ng-template hmKanbanColumnSummary let-column>\n @if (columnSummaries().get(column.id); as sum) {\n @if (sum.oldest) {\n <span class=\"as-colsum\"><hm-icon shape=\"clock\" />{{ sum.oldest }}</span>\n }\n @if (sum.overdue) {\n <span class=\"as-colsum as-colsum--late\"><hm-icon shape=\"alarm-clock\" />{{ sum.overdue }}</span>\n }\n }\n </ng-template>\n\n <!-- Coluna vazia: \u00EDcone grande, a frase e o que costuma cair nela (com filtro, \"nenhuma com estes filtros\"). -->\n <ng-template hmKanbanColumnEmpty let-column>\n <span class=\"as-colempty\">\n <hm-icon class=\"as-colempty__icon\" [shape]=\"emptyIcon(column.id)\" />\n <span class=\"as-colempty__title\">{{ text.columnEmpty }}</span>\n <span class=\"as-colempty__hint\">{{ emptyHint(column.id) }}</span>\n </span>\n </ng-template>\n\n <!-- Rodap\u00E9: \"+ Criar tarefa\" abre o mesmo cadastro, com o status da coluna j\u00E1 escolhido. -->\n <ng-template hmKanbanColumnFooter let-column>\n <button type=\"button\" class=\"as-colfoot\" (click)=\"newTask(column.id)\">\n <hm-icon shape=\"plus\" />{{ text.createTask }}<span class=\"as-sr-only\">: {{ column.title }}</span>\n </button>\n </ng-template>\n\n <ng-template hmKanbanCard let-item>\n <div class=\"as-card\" [class.as-card--top]=\"isTop(item)\">\n @if (item.cover) {\n <div class=\"as-card__cover\" [class]=\"'as-tok--' + item.cover\" aria-hidden=\"true\"></div>\n }\n <div class=\"as-card__body\">\n <div class=\"as-card__top\">\n <span class=\"as-card__key\">{{ item.key }}</span>\n <span class=\"as-card__priority\" [attr.data-priority]=\"item.priority\">\n <hm-icon [shape]=\"priorityIcon(item)\" /><span class=\"as-sr-only\">{{ text.priority }}: </span>{{ priorityName(item) }}\n </span>\n @if (isTop(item)) {\n <span class=\"as-card__badge\"><hm-icon shape=\"flame\" />{{ text.topVoted }}</span>\n }\n <span #menuAnchor class=\"as-card__menu\">\n <hm-menu-button size=\"small\" icon=\"ellipsis-vertical\" [showArrow]=\"false\" align=\"end\" [label]=\"menuLabel(item)\"\n [items]=\"menuOf(item)\" (chosen)=\"onMenu(item, $event, menuAnchor)\" />\n </span>\n </div>\n <button type=\"button\" class=\"as-card__title\" (click)=\"openDetail(item)\">{{ item.title }}</button>\n <span class=\"as-card__context\" [attr.data-kind]=\"item.kind\"><hm-icon [shape]=\"kindIcon(item)\" />{{ contextOf(item) }}</span>\n @if (item.labels.length) {\n <div class=\"as-card__labels\">\n @for (l of item.labels; track l.id) {\n <span class=\"as-label\" [class]=\"'as-tok--' + l.color\">{{ l.name }}</span>\n }\n </div>\n }\n @if (excerpt(item); as ex) {\n <p class=\"as-card__excerpt\">{{ ex }}</p>\n }\n @if (cardItems(item).length) {\n <ul class=\"as-card__checklist\">\n @for (c of cardItems(item); track c.id) {\n <li>\n <button type=\"button\" class=\"as-card__check\" [attr.aria-pressed]=\"c.done\" (click)=\"toggleCardItem(item, c)\">\n <hm-icon [shape]=\"c.done ? 'check-circle' : 'circle-empty'\" /><span [class.as-card__done]=\"c.done\">{{ c.text }}</span>\n </button>\n </li>\n }\n </ul>\n }\n @if (item.due_date) {\n <span class=\"as-card__due\" [attr.data-due]=\"dueOf(item) ?? 'closed'\"><hm-icon [shape]=\"dueIcon(item)\" />{{ dueText(item) }}</span>\n }\n <div class=\"as-card__foot\">\n <span class=\"as-card__count\" [class.as-card__count--zero]=\"!item.comments_count\" [attr.title]=\"commentsText(item.comments_count)\">\n <hm-icon shape=\"comment-message\" /><span aria-hidden=\"true\">{{ item.comments_count }}</span><span class=\"as-sr-only\">{{ commentsText(item.comments_count) }}</span>\n </span>\n @if (item.checklist.total) {\n <span class=\"as-card__count\" [class.as-card__count--full]=\"item.checklist.done === item.checklist.total\" [attr.title]=\"checklistText(item)\">\n <hm-icon shape=\"list-check\" /><span aria-hidden=\"true\">{{ item.checklist.done }}/{{ item.checklist.total }}</span><span class=\"as-sr-only\">{{ checklistText(item) }}</span>\n </span>\n }\n <span class=\"as-card__count\" [class.as-card__count--on]=\"item.votes > 0\" [class.as-card__count--zero]=\"!item.votes\" [attr.title]=\"votesText(item.votes)\">\n <hm-icon shape=\"thumbs-up\" /><span aria-hidden=\"true\">{{ item.votes }}</span><span class=\"as-sr-only\">{{ votesText(item.votes) }}</span>\n </span>\n <span class=\"as-card__date\" [attr.title]=\"createdText(item)\">\n <hm-icon shape=\"clock\" /><span aria-hidden=\"true\">{{ date(item.created_at) }}</span><span class=\"as-sr-only\">{{ createdText(item) }}</span>\n </span>\n <span class=\"as-card__faces\">\n @for (p of faces(item); track p.user_id) {\n <hm-account-avatar size=\"small\" [name]=\"personName(p)\" [imageUrl]=\"p.picture\" [attr.title]=\"personName(p)\" />\n } @empty {\n <span class=\"as-card__nobody\" [attr.title]=\"text.assigneeNone\">\n <hm-icon shape=\"user-circle\" /><span class=\"as-sr-only\">{{ text.assigneeNone }}</span>\n </span>\n }\n @if (moreFaces(item); as more) { <span class=\"as-card__more\">{{ more }}</span> }\n </span>\n </div>\n </div>\n </div>\n </ng-template>\n </hm-kanban>\n }\n } @else {\n <!-- Arquivo: arquivadas, recusadas e conclu\u00EDdas fora da janela do quadro, paginado no servidor. -->\n <div class=\"as-suggestions__filters\">\n <hm-search-field class=\"as-suggestions__search\" [label]=\"text.archiveSearch\" labelHidden [placeholder]=\"text.archiveSearch\"\n [landmark]=\"false\" [debounce]=\"300\" [value]=\"archiveSearch()\" (search)=\"onArchiveSearch($event)\" />\n <hm-select class=\"as-suggestions__select\" [label]=\"text.statusFilter\" labelHidden [value]=\"archiveStatus()\"\n (valueChange)=\"onArchiveFilter('status', $any($event))\">\n <option value=\"\">{{ text.statusFilter }}: {{ text.allStatuses }}</option>\n @for (st of statuses; track st) {\n <option [value]=\"st\">{{ text.statuses[st] }}</option>\n }\n </hm-select>\n <hm-select class=\"as-suggestions__select\" [label]=\"text.kindFilter\" labelHidden [value]=\"archiveKind()\"\n (valueChange)=\"onArchiveFilter('kind', $any($event))\">\n <option value=\"\">{{ text.allKinds }}</option>\n @for (k of kinds; track k) {\n <option [value]=\"k\">{{ text.kinds[k] }}</option>\n }\n </hm-select>\n @if (archiveTotal()) {\n <span class=\"as-suggestions__count\" aria-live=\"polite\">{{ archiveCountText() }}</span>\n }\n </div>\n\n @if (archiveError()) {\n <hm-alert variant=\"error\">{{ archiveError() }}</hm-alert>\n } @else if (!archiveLoading() && archived().length === 0) {\n <hm-empty icon=\"archive\" [heading]=\"text.archiveEmpty\" [description]=\"archiveEmptyHint()\" />\n } @else {\n <ul class=\"as-archive\" [attr.aria-label]=\"text.archiveLabel\" [attr.aria-busy]=\"archiveLoading() ? 'true' : null\">\n @for (s of archived(); track s.id) {\n <li class=\"as-archive__item\">\n <div class=\"as-archive__main\">\n <div class=\"as-archive__tags\">\n <span class=\"as-card__key\">{{ s.key }}</span>\n <hm-tag [name]=\"text.statuses[s.status]\" [color]=\"statusColors[s.status]\" />\n <hm-tag [name]=\"text.kinds[s.kind]\" [color]=\"kindColors[s.kind]\" />\n @if (s.merged_into; as into) {\n <!-- Agrupada: no arquivo, com o destino; os coment\u00E1rios ficam nela. -->\n <hm-tag [name]=\"text.mergedTag\" color=\"info\" />\n <button type=\"button\" class=\"as-archive__link\" (click)=\"openTask(into.id)\">{{ mergedIntoText(into.key) }}</button>\n } @else {\n <span class=\"as-archive__date\">{{ closedOn(s) }}</span>\n }\n </div>\n <button type=\"button\" class=\"as-card__title\" (click)=\"openDetail(s)\">{{ s.title }}</button>\n <div class=\"as-archive__meta\">\n <hm-account-avatar size=\"small\" [name]=\"personName(s.author)\" [imageUrl]=\"s.author?.picture\" />\n <span>{{ personName(s.author) }}</span>\n <span aria-hidden=\"true\">\u00B7</span>\n <span>{{ s.screen.label || text.noScreen }}</span>\n @if (s.votes) {\n <span aria-hidden=\"true\">\u00B7</span>\n <span>{{ votesText(s.votes) }}</span>\n }\n </div>\n </div>\n @if (s.merged_into) {\n <hm-button size=\"small\" emphasis=\"medium\" icon=\"unlink\" [loading]=\"reopening() === s.id\" (clicked)=\"unmergeFromArchive(s)\">\n {{ text.unmerge }}\n </hm-button>\n } @else if (s.archived_at) {\n <hm-button size=\"small\" emphasis=\"medium\" icon=\"unarchive\" [loading]=\"reopening() === s.id\" (clicked)=\"reopen(s)\">\n {{ text.menuRestore }}\n </hm-button>\n } @else if (s.status === 'recusada') {\n <hm-button size=\"small\" emphasis=\"medium\" icon=\"unarchive\" [loading]=\"reopening() === s.id\" (clicked)=\"reopen(s)\">\n {{ text.reopen }}\n </hm-button>\n }\n </li>\n }\n </ul>\n @if (archiveTotal() > archivePage().size) {\n <hm-paginator variant=\"simple\" [label]=\"text.pagination\" [state]=\"archivePage()\" [totalItems]=\"archiveTotal()\"\n [sizes]=\"[20, 50]\" (stateChanged)=\"onArchivePage($event)\" />\n }\n }\n }\n</div>\n\n<!-- Capa pelo \"\u22EF\" do cart\u00E3o: o popover abre ancorado no bot\u00E3o do cart\u00E3o escolhido. -->\n<hm-popover #coverPop [label]=\"text.cover\">\n <spine-suggestion-cover-picker [value]=\"coverTarget()?.cover ?? null\" (picked)=\"pickCover($event)\" />\n</hm-popover>\n\n<spine-suggestion-task-detail [clientId]=\"clientId()\" [task]=\"selected()\" [draft]=\"drafting()\" [draftStatus]=\"draftStatus()\"\n [labels]=\"catalog()\" [canOpenScreen]=\"canOpenScreen()\"\n (changed)=\"replace($event)\" (created)=\"onCreated($event)\" (removed)=\"onRemoved($event)\"\n (closed)=\"closeDetail()\" (labelCreated)=\"onLabelCreated($event)\"\n (mergeRequested)=\"startMerge($event)\" (openTask)=\"openTask($event)\" (unmerged)=\"onUnmerged()\" />\n\n<!-- \"Agrupar em\u2026\" (do \"\u22EF\" do cart\u00E3o ou do detalhe): as abertas do quadro, com a pr\u00E9via do servidor. -->\n<spine-suggestion-merge-picker [clientId]=\"clientId()\" [source]=\"mergeSource()\" [candidates]=\"all()\"\n (merged)=\"onMerged($event)\" (closed)=\"mergeSource.set(null)\" />\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}.as-tok--cat-1{--as-tok: var(--hm-data-categorical-1);background:var(--as-tok)}.as-tok--cat-2{--as-tok: var(--hm-data-categorical-2);background:var(--as-tok)}.as-tok--cat-3{--as-tok: var(--hm-data-categorical-3);background:var(--as-tok)}.as-tok--cat-4{--as-tok: var(--hm-data-categorical-4);background:var(--as-tok)}.as-tok--cat-5{--as-tok: var(--hm-data-categorical-5);background:var(--as-tok)}.as-tok--cat-6{--as-tok: var(--hm-data-categorical-6);background:var(--as-tok)}.as-tok--cat-7{--as-tok: var(--hm-data-categorical-7);background:var(--as-tok)}.as-tok--cat-8{--as-tok: var(--hm-data-categorical-8);background:var(--as-tok)}.as-tok--grad-1{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-2))}.as-tok--grad-2{--as-tok: var(--hm-data-categorical-3);background:linear-gradient(135deg,var(--hm-data-categorical-3),var(--hm-data-categorical-4))}.as-tok--grad-3{--as-tok: var(--hm-data-categorical-5);background:linear-gradient(135deg,var(--hm-data-categorical-5),var(--hm-data-categorical-6))}.as-tok--grad-4{--as-tok: var(--hm-data-categorical-7);background:linear-gradient(135deg,var(--hm-data-categorical-7),var(--hm-data-categorical-8))}.as-tok--grad-5{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-5))}.as-tok--grad-6{--as-tok: var(--hm-data-categorical-2);background:linear-gradient(135deg,var(--hm-data-categorical-2),var(--hm-data-categorical-6))}.as-label{display:inline-flex;align-items:center;gap:var(--hm-space-micro);max-inline-size:100%;padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:color-mix(in srgb,var(--as-tok, var(--hm-border)) 18%,var(--hm-surface-raised));color:var(--hm-text);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);line-height:1.6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.as-label:before{flex:none;inline-size:.5rem;block-size:.5rem;border-radius:50%;background:var(--as-tok, var(--hm-border));content:\"\"}@media(forced-colors:active){.as-label{border:var(--hm-border-hairline) solid CanvasText}}:host{display:block;container-type:inline-size}.as-sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}.as-suggestions__bar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.as-suggestions__spacer{flex:1}.as-split{display:inline-flex;align-items:stretch}.as-split ::ng-deep hm-button .hm-btn{border-start-end-radius:0;border-end-end-radius:0}.as-split ::ng-deep .as-split__more .hm-menu-button{--hm-icon-btn-color: var(--hm-on-action);--hm-icon-btn-hover: var(--hm-action-hover);block-size:var(--hm-control-height);border-inline-start:var(--hm-border-hairline) solid color-mix(in srgb,var(--hm-on-action) 40%,transparent);border-start-start-radius:0;border-end-start-radius:0;background:var(--hm-action)}.as-kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(12rem,1fr));gap:var(--hm-space-sm)}.as-kpis[aria-busy=true] .as-kpi__value{color:var(--hm-muted)}.as-kpi{--as-tone: var(--hm-muted);--as-tone-bg: var(--hm-hover);--as-tint: 0%;display:flex;align-items:flex-start;gap:var(--hm-space-sm);box-sizing:border-box;min-inline-size:0;margin:0;padding:var(--hm-space-sm);border:var(--hm-border-hairline) solid color-mix(in srgb,var(--as-tone) 35%,var(--hm-border));border-radius:var(--hm-radius);background:color-mix(in srgb,var(--as-tone) var(--as-tint),var(--hm-surface-raised));color:var(--hm-text);font:inherit;text-align:start}.as-kpi[data-tone=danger]{--as-tone: var(--hm-danger);--as-tone-bg: var(--hm-danger-bg);--as-tint: 7%}.as-kpi[data-tone=warning]{--as-tone: var(--hm-warning);--as-tone-bg: var(--hm-warning-bg);--as-tint: 7%}.as-kpi[data-tone=info]{--as-tone: var(--hm-info);--as-tone-bg: var(--hm-info-bg);--as-tint: 7%}.as-kpi[data-tone=success]{--as-tone: var(--hm-success);--as-tone-bg: var(--hm-success-bg);--as-tint: 7%}.as-kpi[data-tone=accent]{--as-tone: var(--hm-accent);--as-tone-bg: var(--hm-accent-subtle);--as-tint: 7%}.as-kpi--wide{grid-column:span 2}.as-kpi__icon{display:inline-flex;flex:none;align-items:center;justify-content:center;inline-size:var(--hm-control-height-large);block-size:var(--hm-control-height-large);border-radius:var(--hm-radius);background:var(--as-tone-bg);color:var(--as-tone)}.as-kpi__icon hm-icon{--hm-icon-size: calc(var(--hm-control-height-large) * .55)}.as-kpi__body{display:flex;flex:1;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.as-kpi__label{color:var(--hm-muted);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-medium)}.as-kpi__value{color:var(--hm-text);font-size:var(--hm-font-size-xxl);font-weight:var(--hm-font-weight-bold);font-variant-numeric:tabular-nums;line-height:var(--hm-line-height-tight)}.as-kpi__note{display:inline-flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-micro);color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-kpi__note--up{color:var(--hm-danger);font-weight:var(--hm-font-weight-semibold)}.as-kpi__note--good{color:var(--hm-success);font-weight:var(--hm-font-weight-semibold)}.as-kpi__parts,.as-kpi__legend{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-micro) var(--hm-space-xs);font-size:var(--hm-font-size-xs)}.as-kpi__part{display:inline-flex;align-items:center;gap:var(--hm-space-micro);color:var(--hm-muted);white-space:nowrap}.as-kpi__dot{flex:none;inline-size:.5rem;block-size:.5rem;border-radius:50%;background:var(--as-dot, var(--hm-muted))}.as-kpi__part[data-status=em_analise]{--as-dot: var(--hm-info)}.as-kpi__part[data-status=planejada]{--as-dot: var(--hm-accent)}.as-kpi__part[data-status=em_desenvolvimento]{--as-dot: var(--hm-warning)}.as-kpi__bar{display:flex;gap:var(--hm-border-thin);block-size:var(--hm-space-xs);margin-block:var(--hm-space-micro);overflow:hidden;border-radius:var(--hm-radius-pill);background:var(--hm-hover)}.as-kpi__seg{flex:none;min-inline-size:var(--hm-space-micro);background:var(--as-dot, var(--hm-muted))}.as-kpi [data-priority=urgente]{--as-dot: var(--hm-danger)}.as-kpi [data-priority=alta]{--as-dot: var(--hm-warning)}.as-kpi [data-priority=media]{--as-dot: var(--hm-info)}.as-kpi [data-priority=baixa]{--as-dot: var(--hm-muted)}.as-kpi__chip{display:inline-flex;align-items:center;gap:var(--hm-space-micro);padding:0 var(--hm-space-xxxs);border:var(--hm-border-hairline) solid transparent;border-radius:var(--hm-radius-pill);background:none;color:var(--hm-text);font:inherit;font-variant-numeric:tabular-nums;white-space:nowrap;cursor:pointer}.as-kpi__chip:hover:not(:disabled){background:var(--hm-hover)}.as-kpi__chip:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color)}.as-kpi__chip[aria-pressed=true]{border-color:var(--hm-text);background:var(--hm-hover);font-weight:var(--hm-font-weight-semibold)}.as-kpi__chip:disabled{color:var(--hm-muted);cursor:default}button.as-kpi{cursor:pointer;transition:border-color var(--hm-duration-quick) var(--hm-ease-standard),background var(--hm-duration-quick) var(--hm-ease-standard)}button.as-kpi:hover{border-color:var(--as-tone)}button.as-kpi:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:var(--hm-border-hairline)}button.as-kpi[aria-pressed=true]{--as-tint: 16%;border-color:var(--as-tone);box-shadow:inset 0 0 0 var(--hm-border-thin) var(--as-tone)}.as-filters{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.as-filters__search{flex:3 1 16rem;min-inline-size:0}.as-filters__select{flex:1 1 9rem;min-inline-size:8rem;max-inline-size:13rem}.as-filters__spacer{flex:1 0 0}.as-filters__more{display:inline-flex;flex:none;align-items:center;gap:var(--hm-space-nano);box-sizing:border-box;block-size:var(--hm-control-height);padding-inline:var(--hm-space-xs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised);color:var(--hm-text);font:inherit;font-size:var(--hm-font-size-sm);white-space:nowrap;cursor:pointer}.as-filters__more:hover{background:var(--hm-hover)}.as-filters__more:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color)}.as-filters__more[aria-expanded=true],.as-filters__more--on{border-color:var(--hm-action);color:var(--hm-action);font-weight:var(--hm-font-weight-semibold)}.as-suggestions__status{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs) var(--hm-space-sm)}.as-suggestions__hint{flex:1 1 20rem;margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-suggestions__count{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.as-suggestions__filters{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs) var(--hm-space-sm)}.as-suggestions__search{flex:1 1 16rem;min-inline-size:0}.as-suggestions__select{flex:0 1 11rem;min-inline-size:9rem}.as-board{--hm-kanban-column-width: 17.5rem}.as-board ::ng-deep .hm-kanban__board{min-block-size:max(26rem,100dvh - 24rem)}.as-board ::ng-deep .hm-kanban-column{--as-status: var(--hm-border);flex:1 0 var(--hm-kanban-column-width);max-inline-size:36rem;gap:var(--hm-space-xs);border-block-start:var(--hm-border-thin) solid var(--as-status);background:color-mix(in srgb,var(--as-status) 9%,var(--hm-kanban-column-bg))}.as-board ::ng-deep .hm-kanban-column--collapsed{flex:0 0 var(--hm-kanban-collapsed-width)}.as-board ::ng-deep .hm-kanban-column[data-column=em_analise]{--as-status: var(--hm-info)}.as-board ::ng-deep .hm-kanban-column[data-column=planejada]{--as-status: var(--hm-accent)}.as-board ::ng-deep .hm-kanban-column[data-column=em_desenvolvimento]{--as-status: var(--hm-warning)}.as-board ::ng-deep .hm-kanban-column[data-column=concluida]{--as-status: var(--hm-success)}.as-board ::ng-deep .hm-kanban-column__title:before{display:inline-block;inline-size:.625rem;block-size:.625rem;margin-inline-end:var(--hm-space-xs);border-radius:50%;background:var(--as-status);content:\"\"}.as-board ::ng-deep .hm-kanban-column__count{padding-inline:var(--hm-space-xs);font-weight:var(--hm-font-weight-semibold)}.as-board ::ng-deep .hm-kanban-column__summary{padding-inline:var(--hm-space-micro)}.as-board ::ng-deep .hm-kanban-column__list{gap:var(--hm-space-xs)}.as-board ::ng-deep .hm-kanban-card{padding:0;overflow:hidden}.as-board ::ng-deep .hm-kanban-card:has(.as-card--top){border-color:var(--hm-warning)}.as-colsum{display:inline-flex;align-items:center;gap:var(--hm-space-micro);white-space:nowrap}.as-colsum--late{color:var(--hm-danger);font-weight:var(--hm-font-weight-semibold)}.as-colempty{display:flex;flex-direction:column;align-items:center;gap:var(--hm-space-xxxs);padding:var(--hm-space-lg) var(--hm-space-sm);text-align:center}.as-colempty__icon{--hm-icon-size: var(--hm-control-height-large);color:var(--hm-muted);opacity:.6}.as-colempty__title{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.as-colempty__hint{max-inline-size:16rem;color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-colfoot{display:flex;align-items:center;gap:var(--hm-space-nano);box-sizing:border-box;inline-size:100%;block-size:var(--hm-control-height-dense);padding-inline:var(--hm-space-xs);border:0;border-radius:var(--hm-radius-control);background:transparent;color:var(--hm-muted);font:inherit;font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold);cursor:pointer}.as-colfoot:hover{background:var(--hm-hover);color:var(--hm-text)}.as-colfoot:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color)}.as-card{display:flex;flex-direction:column;min-inline-size:0}.as-card__cover{block-size:2.5rem}.as-card__body{display:flex;flex-direction:column;gap:var(--hm-space-xxxs);padding:var(--hm-space-sm);min-inline-size:0}.as-card__top{display:flex;align-items:center;gap:var(--hm-space-xxxs) var(--hm-space-xs);min-inline-size:0}.as-card__key{flex:none;color:var(--hm-muted);font-family:var(--hm-font-mono, monospace);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold)}.as-card__menu{margin-inline-start:auto}.as-card__priority{display:inline-flex;align-items:center;gap:var(--hm-space-micro);padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:var(--hm-hover);color:var(--hm-text);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);line-height:1.6;white-space:nowrap}.as-card__priority hm-icon{--hm-icon-size: .875rem;color:var(--hm-muted)}.as-card__priority[data-priority=urgente]{background:var(--hm-danger-bg);color:var(--hm-danger)}.as-card__priority[data-priority=urgente] hm-icon{color:var(--hm-danger)}.as-card__priority[data-priority=alta]{background:var(--hm-warning-bg)}.as-card__priority[data-priority=alta] hm-icon{color:var(--hm-warning)}.as-card__priority[data-priority=media]{background:var(--hm-info-bg)}.as-card__priority[data-priority=media] hm-icon{color:var(--hm-info)}.as-card__badge{display:inline-flex;align-items:center;gap:var(--hm-space-micro);padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:var(--hm-warning-bg);color:var(--hm-text);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);white-space:nowrap}.as-card__title{margin-block-start:var(--hm-space-micro);padding:0;border:0;background:none;color:var(--hm-text);font:inherit;font-weight:var(--hm-font-weight-bold);line-height:var(--hm-line-height-snug);text-align:start;overflow-wrap:anywhere;cursor:pointer}.as-card__title:hover,.as-card__title:focus-visible{color:var(--hm-accent);text-decoration:underline}.as-card__context{display:inline-flex;align-items:center;gap:var(--hm-space-micro);min-inline-size:0;color:var(--hm-action);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-medium);overflow-wrap:anywhere}.as-card__context hm-icon{flex:none}.as-card__context[data-kind=melhoria] hm-icon{color:var(--hm-accent)}.as-card__context[data-kind=nova_funcionalidade] hm-icon{color:var(--hm-info)}.as-card__context[data-kind=problema] hm-icon{color:var(--hm-danger)}.as-card__labels{display:flex;flex-wrap:wrap;gap:var(--hm-space-micro)}.as-card__excerpt{display:-webkit-box;margin:0;overflow:hidden;color:var(--hm-muted);font-size:var(--hm-font-size-sm);-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2}.as-card__checklist{display:flex;flex-direction:column;gap:1px;margin:var(--hm-space-micro) 0 0;padding:var(--hm-space-micro);border-radius:var(--hm-radius-control);background:var(--hm-hover);list-style:none}.as-card__check{display:flex;align-items:flex-start;gap:var(--hm-space-micro);inline-size:100%;padding:1px var(--hm-space-micro);border:0;border-radius:var(--hm-radius-control);background:none;color:var(--hm-text);font:inherit;font-size:var(--hm-font-size-sm);text-align:start;cursor:pointer}.as-card__check[aria-pressed=true] hm-icon{color:var(--hm-success)}.as-card__check:hover{background:var(--hm-hover)}.as-card__check:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color)}.as-card__done{color:var(--hm-muted);text-decoration:line-through}.as-card__due{display:inline-flex;align-self:flex-start;align-items:center;gap:var(--hm-space-micro);margin-block-start:var(--hm-space-micro);padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:var(--hm-hover);color:var(--hm-muted);font-size:var(--hm-font-size-xs);line-height:1.6;white-space:nowrap}.as-card__due[data-due=overdue]{background:var(--hm-danger-bg);color:var(--hm-danger);font-weight:var(--hm-font-weight-semibold)}.as-card__due[data-due=today],.as-card__due[data-due=tomorrow]{background:var(--hm-warning-bg);color:var(--hm-text);font-weight:var(--hm-font-weight-semibold)}.as-card__due[data-due=today] hm-icon,.as-card__due[data-due=tomorrow] hm-icon{color:var(--hm-warning)}.as-card__foot{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs) var(--hm-space-sm);margin-block-start:var(--hm-space-xxxs);padding-block-start:var(--hm-space-xs);border-block-start:var(--hm-border-hairline) solid var(--hm-border)}.as-card__count,.as-card__date{display:inline-flex;align-items:center;gap:var(--hm-space-micro);color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-variant-numeric:tabular-nums;white-space:nowrap}.as-card__count--on{color:var(--hm-text);font-weight:var(--hm-font-weight-semibold)}.as-card__count--full{color:var(--hm-success)}.as-card__count--zero{opacity:.75}.as-card__faces{display:inline-flex;align-items:center;margin-inline-start:auto}.as-card__faces>hm-account-avatar{border-radius:50%;box-shadow:0 0 0 var(--hm-border-thin) var(--hm-kanban-card-bg, var(--hm-surface-raised))}.as-card__faces>hm-account-avatar+hm-account-avatar{margin-inline-start:calc(var(--hm-space-xs) * -1)}.as-card__more{margin-inline-start:var(--hm-space-micro);color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold)}.as-card__nobody{display:inline-flex;color:var(--hm-muted);opacity:.7}.as-archive{display:flex;flex-direction:column;gap:var(--hm-space-xs);margin:0;padding:0;list-style:none}.as-archive[aria-busy=true]{opacity:.6}.as-archive__item{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm);padding:var(--hm-space-sm);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.as-archive__main{display:flex;flex:1 1 20rem;flex-direction:column;gap:var(--hm-space-xxxs);min-inline-size:0}.as-archive__tags,.as-archive__meta{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs) var(--hm-space-xs)}.as-archive__meta,.as-archive__date{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.as-archive__link{padding:0;border:0;background:none;color:var(--hm-accent);font:inherit;font-size:var(--hm-font-size-sm);cursor:pointer}.as-archive__link:hover{text-decoration:underline}.as-archive__link:focus-visible{outline:var(--hm-border-thin) solid var(--hm-accent);outline-offset:var(--hm-border-thin)}@media(forced-colors:active){.as-kpi,.as-card__priority,.as-card__due{border:var(--hm-border-hairline) solid CanvasText}button.as-kpi[aria-pressed=true],.as-kpi__chip[aria-pressed=true]{outline:var(--hm-border-thin) solid Highlight}}@container (max-width: 52rem){.as-kpi--wide{grid-column:1/-1}}@container (max-width: 40rem){.as-kpis{grid-template-columns:repeat(auto-fit,minmax(9.5rem,1fr))}.as-kpi{gap:var(--hm-space-xs);padding:var(--hm-space-xs)}.as-kpi__icon{inline-size:var(--hm-control-height);block-size:var(--hm-control-height)}.as-kpi__icon hm-icon{--hm-icon-size: calc(var(--hm-control-height) * .55)}.as-kpi__value{font-size:var(--hm-font-size-xl)}.as-board{--hm-kanban-column-width: min(18rem, 85cqi)}.as-board ::ng-deep .hm-kanban__board{min-block-size:0;scroll-snap-type:x proximity;scroll-padding-inline:var(--hm-space-xs)}.as-board ::ng-deep .hm-kanban-column{flex:0 0 var(--hm-kanban-column-width);scroll-snap-align:start}.as-board ::ng-deep .hm-kanban-column--collapsed{flex:0 0 var(--hm-kanban-collapsed-width)}.as-filters__select,.as-suggestions__select{flex:1 1 9rem;max-inline-size:none}.as-filters__spacer{display:none}}\n"] }]
|
|
2089
|
+
}], ctorParameters: () => [], propDecorators: { clientId: [{ type: i0.Input, args: [{ isSignal: true, alias: "clientId", required: true }] }], coverPop: [{ type: i0.ViewChild, args: ['coverPop', { isSignal: true }] }] } });
|
|
2090
|
+
|
|
2091
|
+
/**
|
|
2092
|
+
* Tela "Sugestões" do auto-serviço (router `AUTO_SERVICE_SUGGESTIONS_TEMPLATE_ROUTER_ID`, na raiz do menu; migration
|
|
2093
|
+
* 0036 do argos): o seletor de client + o quadro de sugestões que era a aba Cliente › Sugestões. Carregada só por
|
|
2094
|
+
* `AUTO_SERVICE_SCREENS.suggestions` -- nunca exportada pelo ponto de entrada (ver `public-api.ts`).
|
|
2095
|
+
*/
|
|
2096
|
+
class AutoServiceSuggestionsPageComponent {
|
|
2097
|
+
constructor() {
|
|
2098
|
+
this.client = signal(null, ...(ngDevMode ? [{ debugName: "client" }] : /* istanbul ignore next */ []));
|
|
2099
|
+
}
|
|
2100
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceSuggestionsPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2101
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: AutoServiceSuggestionsPageComponent, isStandalone: true, selector: "spine-auto-service-suggestions-page", ngImport: i0, template: `
|
|
2102
|
+
<div class="as-stack">
|
|
2103
|
+
<spine-auto-service-client-picker (clientChange)="client.set($event)" />
|
|
2104
|
+
@if (client(); as c) {
|
|
2105
|
+
<spine-auto-service-suggestions [clientId]="c.id" />
|
|
2106
|
+
}
|
|
2107
|
+
</div>
|
|
2108
|
+
`, 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: AutoServiceSuggestionsComponent, selector: "spine-auto-service-suggestions", inputs: ["clientId"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2109
|
+
}
|
|
2110
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceSuggestionsPageComponent, decorators: [{
|
|
2111
|
+
type: Component,
|
|
2112
|
+
args: [{ selector: 'spine-auto-service-suggestions-page', imports: [AutoServiceClientPickerComponent, AutoServiceSuggestionsComponent], template: `
|
|
2113
|
+
<div class="as-stack">
|
|
2114
|
+
<spine-auto-service-client-picker (clientChange)="client.set($event)" />
|
|
2115
|
+
@if (client(); as c) {
|
|
2116
|
+
<spine-auto-service-suggestions [clientId]="c.id" />
|
|
2117
|
+
}
|
|
2118
|
+
</div>
|
|
2119
|
+
`, 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"] }]
|
|
2120
|
+
}] });
|
|
2121
|
+
|
|
2122
|
+
export { AutoServiceSuggestionsPageComponent };
|
|
2123
|
+
//# sourceMappingURL=trismegisto-spine-auto-service-auto-service-suggestions-page.component-DIeunpQ9.mjs.map
|