@trismegisto/spine 3.3.1 → 3.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-mMjE3JOL.mjs +2179 -0
  2. package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-mMjE3JOL.mjs.map +1 -0
  3. package/fesm2022/{trismegisto-spine-auto-service-home.component-rQ3dIGIE.mjs → trismegisto-spine-auto-service-home.component-9WvXOH3l.mjs} +7 -4
  4. package/fesm2022/trismegisto-spine-auto-service-home.component-9WvXOH3l.mjs.map +1 -0
  5. package/fesm2022/trismegisto-spine-auto-service.mjs +2 -2
  6. package/fesm2022/trismegisto-spine-graphql.mjs +3 -1
  7. package/fesm2022/trismegisto-spine-graphql.mjs.map +1 -1
  8. package/fesm2022/trismegisto-spine-layout-suggestions-page.component-xI2gDMok.mjs +532 -0
  9. package/fesm2022/trismegisto-spine-layout-suggestions-page.component-xI2gDMok.mjs.map +1 -0
  10. package/fesm2022/trismegisto-spine-layout-trismegisto-spine-layout-Clphrsgc.mjs +8429 -0
  11. package/fesm2022/trismegisto-spine-layout-trismegisto-spine-layout-Clphrsgc.mjs.map +1 -0
  12. package/fesm2022/trismegisto-spine-layout.mjs +1 -7651
  13. package/fesm2022/trismegisto-spine-layout.mjs.map +1 -1
  14. package/fesm2022/trismegisto-spine-notification.mjs +1 -1
  15. package/fesm2022/trismegisto-spine-notification.mjs.map +1 -1
  16. package/fesm2022/trismegisto-spine-routing.mjs +16 -3
  17. package/fesm2022/trismegisto-spine-routing.mjs.map +1 -1
  18. package/fesm2022/trismegisto-spine-shell.mjs +4 -2
  19. package/fesm2022/trismegisto-spine-shell.mjs.map +1 -1
  20. package/fesm2022/trismegisto-spine.mjs +286 -29
  21. package/fesm2022/trismegisto-spine.mjs.map +1 -1
  22. package/package.json +6 -6
  23. package/types/trismegisto-spine-layout.d.ts +879 -20
  24. package/types/trismegisto-spine-layout.d.ts.map +1 -1
  25. package/types/trismegisto-spine-routing.d.ts.map +1 -1
  26. package/types/trismegisto-spine-shell.d.ts.map +1 -1
  27. package/types/trismegisto-spine.d.ts +130 -12
  28. package/types/trismegisto-spine.d.ts.map +1 -1
  29. package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-DIeunpQ9.mjs +0 -2123
  30. package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-DIeunpQ9.mjs.map +0 -1
  31. package/fesm2022/trismegisto-spine-auto-service-home.component-rQ3dIGIE.mjs.map +0 -1
@@ -0,0 +1,2179 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, Injectable, input, output, signal, computed, effect, untracked, ChangeDetectionStrategy, Component, 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 { AccountAvatarComponent, SearchFieldComponent, ButtonComponent, CheckboxComponent, HermesToastService, HermesDialogService, AlertComponent, DatePickerComponent, IconButtonComponent, IconComponent, 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_WAITING, SPINE_SUGGESTION_WAITING_COLORS, SPINE_SUGGESTION_KINDS, SPINE_SUGGESTION_KIND_COLORS, SPINE_SUGGESTION_LIMITS, SpineSuggestionChatComponent, SpineTagRowComponent, SPINE_SUGGESTION_STATUS_ICONS, NEW_SCREEN_VALUE, SPINE_SUGGESTION_KIND_ICONS, screenName } 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
+ /** Marca a conversa como lida por quem consulta (zera o `unread` da tarefa). */
111
+ markRead(clientId, id) {
112
+ return firstValueFrom(this.http.post(this.url `/${clientId}/suggestions/${id}/read`, {}, { headers: this.headers }));
113
+ }
114
+ /** Responsável/envolvido: `userId` ou "me" (atribuir a mim). */
115
+ addPerson(clientId, id, userId, role) {
116
+ return firstValueFrom(this.http.post(this.url `/${clientId}/suggestions/${id}/people`, { user_id: userId, role }, { headers: this.headers }));
117
+ }
118
+ removePerson(clientId, id, userId, role) {
119
+ return firstValueFrom(this.http.delete(this.url `/${clientId}/suggestions/${id}/people/${userId}`, {
120
+ headers: this.headers, params: new HttpParams().set('role', role),
121
+ }));
122
+ }
123
+ addChecklistItem(clientId, id, text) {
124
+ return firstValueFrom(this.http.post(this.url `/${clientId}/suggestions/${id}/checklist`, { text }, { headers: this.headers }));
125
+ }
126
+ updateChecklistItem(clientId, id, itemId, change) {
127
+ return firstValueFrom(this.http.patch(this.url `/${clientId}/suggestions/${id}/checklist/${itemId}`, change, { headers: this.headers }));
128
+ }
129
+ removeChecklistItem(clientId, id, itemId) {
130
+ return firstValueFrom(this.http.delete(this.url `/${clientId}/suggestions/${id}/checklist/${itemId}`, { headers: this.headers }));
131
+ }
132
+ // --- rótulos ---
133
+ async labels(clientId) {
134
+ const res = await this.read(this.url `/${clientId}/suggestions/labels`);
135
+ return res.labels ?? [];
136
+ }
137
+ createLabel(clientId, name, color) {
138
+ return firstValueFrom(this.http.post(this.url `/${clientId}/suggestions/labels`, { name, color }, { headers: this.headers }));
139
+ }
140
+ setLabel(clientId, id, labelId, linked) {
141
+ const url = this.url `/${clientId}/suggestions/${id}/labels/${labelId}`;
142
+ return firstValueFrom(linked
143
+ ? this.http.put(url, {}, { headers: this.headers })
144
+ : this.http.delete(url, { headers: this.headers }));
145
+ }
146
+ // --- pessoas do realm do app (a mesma busca da aba Grupos, com a foto) ---
147
+ async searchPeople(clientId, search) {
148
+ let params = new HttpParams().set('limit', 10);
149
+ if (search.trim())
150
+ params = params.set('search', search.trim());
151
+ const res = await this.read(this.url `/${clientId}/users`, params);
152
+ return res.users ?? [];
153
+ }
154
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceSuggestionsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
155
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceSuggestionsService, providedIn: 'root' }); }
156
+ }
157
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceSuggestionsService, decorators: [{
158
+ type: Injectable,
159
+ args: [{ providedIn: 'root' }]
160
+ }] });
161
+
162
+ // 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
163
+ // tarefa -- chave, prioridade, datas, capa, descrição rica, pessoas, rótulos e checklist. Constantes e contas puras
164
+ // (prazo, capa) ficam aqui, fora dos componentes, para o cartão, o detalhe e os testes usarem as mesmas.
165
+ const TASK_PRIORITIES = ['urgente', 'alta', 'media', 'baixa'];
166
+ /** Ícone e intenção de cada prioridade (o texto vem do catálogo; a cor nunca é a única pista). */
167
+ const TASK_PRIORITY_ICONS = {
168
+ urgente: 'exclamation-triangle', alta: 'arrow-up', media: 'minus', baixa: 'arrow-bottom',
169
+ };
170
+ /** Cores de rótulo: tokens da paleta categórica do tema (o servidor só aceita estes). */
171
+ const TASK_LABEL_COLORS = ['cat-1', 'cat-2', 'cat-3', 'cat-4', 'cat-5', 'cat-6', 'cat-7', 'cat-8'];
172
+ /** Hoje como AAAA-MM-DD no fuso dado (o do perfil da pessoa). */
173
+ function todayIso(timeZone, now = new Date()) {
174
+ try {
175
+ return new Intl.DateTimeFormat('en-CA', { timeZone, year: 'numeric', month: '2-digit', day: '2-digit' }).format(now);
176
+ }
177
+ catch {
178
+ return now.toISOString().slice(0, 10);
179
+ }
180
+ }
181
+ /** AAAA-MM-DD mais `days` dias (negativo volta). */
182
+ function addDays(iso, days) {
183
+ const d = new Date(`${iso}T00:00:00Z`);
184
+ d.setUTCDate(d.getUTCDate() + days);
185
+ return d.toISOString().slice(0, 10);
186
+ }
187
+ /** O prazo da tarefa contra hoje. Concluída/recusada não vence. */
188
+ function dueState(task, today) {
189
+ if (!task.due_date || task.status === 'concluida' || task.status === 'recusada')
190
+ return null;
191
+ if (task.due_date < today)
192
+ return 'overdue';
193
+ if (task.due_date === today)
194
+ return 'today';
195
+ if (task.due_date === addDays(today, 1))
196
+ return 'tomorrow';
197
+ return 'later';
198
+ }
199
+ /** Dias inteiros entre duas datas AAAA-MM-DD (`to` - `from`). */
200
+ function daysBetween(from, to) {
201
+ return Math.round((Date.parse(`${to}T00:00:00Z`) - Date.parse(`${from}T00:00:00Z`)) / 86_400_000);
202
+ }
203
+ /** Duração em dias, horas e minutos inteiros (o "4h 12min" do quadro). Negativa vira zero. */
204
+ function splitDuration(ms) {
205
+ const minutes = Math.max(0, Math.floor(ms / 60_000));
206
+ return { d: Math.floor(minutes / 1440), h: Math.floor((minutes % 1440) / 60), m: minutes % 60 };
207
+ }
208
+ /** Responsável por mim? (o `sub` da sessão) */
209
+ const assignedTo = (task, userId) => !!userId && task.assignees.some((p) => p.user_id === userId);
210
+
211
+ // Os pequenos seletores do quadro de sugestões, para ir dentro de um `hm-popover` (o cartão e o detalhe da tarefa):
212
+ // pessoas do realm do app (com foto) e rótulos do catálogo do app.
213
+ /**
214
+ * Pessoa do realm do app: busca (a mesma do auto-serviço, com a foto), as já escolhidas de fora. Lê na abertura (as
215
+ * primeiras) e a cada termo, com espera.
216
+ */
217
+ class SuggestionPeoplePickerComponent {
218
+ constructor() {
219
+ this.text = inject(SPINE_MESSAGES).suggestions;
220
+ this.service = inject(AutoServiceSuggestionsService);
221
+ this.clientId = input.required(...(ngDevMode ? [{ debugName: "clientId" }] : /* istanbul ignore next */ []));
222
+ /** Quem já está no papel (não aparece de novo). */
223
+ this.exclude = input([], ...(ngDevMode ? [{ debugName: "exclude" }] : /* istanbul ignore next */ []));
224
+ this.picked = output();
225
+ this.found = signal([], ...(ngDevMode ? [{ debugName: "found" }] : /* istanbul ignore next */ []));
226
+ this.loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
227
+ this.options = computed(() => this.found().filter((p) => !this.exclude().includes(p.id)), ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
228
+ this.seq = 0;
229
+ effect(() => {
230
+ this.clientId();
231
+ untracked(() => void this.load(''));
232
+ });
233
+ }
234
+ async load(term) {
235
+ const mine = ++this.seq;
236
+ this.loading.set(true);
237
+ try {
238
+ const list = await this.service.searchPeople(this.clientId(), term);
239
+ if (mine === this.seq)
240
+ this.found.set(list);
241
+ }
242
+ catch {
243
+ if (mine === this.seq)
244
+ this.found.set([]);
245
+ }
246
+ finally {
247
+ if (mine === this.seq)
248
+ this.loading.set(false);
249
+ }
250
+ }
251
+ nameOf(p) {
252
+ return p.name || p.username || p.email || '';
253
+ }
254
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionPeoplePickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
255
+ 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: `
256
+ <div class="as-picker">
257
+ <hm-search-field [label]="text.peopleSearch" labelHidden [placeholder]="text.peopleSearch" [landmark]="false"
258
+ [debounce]="250" [loading]="loading()" (search)="load($event)" />
259
+ <ul class="as-picker__list" [attr.aria-busy]="loading() ? 'true' : null">
260
+ @for (p of options(); track p.id) {
261
+ <li>
262
+ <button type="button" class="as-picker__person" (click)="picked.emit(p)">
263
+ <hm-account-avatar size="small" [name]="nameOf(p)" [imageUrl]="p.picture" />
264
+ <span class="as-picker__person-text">
265
+ <span class="as-picker__person-name">{{ nameOf(p) }}</span>
266
+ @if (p.username && p.name) { <span class="as-picker__muted">{{ p.username }}</span> }
267
+ </span>
268
+ </button>
269
+ </li>
270
+ } @empty {
271
+ @if (!loading()) { <li class="as-picker__muted">{{ text.peopleEmpty }}</li> }
272
+ }
273
+ </ul>
274
+ </div>
275
+ `, 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-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 }); }
276
+ }
277
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionPeoplePickerComponent, decorators: [{
278
+ type: Component,
279
+ args: [{ selector: 'spine-suggestion-people-picker', imports: [AccountAvatarComponent, SearchFieldComponent], template: `
280
+ <div class="as-picker">
281
+ <hm-search-field [label]="text.peopleSearch" labelHidden [placeholder]="text.peopleSearch" [landmark]="false"
282
+ [debounce]="250" [loading]="loading()" (search)="load($event)" />
283
+ <ul class="as-picker__list" [attr.aria-busy]="loading() ? 'true' : null">
284
+ @for (p of options(); track p.id) {
285
+ <li>
286
+ <button type="button" class="as-picker__person" (click)="picked.emit(p)">
287
+ <hm-account-avatar size="small" [name]="nameOf(p)" [imageUrl]="p.picture" />
288
+ <span class="as-picker__person-text">
289
+ <span class="as-picker__person-name">{{ nameOf(p) }}</span>
290
+ @if (p.username && p.name) { <span class="as-picker__muted">{{ p.username }}</span> }
291
+ </span>
292
+ </button>
293
+ </li>
294
+ } @empty {
295
+ @if (!loading()) { <li class="as-picker__muted">{{ text.peopleEmpty }}</li> }
296
+ }
297
+ </ul>
298
+ </div>
299
+ `, 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-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"] }]
300
+ }], 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"] }] } });
301
+ /**
302
+ * 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
303
+ * existe, "Criar o rótulo" com a cor escolhida.
304
+ */
305
+ class SuggestionLabelPickerComponent {
306
+ constructor() {
307
+ this.text = inject(SPINE_MESSAGES).suggestions;
308
+ this.labels = input([], ...(ngDevMode ? [{ debugName: "labels" }] : /* istanbul ignore next */ []));
309
+ this.selected = input([], ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
310
+ this.toggled = output();
311
+ this.created = output();
312
+ this.colors = TASK_LABEL_COLORS;
313
+ this.term = signal('', ...(ngDevMode ? [{ debugName: "term" }] : /* istanbul ignore next */ []));
314
+ this.color = signal('cat-1', ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
315
+ this.shown = computed(() => {
316
+ const t = this.term().trim().toLocaleLowerCase();
317
+ return this.labels().filter((l) => !t || l.name.toLocaleLowerCase().includes(t));
318
+ }, ...(ngDevMode ? [{ debugName: "shown" }] : /* istanbul ignore next */ []));
319
+ /** Termo que ainda não é rótulo: dá para criar. */
320
+ this.creatable = computed(() => {
321
+ const t = this.term().trim().toLocaleLowerCase();
322
+ return !!t && t.length <= 40 && !this.labels().some((l) => l.name.toLocaleLowerCase() === t);
323
+ }, ...(ngDevMode ? [{ debugName: "creatable" }] : /* istanbul ignore next */ []));
324
+ this.createText = computed(() => formatMessage(this.text.createLabel, { name: this.term().trim() }), ...(ngDevMode ? [{ debugName: "createText" }] : /* istanbul ignore next */ []));
325
+ }
326
+ colorName(i) {
327
+ return formatMessage(this.text.labelColorName, { n: i + 1 });
328
+ }
329
+ create() {
330
+ const name = this.term().trim();
331
+ if (!name)
332
+ return;
333
+ this.created.emit({ name, color: this.color() });
334
+ this.term.set('');
335
+ }
336
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionLabelPickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
337
+ 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: `
338
+ <div class="as-picker">
339
+ <hm-search-field [label]="text.labelFilter" labelHidden [placeholder]="text.labelFilter" [landmark]="false"
340
+ [value]="term()" (valueChange)="term.set($event)" />
341
+ <ul class="as-picker__list">
342
+ @for (l of shown(); track l.id) {
343
+ <li class="as-picker__label-row">
344
+ <span class="as-picker__dot" [class]="'as-tok--' + l.color" aria-hidden="true"></span>
345
+ <hm-checkbox [label]="l.name" [checked]="selected().includes(l.id)" (checkedChange)="toggled.emit({ label: l, linked: $event })" />
346
+ </li>
347
+ } @empty {
348
+ @if (!creatable()) { <li class="as-picker__muted">{{ text.noLabels }}</li> }
349
+ }
350
+ </ul>
351
+ @if (creatable()) {
352
+ <span class="as-picker__heading">{{ text.labelColor }}</span>
353
+ <div class="as-picker__swatches">
354
+ @for (c of colors; track c; let i = $index) {
355
+ <button type="button" class="as-picker__swatch as-picker__swatch--small" [class]="'as-tok--' + c"
356
+ [attr.aria-label]="colorName(i)" [attr.aria-pressed]="color() === c" (click)="color.set(c)"></button>
357
+ }
358
+ </div>
359
+ <hm-button size="small" emphasis="medium" icon="plus" (clicked)="create()">{{ createText() }}</hm-button>
360
+ }
361
+ </div>
362
+ `, 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-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 }); }
363
+ }
364
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionLabelPickerComponent, decorators: [{
365
+ type: Component,
366
+ args: [{ selector: 'spine-suggestion-label-picker', imports: [ButtonComponent, CheckboxComponent, SearchFieldComponent], template: `
367
+ <div class="as-picker">
368
+ <hm-search-field [label]="text.labelFilter" labelHidden [placeholder]="text.labelFilter" [landmark]="false"
369
+ [value]="term()" (valueChange)="term.set($event)" />
370
+ <ul class="as-picker__list">
371
+ @for (l of shown(); track l.id) {
372
+ <li class="as-picker__label-row">
373
+ <span class="as-picker__dot" [class]="'as-tok--' + l.color" aria-hidden="true"></span>
374
+ <hm-checkbox [label]="l.name" [checked]="selected().includes(l.id)" (checkedChange)="toggled.emit({ label: l, linked: $event })" />
375
+ </li>
376
+ } @empty {
377
+ @if (!creatable()) { <li class="as-picker__muted">{{ text.noLabels }}</li> }
378
+ }
379
+ </ul>
380
+ @if (creatable()) {
381
+ <span class="as-picker__heading">{{ text.labelColor }}</span>
382
+ <div class="as-picker__swatches">
383
+ @for (c of colors; track c; let i = $index) {
384
+ <button type="button" class="as-picker__swatch as-picker__swatch--small" [class]="'as-tok--' + c"
385
+ [attr.aria-label]="colorName(i)" [attr.aria-pressed]="color() === c" (click)="color.set(c)"></button>
386
+ }
387
+ </div>
388
+ <hm-button size="small" emphasis="medium" icon="plus" (clicked)="create()">{{ createText() }}</hm-button>
389
+ }
390
+ </div>
391
+ `, 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-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"] }]
392
+ }], 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"] }] } });
393
+
394
+ const EVENTS_PAGE = 20;
395
+ /** Copia para a área de transferência; false se o navegador recusar. */
396
+ async function copyText(value) {
397
+ try {
398
+ await navigator.clipboard.writeText(value);
399
+ return true;
400
+ }
401
+ catch {
402
+ return false;
403
+ }
404
+ }
405
+ /**
406
+ * A tarefa aberta (modal grande, duas colunas, como os quadros de tarefas conhecidos), montada pelo quadro.
407
+ *
408
+ * - **Esquerda**: título editável no lugar (Enter salva, Esc desiste), descrição em `hm-rich-text` (ler; "Editar"
409
+ * abre o editor e "Salvar" grava o HTML, que o servidor saneia), checklist (marcar, renomear, subir/descer, remover,
410
+ * "Mostrar no cartão") e as abas Comentários e Atividades (do mais novo, "Carregar mais"). Comentário é texto simples
411
+ * e pode ser interno (só a equipe vê; quem pediu não lê em "Minhas sugestões").
412
+ * - **Direita**: status, tipo, responsáveis ("Atribuir a mim") e envolvidos (busca de pessoas do realm, com foto),
413
+ * relator, prioridade, início e conclusão, rótulos (catálogo do app; cria na hora), capa, tela de origem ("Abrir a
414
+ * tela" quando o app administrado é o desta sessão), apoios, criada/atualizada e "Última alteração há X por Fulano";
415
+ * a chave com copiar; arquivar/restaurar e excluir (com confirmação).
416
+ * - **Agrupar**: "Relatores" (o autor e quem veio das agrupadas, com foto) no lugar do relator; "Agrupadas aqui" (chave,
417
+ * título e autor de cada origem, com link e "Desagrupar"); "Agrupar em…" pede o seletor ao quadro (`mergeRequested`).
418
+ * Na agrupada, o aviso "Agrupada em SUG-12" no topo, com "Abrir SUG-12" (`openTask`) e "Desagrupar" (`unmerged`).
419
+ * - **Nova tarefa** (`task` nulo, `draft`): título, tipo, coluna, prioridade, datas, descrição, rótulos e "Atribuir a
420
+ * mim"; criar troca para a tarefa completa.
421
+ *
422
+ * Toda mudança vai direto ao servidor, que devolve a tarefa inteira: ela vira a nova verdade aqui (`taskChange`) e no
423
+ * quadro (`changed`).
424
+ */
425
+ class SuggestionTaskDetailComponent {
426
+ constructor() {
427
+ this.messages = inject(SPINE_MESSAGES);
428
+ this.text = this.messages.suggestions;
429
+ this.service = inject(AutoServiceSuggestionsService);
430
+ this.toast = inject(HermesToastService);
431
+ this.dialogs = inject(HermesDialogService);
432
+ this.preferences = inject(SpinePreferencesService);
433
+ this.userStorage = inject(SpineUserStorage, { optional: true });
434
+ this.router = inject(Router, { optional: true });
435
+ this.location = inject(Location, { optional: true });
436
+ this.clientId = input.required(...(ngDevMode ? [{ debugName: "clientId" }] : /* istanbul ignore next */ []));
437
+ /** A tarefa aberta (leve, do quadro: a completa chega do servidor). null + `draft` = nova tarefa. */
438
+ this.task = input(null, ...(ngDevMode ? [{ debugName: "task" }] : /* istanbul ignore next */ []));
439
+ this.draft = input(false, ...(ngDevMode ? [{ debugName: "draft" }] : /* istanbul ignore next */ []));
440
+ /** Coluna da nova tarefa. */
441
+ this.draftStatus = input('em_analise', ...(ngDevMode ? [{ debugName: "draftStatus" }] : /* istanbul ignore next */ []));
442
+ this.labels = input([], ...(ngDevMode ? [{ debugName: "labels" }] : /* istanbul ignore next */ []));
443
+ /** A tela da sugestão abre daqui (o app administrado é o desta sessão). */
444
+ this.canOpenScreen = input(false, ...(ngDevMode ? [{ debugName: "canOpenScreen" }] : /* istanbul ignore next */ []));
445
+ /** A tarefa mudou (o servidor devolveu a nova versão). */
446
+ this.changed = output();
447
+ this.created = output();
448
+ this.removed = output();
449
+ this.closed = output();
450
+ /** Um rótulo novo entrou no catálogo. */
451
+ this.labelCreated = output();
452
+ /** "Agrupar em…": o quadro fecha este modal e abre o seletor. */
453
+ this.mergeRequested = output();
454
+ /** Abrir outra tarefa pelo id (o destino de uma agrupada, uma das agrupadas). */
455
+ this.openTask = output();
456
+ /** Desagrupou (daqui): o quadro relê, a origem volta para ele. */
457
+ this.unmerged = output();
458
+ this.statuses = SPINE_SUGGESTION_STATUSES;
459
+ this.statusColors = SPINE_SUGGESTION_STATUS_COLORS;
460
+ this.waitingValues = SPINE_SUGGESTION_WAITING;
461
+ this.waitingColors = SPINE_SUGGESTION_WAITING_COLORS;
462
+ this.kinds = SPINE_SUGGESTION_KINDS;
463
+ this.kindColors = SPINE_SUGGESTION_KIND_COLORS;
464
+ this.priorities = TASK_PRIORITIES;
465
+ this.priorityIcons = TASK_PRIORITY_ICONS;
466
+ this.limits = SPINE_SUGGESTION_LIMITS;
467
+ this.today = todayIso(this.preferences.effectiveTimeZone());
468
+ this.current = signal(null, ...(ngDevMode ? [{ debugName: "current" }] : /* istanbul ignore next */ []));
469
+ this.loadError = signal('', ...(ngDevMode ? [{ debugName: "loadError" }] : /* istanbul ignore next */ []));
470
+ this.busy = signal(false, ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
471
+ this.open = computed(() => !!this.current() || this.draft(), ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
472
+ this.heading = computed(() => {
473
+ const t = this.current();
474
+ return t ? `${t.key} · ${t.title}` : this.text.newTaskHeading;
475
+ }, ...(ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []));
476
+ // título e descrição
477
+ this.editingTitle = signal(false, ...(ngDevMode ? [{ debugName: "editingTitle" }] : /* istanbul ignore next */ []));
478
+ this.titleDraft = signal('', ...(ngDevMode ? [{ debugName: "titleDraft" }] : /* istanbul ignore next */ []));
479
+ this.editingDescription = signal(false, ...(ngDevMode ? [{ debugName: "editingDescription" }] : /* istanbul ignore next */ []));
480
+ this.descriptionDraft = signal('', ...(ngDevMode ? [{ debugName: "descriptionDraft" }] : /* istanbul ignore next */ []));
481
+ // checklist
482
+ this.newItem = signal('', ...(ngDevMode ? [{ debugName: "newItem" }] : /* istanbul ignore next */ []));
483
+ this.renaming = signal(null, ...(ngDevMode ? [{ debugName: "renaming" }] : /* istanbul ignore next */ []));
484
+ this.renameDraft = signal('', ...(ngDevMode ? [{ debugName: "renameDraft" }] : /* istanbul ignore next */ []));
485
+ // comentários e atividades
486
+ /** Resumo (descrição e checklist), Comentários (a conversa com quem pediu) e Histórico (tudo o que mudou). */
487
+ this.tab = signal('summary', ...(ngDevMode ? [{ debugName: "tab" }] : /* istanbul ignore next */ []));
488
+ this.comments = signal([], ...(ngDevMode ? [{ debugName: "comments" }] : /* istanbul ignore next */ []));
489
+ this.commentsTotal = signal(0, ...(ngDevMode ? [{ debugName: "commentsTotal" }] : /* istanbul ignore next */ []));
490
+ this.activity = signal([], ...(ngDevMode ? [{ debugName: "activity" }] : /* istanbul ignore next */ []));
491
+ this.activityTotal = signal(0, ...(ngDevMode ? [{ debugName: "activityTotal" }] : /* istanbul ignore next */ []));
492
+ this.eventsLoading = signal(false, ...(ngDevMode ? [{ debugName: "eventsLoading" }] : /* istanbul ignore next */ []));
493
+ this.chat = viewChild(SpineSuggestionChatComponent, ...(ngDevMode ? [{ debugName: "chat" }] : /* istanbul ignore next */ []));
494
+ /**
495
+ * A aba Comentários como chat (do mais antigo ao mais novo; a lista vem do mais novo). À direita, o que a pessoa
496
+ * escreveu como equipe; o que ela escreveu como quem pede (pela página) fica à esquerda, como os outros.
497
+ */
498
+ this.chatItems = computed(() => [...this.comments()].reverse().map((e) => ({
499
+ id: e.id, text: e.comment ?? '', created_at: e.created_at, author: e.actor, from_requester: !!e.from_requester,
500
+ internal: !!e.internal, status: e.kind === 'status' && e.to_status !== 'nova' ? e.to_status : null,
501
+ })), ...(ngDevMode ? [{ debugName: "chatItems" }] : /* istanbul ignore next */ []));
502
+ /** As abas de eventos já lidas desta tarefa (vazia também conta: trocar de aba não relê). */
503
+ this.loadedTabs = new Set();
504
+ this.comment = signal('', ...(ngDevMode ? [{ debugName: "comment" }] : /* istanbul ignore next */ []));
505
+ this.commentInternal = signal(false, ...(ngDevMode ? [{ debugName: "commentInternal" }] : /* istanbul ignore next */ []));
506
+ this.commentError = signal('', ...(ngDevMode ? [{ debugName: "commentError" }] : /* istanbul ignore next */ []));
507
+ this.commenting = signal(false, ...(ngDevMode ? [{ debugName: "commenting" }] : /* istanbul ignore next */ []));
508
+ // nova tarefa
509
+ this.draftTitle = signal('', ...(ngDevMode ? [{ debugName: "draftTitle" }] : /* istanbul ignore next */ []));
510
+ this.draftTitleError = signal('', ...(ngDevMode ? [{ debugName: "draftTitleError" }] : /* istanbul ignore next */ []));
511
+ this.draftKind = signal('melhoria', ...(ngDevMode ? [{ debugName: "draftKind" }] : /* istanbul ignore next */ []));
512
+ this.draftColumn = signal('em_analise', ...(ngDevMode ? [{ debugName: "draftColumn" }] : /* istanbul ignore next */ []));
513
+ this.draftPriority = signal('media', ...(ngDevMode ? [{ debugName: "draftPriority" }] : /* istanbul ignore next */ []));
514
+ this.draftStart = signal(null, ...(ngDevMode ? [{ debugName: "draftStart" }] : /* istanbul ignore next */ []));
515
+ this.draftDue = signal(null, ...(ngDevMode ? [{ debugName: "draftDue" }] : /* istanbul ignore next */ []));
516
+ this.draftDescription = signal('', ...(ngDevMode ? [{ debugName: "draftDescription" }] : /* istanbul ignore next */ []));
517
+ this.draftLabels = signal([], ...(ngDevMode ? [{ debugName: "draftLabels" }] : /* istanbul ignore next */ []));
518
+ this.draftAssignMe = signal(true, ...(ngDevMode ? [{ debugName: "draftAssignMe" }] : /* istanbul ignore next */ []));
519
+ this.selectedLabels = computed(() => this.current()?.labels.map((l) => l.id) ?? this.draftLabels(), ...(ngDevMode ? [{ debugName: "selectedLabels" }] : /* istanbul ignore next */ []));
520
+ this.assigneeIds = computed(() => this.current()?.assignees.map((p) => p.user_id) ?? [], ...(ngDevMode ? [{ debugName: "assigneeIds" }] : /* istanbul ignore next */ []));
521
+ this.watcherIds = computed(() => this.current()?.watchers.map((p) => p.user_id) ?? [], ...(ngDevMode ? [{ debugName: "watcherIds" }] : /* istanbul ignore next */ []));
522
+ this.checklistItems = computed(() => [...(this.current()?.checklist.items ?? [])].sort((a, b) => a.position - b.position), ...(ngDevMode ? [{ debugName: "checklistItems" }] : /* istanbul ignore next */ []));
523
+ this.dueSituation = computed(() => {
524
+ const t = this.current();
525
+ return t ? dueState(t, this.today) : null;
526
+ }, ...(ngDevMode ? [{ debugName: "dueSituation" }] : /* istanbul ignore next */ []));
527
+ this.isMine = computed(() => {
528
+ const me = this.me();
529
+ return !!me && this.assigneeIds().includes(me);
530
+ }, ...(ngDevMode ? [{ debugName: "isMine" }] : /* istanbul ignore next */ []));
531
+ effect(() => {
532
+ const task = this.task();
533
+ const draft = this.draft();
534
+ untracked(() => {
535
+ this.reset();
536
+ if (task)
537
+ void this.load(task);
538
+ else if (draft)
539
+ this.startDraft();
540
+ });
541
+ });
542
+ }
543
+ me() {
544
+ return this.userStorage?.userId() ?? '';
545
+ }
546
+ reset() {
547
+ this.current.set(null);
548
+ this.loadError.set('');
549
+ this.editingTitle.set(false);
550
+ this.editingDescription.set(false);
551
+ this.renaming.set(null);
552
+ this.newItem.set('');
553
+ this.comments.set([]);
554
+ this.activity.set([]);
555
+ this.commentsTotal.set(0);
556
+ this.activityTotal.set(0);
557
+ this.comment.set('');
558
+ this.commentInternal.set(false);
559
+ this.commentError.set('');
560
+ this.tab.set('summary');
561
+ this.loadedTabs.clear();
562
+ }
563
+ startDraft() {
564
+ this.draftTitle.set('');
565
+ this.draftTitleError.set('');
566
+ this.draftKind.set('melhoria');
567
+ this.draftColumn.set(this.draftStatus());
568
+ this.draftPriority.set('media');
569
+ this.draftStart.set(null);
570
+ this.draftDue.set(null);
571
+ this.draftDescription.set('');
572
+ this.draftLabels.set([]);
573
+ this.draftAssignMe.set(true);
574
+ }
575
+ async load(light) {
576
+ this.current.set(light);
577
+ try {
578
+ const full = await this.service.task(this.clientId(), light.id);
579
+ if (this.task()?.id !== light.id)
580
+ return;
581
+ this.current.set({ ...full, author: full.author ?? light.author });
582
+ void this.loadEvents('comments', true);
583
+ if (full.unread || light.unread)
584
+ this.markRead({ ...full, author: full.author ?? light.author });
585
+ }
586
+ catch {
587
+ this.loadError.set(this.text.loadError);
588
+ }
589
+ }
590
+ setOpen(open) {
591
+ if (!open)
592
+ this.closed.emit();
593
+ }
594
+ /** Aplica a versão do servidor aqui e avisa o quadro. */
595
+ apply(task) {
596
+ const prev = this.current();
597
+ const next = { ...task, author: task.author ?? prev?.author ?? null };
598
+ this.current.set(next);
599
+ this.changed.emit(next);
600
+ }
601
+ /** Roda uma mudança no servidor; erro = aviso no canto. */
602
+ async run(call, success) {
603
+ if (this.busy())
604
+ return false;
605
+ this.busy.set(true);
606
+ try {
607
+ this.apply(await call());
608
+ if (success)
609
+ this.toast.success(success);
610
+ if (this.tab() === 'activity')
611
+ void this.loadEvents('activity', true);
612
+ return true;
613
+ }
614
+ catch {
615
+ this.toast.error(this.text.saveError);
616
+ return false;
617
+ }
618
+ finally {
619
+ this.busy.set(false);
620
+ }
621
+ }
622
+ edit(patch) {
623
+ const t = this.current();
624
+ return t ? this.run(() => this.service.update(this.clientId(), t.id, patch)) : Promise.resolve(false);
625
+ }
626
+ // --- título e descrição ---
627
+ startTitle() {
628
+ this.titleDraft.set(this.current()?.title ?? '');
629
+ this.editingTitle.set(true);
630
+ }
631
+ async saveTitle() {
632
+ const title = this.titleDraft().trim();
633
+ if (!title || title === this.current()?.title) {
634
+ this.editingTitle.set(false);
635
+ return;
636
+ }
637
+ if (await this.edit({ title }))
638
+ this.editingTitle.set(false);
639
+ }
640
+ onTitleKey(event) {
641
+ if (event.key === 'Enter') {
642
+ event.preventDefault();
643
+ void this.saveTitle();
644
+ }
645
+ else if (event.key === 'Escape') {
646
+ event.stopPropagation();
647
+ this.editingTitle.set(false);
648
+ }
649
+ }
650
+ startDescription() {
651
+ this.descriptionDraft.set(this.current()?.description_html ?? '');
652
+ this.editingDescription.set(true);
653
+ }
654
+ async saveDescription() {
655
+ if (await this.edit({ description_html: this.descriptionDraft() }))
656
+ this.editingDescription.set(false);
657
+ }
658
+ // --- campos da direita ---
659
+ /** Abrir a tarefa lê a conversa: zera o "N novas" (aqui e no quadro); falhar não atrapalha. */
660
+ markRead(t) {
661
+ const read = { ...t, unread: 0 };
662
+ this.current.set(read);
663
+ this.changed.emit(read);
664
+ this.service.markRead(this.clientId(), t.id).catch(() => undefined);
665
+ }
666
+ /** Com quem está a vez (o servidor também muda sozinho: comentário público da equipe = quem pediu; resposta = equipe). */
667
+ changeWaiting(value) {
668
+ const t = this.current();
669
+ const waiting = (value || null);
670
+ if (!t || waiting === (t.waiting_on ?? null))
671
+ return;
672
+ void this.edit({ waiting_on: waiting }).then((ok) => { if (ok)
673
+ this.toast.success(this.text.waitingSaved); });
674
+ }
675
+ /** Os rótulos na linha de selos, cada um com a cor do token ('cat-3' → 3). */
676
+ labelTags(t) {
677
+ return t.labels.map((l) => ({ id: l.id, name: l.name, color: Number(l.color.replace('cat-', '')) || undefined }));
678
+ }
679
+ unreadText(count) {
680
+ return pluralMessage(this.text.unread, count, this.messages.meta.locale);
681
+ }
682
+ changeStatus(status) {
683
+ const t = this.current();
684
+ if (!t || status === t.status)
685
+ return;
686
+ void this.run(() => this.service.move(this.clientId(), t.id, { status: status }), this.text.statusSaved);
687
+ }
688
+ changeKind(kind) {
689
+ if (kind !== this.current()?.kind)
690
+ void this.edit({ kind: kind });
691
+ }
692
+ changePriority(priority) {
693
+ if (priority !== this.current()?.priority)
694
+ void this.edit({ priority: priority });
695
+ }
696
+ changeDate(field, value) {
697
+ const t = this.current();
698
+ if (!t || (value || null) === t[field])
699
+ return;
700
+ void this.edit({ [field]: value || null });
701
+ }
702
+ toggleChecklistOnCard(on) {
703
+ if (on !== this.current()?.checklist_on_card)
704
+ void this.edit({ checklist_on_card: on });
705
+ }
706
+ assignMe() {
707
+ const t = this.current();
708
+ if (t)
709
+ void this.run(() => this.service.addPerson(this.clientId(), t.id, 'me', 'responsavel'), this.text.assignedToMe);
710
+ }
711
+ addPerson(person, role, popover) {
712
+ popover.close();
713
+ const t = this.current();
714
+ if (t)
715
+ void this.run(() => this.service.addPerson(this.clientId(), t.id, person.id, role));
716
+ }
717
+ removePerson(person, role) {
718
+ const t = this.current();
719
+ if (t)
720
+ void this.run(() => this.service.removePerson(this.clientId(), t.id, person.user_id, role));
721
+ }
722
+ toggleLabel(label, linked) {
723
+ const t = this.current();
724
+ if (!t) {
725
+ this.draftLabels.update((ids) => (linked ? [...new Set([...ids, label.id])] : ids.filter((id) => id !== label.id)));
726
+ return;
727
+ }
728
+ void this.run(() => this.service.setLabel(this.clientId(), t.id, label.id, linked));
729
+ }
730
+ async createLabel(name, color) {
731
+ try {
732
+ const label = await this.service.createLabel(this.clientId(), name, color);
733
+ const entry = { ...label, uses: 0 };
734
+ this.labelCreated.emit(entry);
735
+ this.toggleLabel(entry, true);
736
+ }
737
+ catch {
738
+ this.toast.error(this.text.saveError);
739
+ }
740
+ }
741
+ async copyKey() {
742
+ const t = this.current();
743
+ if (!t)
744
+ return;
745
+ if (await copyText(t.key))
746
+ this.toast.success(this.text.keyCopied);
747
+ else
748
+ this.toast.error(this.text.copyError);
749
+ }
750
+ screenHref(path) {
751
+ return this.location?.prepareExternalUrl(path) ?? path;
752
+ }
753
+ /** Clique simples navega no app (e fecha); com Ctrl/Cmd/Shift ou botão do meio, o navegador abre a aba. */
754
+ openScreen(event, path) {
755
+ if (!this.router || event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey)
756
+ return;
757
+ event.preventDefault();
758
+ this.closed.emit();
759
+ void this.router.navigateByUrl(path);
760
+ }
761
+ toggleArchived() {
762
+ const t = this.current();
763
+ if (!t)
764
+ return;
765
+ const archive = !t.archived_at;
766
+ void this.run(() => this.service.setArchived(this.clientId(), t.id, archive), archive ? this.text.archivedToast : this.text.restoredToast);
767
+ }
768
+ async remove() {
769
+ const t = this.current();
770
+ if (!t)
771
+ return;
772
+ const ok = await this.dialogs.confirm({
773
+ heading: formatMessage(this.text.deleteHeading, { key: t.key }), message: this.text.deleteMessage,
774
+ confirmLabel: this.text.deleteConfirm, danger: true,
775
+ });
776
+ if (!ok)
777
+ return;
778
+ try {
779
+ await this.service.remove(this.clientId(), t.id);
780
+ this.toast.success(this.text.deleted);
781
+ this.removed.emit(t);
782
+ }
783
+ catch {
784
+ this.toast.error(this.text.saveError);
785
+ }
786
+ }
787
+ // --- agrupar ---
788
+ /** Desagrupa esta (a agrupada): ela volta ao quadro. */
789
+ async unmergeSelf() {
790
+ const t = this.current();
791
+ if (!t?.merged_into || this.busy())
792
+ return;
793
+ if (await this.run(() => this.service.unmerge(this.clientId(), t.id), formatMessage(this.text.unmergedToast, { key: t.key }))) {
794
+ this.unmerged.emit();
795
+ }
796
+ }
797
+ /** Desagrupa uma das agrupadas nesta: relê esta (perde os relatores e apoios que vieram dela). */
798
+ async unmergeOrigin(origin) {
799
+ const t = this.current();
800
+ if (!t)
801
+ return;
802
+ const ok = await this.run(async () => {
803
+ await this.service.unmerge(this.clientId(), origin.id);
804
+ return this.service.task(this.clientId(), t.id);
805
+ }, formatMessage(this.text.unmergedToast, { key: origin.key }));
806
+ if (ok)
807
+ this.unmerged.emit();
808
+ }
809
+ /** Quem pediu: o autor e os relatores extras (do detalhe); sem a lista ainda, só o autor. */
810
+ reportersOf(t) {
811
+ if (t.reporters?.length)
812
+ return t.reporters;
813
+ return [{ ...(t.author ?? { user_id: '', username: null, name: null, picture: null }), role: 'author', from_suggestion_id: null }];
814
+ }
815
+ /** "Autor" ou "via SUG-30" (a agrupada de onde o relator veio). */
816
+ reporterHint(t, r) {
817
+ if (r.role === 'author')
818
+ return this.text.reporterAuthor;
819
+ const origin = t.merged?.find((m) => m.id === r.from_suggestion_id);
820
+ return origin ? formatMessage(this.text.reporterVia, { key: origin.key }) : '';
821
+ }
822
+ mergedIntoText(key) {
823
+ return formatMessage(this.text.mergedInto, { key });
824
+ }
825
+ openMergedText(key) {
826
+ return formatMessage(this.text.openMerged, { key });
827
+ }
828
+ unmergeLabel(key) {
829
+ return formatMessage(this.text.unmergeLabel, { key });
830
+ }
831
+ // --- checklist ---
832
+ addItem() {
833
+ const t = this.current();
834
+ const text = this.newItem().trim();
835
+ if (!t || !text)
836
+ return;
837
+ void this.run(() => this.service.addChecklistItem(this.clientId(), t.id, text)).then((ok) => ok && this.newItem.set(''));
838
+ }
839
+ onNewItemKey(event) {
840
+ if (event.key === 'Enter') {
841
+ event.preventDefault();
842
+ this.addItem();
843
+ }
844
+ }
845
+ toggleItem(item, done) {
846
+ const t = this.current();
847
+ if (t && done !== item.done)
848
+ void this.run(() => this.service.updateChecklistItem(this.clientId(), t.id, item.id, { done }));
849
+ }
850
+ startRename(item) {
851
+ this.renaming.set(item.id);
852
+ this.renameDraft.set(item.text);
853
+ }
854
+ saveRename(item) {
855
+ const text = this.renameDraft().trim();
856
+ this.renaming.set(null);
857
+ const t = this.current();
858
+ if (t && text && text !== item.text)
859
+ void this.run(() => this.service.updateChecklistItem(this.clientId(), t.id, item.id, { text }));
860
+ }
861
+ onRenameKey(event, item) {
862
+ if (event.key === 'Enter') {
863
+ event.preventDefault();
864
+ this.saveRename(item);
865
+ }
866
+ else if (event.key === 'Escape') {
867
+ event.stopPropagation();
868
+ this.renaming.set(null);
869
+ }
870
+ }
871
+ moveItem(item, delta) {
872
+ const t = this.current();
873
+ const index = this.checklistItems().findIndex((i) => i.id === item.id);
874
+ const target = index + delta;
875
+ if (!t || index < 0 || target < 0 || target >= this.checklistItems().length)
876
+ return;
877
+ void this.run(() => this.service.updateChecklistItem(this.clientId(), t.id, item.id, { position: target }));
878
+ }
879
+ removeItem(item) {
880
+ const t = this.current();
881
+ if (t)
882
+ void this.run(() => this.service.removeChecklistItem(this.clientId(), t.id, item.id));
883
+ }
884
+ // --- comentários e atividades ---
885
+ selectTab(tab) {
886
+ const next = tab === 'activity' || tab === 'comments' ? tab : 'summary';
887
+ this.tab.set(next);
888
+ if (next === 'summary')
889
+ return;
890
+ if (!this.loadedTabs.has(next))
891
+ void this.loadEvents(next, true);
892
+ }
893
+ async loadEvents(type, reset) {
894
+ const t = this.current();
895
+ if (!t)
896
+ return;
897
+ const list = type === 'comments' ? this.comments : this.activity;
898
+ const total = type === 'comments' ? this.commentsTotal : this.activityTotal;
899
+ this.eventsLoading.set(true);
900
+ try {
901
+ const res = await this.service.events(this.clientId(), t.id, { type, limit: EVENTS_PAGE, offset: reset ? 0 : list().length });
902
+ if (this.current()?.id !== t.id)
903
+ return;
904
+ list.set(reset ? res.events : [...list(), ...res.events]);
905
+ total.set(res.total);
906
+ this.loadedTabs.add(type);
907
+ }
908
+ catch {
909
+ this.loadError.set(this.text.loadError);
910
+ }
911
+ finally {
912
+ this.eventsLoading.set(false);
913
+ }
914
+ }
915
+ async sendComment(text) {
916
+ const t = this.current();
917
+ const comment = (text ?? this.comment()).trim();
918
+ if (!t || this.commenting())
919
+ return;
920
+ this.commentError.set(comment ? '' : this.text.commentRequired);
921
+ if (!comment)
922
+ return;
923
+ this.commenting.set(true);
924
+ try {
925
+ const event = await this.service.comment(this.clientId(), t.id, comment, this.commentInternal());
926
+ this.comments.update((list) => [event, ...list]);
927
+ this.commentsTotal.update((n) => n + 1);
928
+ this.comment.set('');
929
+ this.commentInternal.set(false);
930
+ const updated = {
931
+ ...t, comments_count: t.comments_count + 1,
932
+ last_activity: { at: event.created_at, actor: event.actor },
933
+ ...(event.internal ? {} : { last_comment: { comment: event.comment ?? comment, created_at: event.created_at } }),
934
+ ...(event.internal || t.status === 'concluida' || t.status === 'recusada'
935
+ ? {} : { waiting_on: 'solicitante', waiting_since: event.created_at }),
936
+ };
937
+ this.current.set(updated);
938
+ this.changed.emit(updated);
939
+ this.chat()?.scrollToEnd();
940
+ }
941
+ catch {
942
+ this.commentError.set(this.text.commentError);
943
+ }
944
+ finally {
945
+ this.commenting.set(false);
946
+ }
947
+ }
948
+ // --- nova tarefa ---
949
+ async create() {
950
+ const title = this.draftTitle().trim();
951
+ this.draftTitleError.set(title ? '' : this.text.titleRequired);
952
+ if (!title || this.busy())
953
+ return;
954
+ this.busy.set(true);
955
+ try {
956
+ const task = await this.service.create(this.clientId(), {
957
+ title, kind: this.draftKind(), status: this.draftColumn(),
958
+ priority: this.draftPriority(), start_date: this.draftStart(), due_date: this.draftDue(),
959
+ description_html: this.draftDescription(), label_ids: this.draftLabels(),
960
+ people: this.draftAssignMe() ? [{ user_id: 'me', role: 'responsavel' }] : [],
961
+ });
962
+ this.toast.success(this.text.taskCreated);
963
+ this.created.emit(task);
964
+ }
965
+ catch {
966
+ this.toast.error(this.text.createError);
967
+ }
968
+ finally {
969
+ this.busy.set(false);
970
+ }
971
+ }
972
+ // --- textos ---
973
+ personName(p) {
974
+ return p?.name || p?.username || this.text.removedAuthor;
975
+ }
976
+ statusLabel(status) {
977
+ if (!status)
978
+ return '—';
979
+ return this.text.statuses[status] ?? this.text.legacyStatuses[status] ?? status;
980
+ }
981
+ /** O valor de um evento 'change' legível: prioridade e tipo pelo catálogo, data no formato local. */
982
+ valueText(field, value) {
983
+ if (!value)
984
+ return '—';
985
+ if (field === 'priority')
986
+ return this.text.priorities[value] ?? value;
987
+ if (field === 'kind')
988
+ return this.text.kinds[value] ?? value;
989
+ if (field === 'start_date' || field === 'due_date')
990
+ return this.day(value);
991
+ return value;
992
+ }
993
+ eventText(e) {
994
+ const actor = e.actor ? this.personName(e.actor) : this.text.someone;
995
+ if (e.kind === 'comment')
996
+ return formatMessage(this.text.eventComment, { actor });
997
+ if (e.kind === 'status')
998
+ return formatMessage(this.text.eventStatus, { actor, from: this.statusLabel(e.from_status), to: this.statusLabel(e.to_status) });
999
+ const field = e.field ?? 'other';
1000
+ const removed = ['start_date', 'due_date', 'cover'].includes(field) && !e.to_value ? `${field}_removed` : field;
1001
+ const template = this.text.activity[removed] ?? this.text.activity.other;
1002
+ return formatMessage(template, { actor, from: this.valueText(e.field, e.from_value), to: this.valueText(e.field, e.to_value) });
1003
+ }
1004
+ lastChange(t) {
1005
+ if (!t.last_activity)
1006
+ return '';
1007
+ const when = this.age(t.last_activity.at);
1008
+ return t.last_activity.actor
1009
+ ? formatMessage(this.text.lastChange, { when, actor: this.personName(t.last_activity.actor) })
1010
+ : formatMessage(this.text.lastChangeNoActor, { when });
1011
+ }
1012
+ dueText(t) {
1013
+ if (!t.due_date)
1014
+ return '';
1015
+ const s = this.dueSituation();
1016
+ if (s === 'overdue')
1017
+ return formatMessage(this.text.dueLate, { date: this.day(t.due_date) });
1018
+ if (s === 'today')
1019
+ return this.text.dueToday;
1020
+ if (s === 'tomorrow')
1021
+ return this.text.dueTomorrow;
1022
+ return formatMessage(this.text.dueOn, { date: this.day(t.due_date) });
1023
+ }
1024
+ removePersonLabel(p) {
1025
+ return formatMessage(this.text.removePerson, { name: this.personName(p) });
1026
+ }
1027
+ itemLabel(template, item) {
1028
+ return formatMessage(template, { text: item.text });
1029
+ }
1030
+ age(iso) {
1031
+ const seconds = Math.round((Date.now() - new Date(iso).getTime()) / 1000);
1032
+ const units = [['year', 31_536_000], ['month', 2_592_000], ['week', 604_800], ['day', 86_400], ['hour', 3_600], ['minute', 60]];
1033
+ try {
1034
+ const rtf = new Intl.RelativeTimeFormat(this.messages.meta.locale, { numeric: 'auto' });
1035
+ for (const [unit, size] of units)
1036
+ if (Math.abs(seconds) >= size)
1037
+ return rtf.format(-Math.floor(seconds / size), unit);
1038
+ return rtf.format(0, 'minute');
1039
+ }
1040
+ catch {
1041
+ return this.date(iso);
1042
+ }
1043
+ }
1044
+ date(iso, withTime = false) {
1045
+ try {
1046
+ return new Intl.DateTimeFormat(this.messages.meta.locale, {
1047
+ dateStyle: 'short', ...(withTime ? { timeStyle: 'short' } : {}), timeZone: this.preferences.effectiveTimeZone(),
1048
+ }).format(new Date(iso));
1049
+ }
1050
+ catch {
1051
+ return iso.slice(0, 10);
1052
+ }
1053
+ }
1054
+ /** Data sem hora (AAAA-MM-DD), sem fuso: o dia é o dia. */
1055
+ day(iso) {
1056
+ try {
1057
+ return new Intl.DateTimeFormat(this.messages.meta.locale, { dateStyle: 'short', timeZone: 'UTC' }).format(new Date(`${iso}T00:00:00Z`));
1058
+ }
1059
+ catch {
1060
+ return iso;
1061
+ }
1062
+ }
1063
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionTaskDetailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1064
+ 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" }, viewQueries: [{ propertyName: "chat", first: true, predicate: SpineSuggestionChatComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<hm-modal [heading]=\"heading()\" [open]=\"open()\" (openChange)=\"setOpen($event)\" size=\"xlarge\">\n @if (current(); as t) {\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 <p class=\"as-task__dates\">\n <span>{{ text.createdAt }} {{ date(t.created_at, true) }}</span>\n <span aria-hidden=\"true\">\u00B7</span>\n <span>{{ text.updatedAt }} {{ date(t.updated_at, true) }}</span>\n @if (lastChange(t); as change) {\n <span aria-hidden=\"true\">\u00B7</span>\n <span>{{ change }}</span>\n }\n </p>\n\n <!-- Abas: Resumo (descri\u00E7\u00E3o e checklist), Coment\u00E1rios (a conversa com quem pediu) e Hist\u00F3rico (tudo o que mudou). -->\n <hm-tabs [label]=\"text.taskLabel\" [active]=\"tab()\" (activeChange)=\"selectTab($event)\">\n <hm-tab id=\"summary\" [label]=\"text.tabSummary\">\n <div class=\"as-task__summary\">\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) {\n <span class=\"as-task__count\">{{ t.checklist.done }}/{{ t.checklist.total }}</span>\n <span 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 </span>\n }\n </h3>\n <hm-switch [label]=\"text.checklistOnCard\" [value]=\"t.checklist_on_card\" (valueChange)=\"toggleChecklistOnCard($event)\" />\n </div>\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 </div>\n </hm-tab>\n <hm-tab id=\"comments\" [label]=\"text.tabComments\" [count]=\"commentsTotal() || null\">\n <!-- A conversa em chat: \u00E0 direita o que a pessoa escreveu como equipe; quem pediu (inclusive ela mesma pela\n p\u00E1gina de quem pede) \u00E0 esquerda, com o papel no cabe\u00E7alho. O \"S\u00F3 para a equipe\" vai junto do Enviar. -->\n <spine-suggestion-chat class=\"as-task__chat\" side=\"equipe\" [me]=\"me()\" [items]=\"chatItems()\"\n [label]=\"text.tabComments\" [empty]=\"text.commentsEmpty\"\n [hasMore]=\"comments().length < commentsTotal()\" [loading]=\"eventsLoading()\"\n [composerLabel]=\"text.comment\" [placeholder]=\"commentInternal() ? text.commentInternal : text.commentHint\"\n [sendLabel]=\"text.commentSend\" [sending]=\"commenting()\" [error]=\"commentError()\"\n [(draft)]=\"comment\" (loadHistory)=\"loadEvents('comments', false)\" (send)=\"sendComment($event)\">\n <hm-checkbox spineChatComposer [label]=\"text.commentInternal\" [checked]=\"commentInternal()\"\n (checkedChange)=\"commentInternal.set($event)\" />\n </spine-suggestion-chat>\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 <!-- A conversa com quem pede: com quem est\u00E1 a vez (muda sozinho a cada mensagem; aqui d\u00E1 para acertar). -->\n <hm-select [label]=\"text.menuWaiting\" [value]=\"t.waiting_on ?? ''\" [disabled]=\"busy()\" (valueChange)=\"changeWaiting($any($event))\">\n <option value=\"\">{{ text.waitingClear }}</option>\n @for (w of waitingValues; track w) { <option [value]=\"w\">{{ text.waiting[w] }}</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 <!-- Em linha, como os grupos: o que n\u00E3o cabe vira \"+N\" (cada r\u00F3tulo na cor dele). -->\n <spine-tag-row [label]=\"text.labels\" [items]=\"labelTags(t)\" [empty]=\"text.noLabels\" />\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 <dl class=\"as-facts\">\n <dt>{{ text.screenOrigin }}</dt>\n <dd class=\"as-facts__screen\">\n @if (t.screen.new) { <hm-tag [name]=\"text.newScreenTag\" color=\"info\" /> }\n <span class=\"as-facts__screen-name\" [attr.title]=\"t.screen.label || null\">{{ t.screen.label || text.noScreen }}</span>\n @if (t.screen.path && canOpenScreen()) {\n <a class=\"as-facts__link\" [href]=\"screenHref(t.screen.path)\" [attr.aria-label]=\"text.openScreen\" [attr.title]=\"text.openScreen\"\n (click)=\"openScreen($event, t.screen.path)\"><hm-icon shape=\"pop-out\" /></a>\n }\n </dd>\n <dt>{{ text.votesField }}</dt>\n <dd>{{ t.votes }}</dd>\n </dl>\n\n <!-- Os grupos de quem pediu: em linha, no modelo dos r\u00F3tulos; o que n\u00E3o cabe vira \"+N\". -->\n <div class=\"as-field\">\n <span class=\"as-field__label\">{{ text.requestingGroups }}</span>\n <spine-tag-row [label]=\"text.requestingGroups\" [items]=\"t.groups ?? []\" [empty]=\"text.noGroups\" />\n </div>\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-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__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__dates{display:flex;flex-wrap:wrap;gap:var(--hm-space-micro) var(--hm-space-xxxs);margin:calc(var(--hm-space-sm) * -1) 0 0;color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.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{display:inline-block;inline-size:6rem;block-size:.25rem;border-radius:var(--hm-radius-pill);background:var(--hm-hover);overflow:hidden}.as-task__progress span{display:block;block-size:100%;background:var(--hm-success)}.as-task__summary{display:flex;flex-direction:column;gap:var(--hm-space-md);padding-block-start:var(--hm-space-sm)}.as-task__chat{--spine-chat-height: min(68dvh, 42rem);margin-block-start: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-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;flex:none;align-items:center;gap:var(--hm-space-micro);color:var(--hm-accent)}.as-facts__screen{display:flex;align-items:center;gap:var(--hm-space-xxxs);min-inline-size:0}.as-facts__screen-name{min-inline-size:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.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)}.as-sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}\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: 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: SpineSuggestionChatComponent, selector: "spine-suggestion-chat", inputs: ["items", "side", "me", "label", "empty", "hasMore", "loading", "canWrite", "composerLabel", "placeholder", "sendLabel", "sending", "error", "draft"], outputs: ["draftChange", "loadHistory", "send"] }, { kind: "component", type: SpineTagRowComponent, selector: "spine-tag-row", inputs: ["items", "label", "empty"] }, { 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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1065
+ }
1066
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionTaskDetailComponent, decorators: [{
1067
+ type: Component,
1068
+ args: [{ selector: 'spine-suggestion-task-detail', imports: [
1069
+ AccountAvatarComponent, AlertComponent, ButtonComponent, CheckboxComponent, DatePickerComponent, IconButtonComponent,
1070
+ IconComponent, InputComponent, ModalComponent, PopoverComponent, PopoverTriggerDirective, RichTextComponent,
1071
+ SelectComponent, SuggestionLabelPickerComponent, SuggestionPeoplePickerComponent,
1072
+ SpineSuggestionChatComponent, SpineTagRowComponent, SwitchComponent, TabComponent, TabsComponent, TagComponent, TextareaComponent,
1073
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<hm-modal [heading]=\"heading()\" [open]=\"open()\" (openChange)=\"setOpen($event)\" size=\"xlarge\">\n @if (current(); as t) {\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 <p class=\"as-task__dates\">\n <span>{{ text.createdAt }} {{ date(t.created_at, true) }}</span>\n <span aria-hidden=\"true\">\u00B7</span>\n <span>{{ text.updatedAt }} {{ date(t.updated_at, true) }}</span>\n @if (lastChange(t); as change) {\n <span aria-hidden=\"true\">\u00B7</span>\n <span>{{ change }}</span>\n }\n </p>\n\n <!-- Abas: Resumo (descri\u00E7\u00E3o e checklist), Coment\u00E1rios (a conversa com quem pediu) e Hist\u00F3rico (tudo o que mudou). -->\n <hm-tabs [label]=\"text.taskLabel\" [active]=\"tab()\" (activeChange)=\"selectTab($event)\">\n <hm-tab id=\"summary\" [label]=\"text.tabSummary\">\n <div class=\"as-task__summary\">\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) {\n <span class=\"as-task__count\">{{ t.checklist.done }}/{{ t.checklist.total }}</span>\n <span 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 </span>\n }\n </h3>\n <hm-switch [label]=\"text.checklistOnCard\" [value]=\"t.checklist_on_card\" (valueChange)=\"toggleChecklistOnCard($event)\" />\n </div>\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 </div>\n </hm-tab>\n <hm-tab id=\"comments\" [label]=\"text.tabComments\" [count]=\"commentsTotal() || null\">\n <!-- A conversa em chat: \u00E0 direita o que a pessoa escreveu como equipe; quem pediu (inclusive ela mesma pela\n p\u00E1gina de quem pede) \u00E0 esquerda, com o papel no cabe\u00E7alho. O \"S\u00F3 para a equipe\" vai junto do Enviar. -->\n <spine-suggestion-chat class=\"as-task__chat\" side=\"equipe\" [me]=\"me()\" [items]=\"chatItems()\"\n [label]=\"text.tabComments\" [empty]=\"text.commentsEmpty\"\n [hasMore]=\"comments().length < commentsTotal()\" [loading]=\"eventsLoading()\"\n [composerLabel]=\"text.comment\" [placeholder]=\"commentInternal() ? text.commentInternal : text.commentHint\"\n [sendLabel]=\"text.commentSend\" [sending]=\"commenting()\" [error]=\"commentError()\"\n [(draft)]=\"comment\" (loadHistory)=\"loadEvents('comments', false)\" (send)=\"sendComment($event)\">\n <hm-checkbox spineChatComposer [label]=\"text.commentInternal\" [checked]=\"commentInternal()\"\n (checkedChange)=\"commentInternal.set($event)\" />\n </spine-suggestion-chat>\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 <!-- A conversa com quem pede: com quem est\u00E1 a vez (muda sozinho a cada mensagem; aqui d\u00E1 para acertar). -->\n <hm-select [label]=\"text.menuWaiting\" [value]=\"t.waiting_on ?? ''\" [disabled]=\"busy()\" (valueChange)=\"changeWaiting($any($event))\">\n <option value=\"\">{{ text.waitingClear }}</option>\n @for (w of waitingValues; track w) { <option [value]=\"w\">{{ text.waiting[w] }}</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 <!-- Em linha, como os grupos: o que n\u00E3o cabe vira \"+N\" (cada r\u00F3tulo na cor dele). -->\n <spine-tag-row [label]=\"text.labels\" [items]=\"labelTags(t)\" [empty]=\"text.noLabels\" />\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 <dl class=\"as-facts\">\n <dt>{{ text.screenOrigin }}</dt>\n <dd class=\"as-facts__screen\">\n @if (t.screen.new) { <hm-tag [name]=\"text.newScreenTag\" color=\"info\" /> }\n <span class=\"as-facts__screen-name\" [attr.title]=\"t.screen.label || null\">{{ t.screen.label || text.noScreen }}</span>\n @if (t.screen.path && canOpenScreen()) {\n <a class=\"as-facts__link\" [href]=\"screenHref(t.screen.path)\" [attr.aria-label]=\"text.openScreen\" [attr.title]=\"text.openScreen\"\n (click)=\"openScreen($event, t.screen.path)\"><hm-icon shape=\"pop-out\" /></a>\n }\n </dd>\n <dt>{{ text.votesField }}</dt>\n <dd>{{ t.votes }}</dd>\n </dl>\n\n <!-- Os grupos de quem pediu: em linha, no modelo dos r\u00F3tulos; o que n\u00E3o cabe vira \"+N\". -->\n <div class=\"as-field\">\n <span class=\"as-field__label\">{{ text.requestingGroups }}</span>\n <spine-tag-row [label]=\"text.requestingGroups\" [items]=\"t.groups ?? []\" [empty]=\"text.noGroups\" />\n </div>\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-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__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__dates{display:flex;flex-wrap:wrap;gap:var(--hm-space-micro) var(--hm-space-xxxs);margin:calc(var(--hm-space-sm) * -1) 0 0;color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.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{display:inline-block;inline-size:6rem;block-size:.25rem;border-radius:var(--hm-radius-pill);background:var(--hm-hover);overflow:hidden}.as-task__progress span{display:block;block-size:100%;background:var(--hm-success)}.as-task__summary{display:flex;flex-direction:column;gap:var(--hm-space-md);padding-block-start:var(--hm-space-sm)}.as-task__chat{--spine-chat-height: min(68dvh, 42rem);margin-block-start: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-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;flex:none;align-items:center;gap:var(--hm-space-micro);color:var(--hm-accent)}.as-facts__screen{display:flex;align-items:center;gap:var(--hm-space-xxxs);min-inline-size:0}.as-facts__screen-name{min-inline-size:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.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)}.as-sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}\n"] }]
1074
+ }], 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"] }], chat: [{ type: i0.ViewChild, args: [i0.forwardRef(() => SpineSuggestionChatComponent), { isSignal: true }] }] } });
1075
+
1076
+ /** Sem acento e sem caixa: "relatório" acha "RELATORIO". */
1077
+ const fold$1 = (s) => s.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
1078
+ /** Teto da lista do seletor: a busca afunila o resto. */
1079
+ const MAX_OPTIONS = 50;
1080
+ /**
1081
+ * "Agrupar em…": escolhe a sugestão principal para uma repetida (`source`), entre as abertas do quadro (`candidates`,
1082
+ * 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
1083
+ * relator passam para SUG-12"); "Agrupar" confirma (`HermesDialogService.confirm`) e grava. Uma sugestão que já recebeu
1084
+ * outras não pode ser agrupada (sem cadeia): o seletor avisa em vez de listar.
1085
+ */
1086
+ class SuggestionMergePickerComponent {
1087
+ constructor() {
1088
+ this.messages = inject(SPINE_MESSAGES);
1089
+ this.text = this.messages.suggestions;
1090
+ this.service = inject(AutoServiceSuggestionsService);
1091
+ this.dialogs = inject(HermesDialogService);
1092
+ this.toast = inject(HermesToastService);
1093
+ this.clientId = input.required(...(ngDevMode ? [{ debugName: "clientId" }] : /* istanbul ignore next */ []));
1094
+ /** A que vai ser agrupada; null = fechado. */
1095
+ this.source = input(null, ...(ngDevMode ? [{ debugName: "source" }] : /* istanbul ignore next */ []));
1096
+ /** As do quadro (o seletor tira a própria e as agrupadas). */
1097
+ this.candidates = input([], ...(ngDevMode ? [{ debugName: "candidates" }] : /* istanbul ignore next */ []));
1098
+ /** Agrupou: o destino inteiro, como o servidor devolveu. */
1099
+ this.merged = output();
1100
+ this.closed = output();
1101
+ this.statusColors = SPINE_SUGGESTION_STATUS_COLORS;
1102
+ this.term = signal('', ...(ngDevMode ? [{ debugName: "term" }] : /* istanbul ignore next */ []));
1103
+ this.picked = signal(null, ...(ngDevMode ? [{ debugName: "picked" }] : /* istanbul ignore next */ []));
1104
+ this.preview = signal(null, ...(ngDevMode ? [{ debugName: "preview" }] : /* istanbul ignore next */ []));
1105
+ this.previewError = signal(false, ...(ngDevMode ? [{ debugName: "previewError" }] : /* istanbul ignore next */ []));
1106
+ this.busy = signal(false, ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
1107
+ this.heading = computed(() => formatMessage(this.text.mergeHeading, { key: this.source()?.key ?? '' }), ...(ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []));
1108
+ this.options = computed(() => {
1109
+ const source = this.source();
1110
+ const term = fold$1(this.term().trim());
1111
+ return this.candidates()
1112
+ .filter((c) => c.id !== source?.id && !c.merged_into)
1113
+ .filter((c) => !term || fold$1(`${c.key} ${c.title}`).includes(term))
1114
+ .slice(0, MAX_OPTIONS);
1115
+ }, ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
1116
+ // Cada abertura começa do zero.
1117
+ effect(() => {
1118
+ this.source();
1119
+ untracked(() => {
1120
+ this.term.set('');
1121
+ this.picked.set(null);
1122
+ this.preview.set(null);
1123
+ this.previewError.set(false);
1124
+ });
1125
+ });
1126
+ }
1127
+ onOpen(open) {
1128
+ if (!open)
1129
+ this.closed.emit();
1130
+ }
1131
+ async pick(target) {
1132
+ const source = this.source();
1133
+ if (!source || this.picked()?.id === target.id)
1134
+ return;
1135
+ this.picked.set(target);
1136
+ this.preview.set(null);
1137
+ this.previewError.set(false);
1138
+ try {
1139
+ const preview = await this.service.mergePreview(this.clientId(), source.id, target.id);
1140
+ if (this.picked()?.id === target.id)
1141
+ this.preview.set(preview);
1142
+ }
1143
+ catch {
1144
+ if (this.picked()?.id === target.id)
1145
+ this.previewError.set(true);
1146
+ }
1147
+ }
1148
+ previewText(p) {
1149
+ const locale = this.messages.meta.locale;
1150
+ return formatMessage(this.text.mergePreview, {
1151
+ votes: pluralMessage(this.text.votes, p.votes, locale),
1152
+ reporters: pluralMessage(this.text.reportersCount, p.reporters, locale),
1153
+ key: p.target.key,
1154
+ });
1155
+ }
1156
+ votesText(count) {
1157
+ return pluralMessage(this.text.votes, count, this.messages.meta.locale);
1158
+ }
1159
+ async confirm() {
1160
+ const source = this.source();
1161
+ const preview = this.preview();
1162
+ const target = this.picked();
1163
+ if (!source || !preview || !target || this.busy())
1164
+ return;
1165
+ const ok = await this.dialogs.confirm({
1166
+ heading: formatMessage(this.text.mergeConfirmHeading, { key: source.key, target: target.key }),
1167
+ message: `${this.previewText(preview)} ${this.text.mergeConfirmMessage}`,
1168
+ confirmLabel: this.text.mergeConfirm,
1169
+ });
1170
+ if (!ok)
1171
+ return;
1172
+ this.busy.set(true);
1173
+ try {
1174
+ const saved = await this.service.merge(this.clientId(), source.id, target.id);
1175
+ this.toast.success(formatMessage(this.text.mergedToast, { key: source.key, target: target.key }));
1176
+ this.merged.emit({ source, target: saved });
1177
+ }
1178
+ catch {
1179
+ this.toast.error(this.text.mergeError);
1180
+ }
1181
+ finally {
1182
+ this.busy.set(false);
1183
+ }
1184
+ }
1185
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionMergePickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1186
+ 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: `
1187
+ <hm-modal [heading]="heading()" [open]="!!source()" (openChange)="onOpen($event)">
1188
+ @if (source(); as s) {
1189
+ <div class="as-merge">
1190
+ <p class="as-merge__intro">{{ text.mergeIntro }}</p>
1191
+ @if (s.merged_count) {
1192
+ <hm-alert variant="warning">{{ text.mergeHasMerged }}</hm-alert>
1193
+ } @else {
1194
+ <hm-search-field [label]="text.mergeSearch" labelHidden [placeholder]="text.mergeSearch" [landmark]="false"
1195
+ [value]="term()" (search)="term.set($event)" />
1196
+ <ul class="as-merge__list" [attr.aria-label]="text.mergeSearch">
1197
+ @for (c of options(); track c.id) {
1198
+ <li>
1199
+ <button type="button" class="as-merge__option" [class.as-merge__option--on]="picked()?.id === c.id"
1200
+ [attr.aria-pressed]="picked()?.id === c.id" (click)="pick(c)">
1201
+ <span class="as-merge__key">{{ c.key }}</span>
1202
+ <span class="as-merge__title">{{ c.title }}</span>
1203
+ <span class="as-merge__meta">
1204
+ <hm-tag [name]="text.statuses[c.status]" [color]="statusColors[c.status]" />
1205
+ <span>{{ votesText(c.votes) }}</span>
1206
+ </span>
1207
+ </button>
1208
+ </li>
1209
+ } @empty {
1210
+ <li class="as-merge__empty">{{ text.mergeNone }}</li>
1211
+ }
1212
+ </ul>
1213
+ @if (picked()) {
1214
+ <p class="as-merge__preview" aria-live="polite">
1215
+ @if (previewError()) { {{ text.mergePreviewError }} }
1216
+ @else if (preview(); as p) { {{ previewText(p) }} }
1217
+ @else { {{ text.mergeCalculating }} }
1218
+ </p>
1219
+ }
1220
+ }
1221
+ </div>
1222
+ }
1223
+ <div footer class="as-merge__footer">
1224
+ <hm-button emphasis="medium" (clicked)="closed.emit()">{{ text.cancel }}</hm-button>
1225
+ <hm-button icon="layers" [disabled]="!preview() || !!source()?.merged_count" [loading]="busy()" (clicked)="confirm()">
1226
+ {{ text.mergeConfirm }}
1227
+ </hm-button>
1228
+ </div>
1229
+ </hm-modal>
1230
+ `, 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 }); }
1231
+ }
1232
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionMergePickerComponent, decorators: [{
1233
+ type: Component,
1234
+ args: [{ selector: 'spine-suggestion-merge-picker', imports: [AlertComponent, ButtonComponent, ModalComponent, SearchFieldComponent, TagComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
1235
+ <hm-modal [heading]="heading()" [open]="!!source()" (openChange)="onOpen($event)">
1236
+ @if (source(); as s) {
1237
+ <div class="as-merge">
1238
+ <p class="as-merge__intro">{{ text.mergeIntro }}</p>
1239
+ @if (s.merged_count) {
1240
+ <hm-alert variant="warning">{{ text.mergeHasMerged }}</hm-alert>
1241
+ } @else {
1242
+ <hm-search-field [label]="text.mergeSearch" labelHidden [placeholder]="text.mergeSearch" [landmark]="false"
1243
+ [value]="term()" (search)="term.set($event)" />
1244
+ <ul class="as-merge__list" [attr.aria-label]="text.mergeSearch">
1245
+ @for (c of options(); track c.id) {
1246
+ <li>
1247
+ <button type="button" class="as-merge__option" [class.as-merge__option--on]="picked()?.id === c.id"
1248
+ [attr.aria-pressed]="picked()?.id === c.id" (click)="pick(c)">
1249
+ <span class="as-merge__key">{{ c.key }}</span>
1250
+ <span class="as-merge__title">{{ c.title }}</span>
1251
+ <span class="as-merge__meta">
1252
+ <hm-tag [name]="text.statuses[c.status]" [color]="statusColors[c.status]" />
1253
+ <span>{{ votesText(c.votes) }}</span>
1254
+ </span>
1255
+ </button>
1256
+ </li>
1257
+ } @empty {
1258
+ <li class="as-merge__empty">{{ text.mergeNone }}</li>
1259
+ }
1260
+ </ul>
1261
+ @if (picked()) {
1262
+ <p class="as-merge__preview" aria-live="polite">
1263
+ @if (previewError()) { {{ text.mergePreviewError }} }
1264
+ @else if (preview(); as p) { {{ previewText(p) }} }
1265
+ @else { {{ text.mergeCalculating }} }
1266
+ </p>
1267
+ }
1268
+ }
1269
+ </div>
1270
+ }
1271
+ <div footer class="as-merge__footer">
1272
+ <hm-button emphasis="medium" (clicked)="closed.emit()">{{ text.cancel }}</hm-button>
1273
+ <hm-button icon="layers" [disabled]="!preview() || !!source()?.merged_count" [loading]="busy()" (clicked)="confirm()">
1274
+ {{ text.mergeConfirm }}
1275
+ </hm-button>
1276
+ </div>
1277
+ </hm-modal>
1278
+ `, 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"] }]
1279
+ }], 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"] }] } });
1280
+
1281
+ /** As colunas do quadro: recusada não tem coluna (sai na hora; fica no arquivo). */
1282
+ const BOARD_STATUSES = ['em_analise', 'planejada', 'em_desenvolvimento', 'concluida'];
1283
+ const OPEN_STATUSES = ['em_analise', 'planejada', 'em_desenvolvimento'];
1284
+ /** Quantas levam o selo "Mais votada" (das com algum apoio, no quadro inteiro, não só no filtrado). */
1285
+ const TOP_VOTED = 3;
1286
+ /** Responsáveis que aparecem no cartão; o resto vira "+N". */
1287
+ const FACES = 3;
1288
+ const ARCHIVE_PAGE = 20;
1289
+ const EXCERPT = 140;
1290
+ /** Parâmetro do link copiado ("?sugestao=SUG-12"): abrir a tela com ele abre a tarefa. */
1291
+ const SUGGESTION_LINK_PARAM = 'sugestao';
1292
+ const PRIORITY_RANK = { urgente: 0, alta: 1, media: 2, baixa: 3 };
1293
+ const byPosition = (a, b) => a.position - b.position || a.created_at.localeCompare(b.created_at);
1294
+ const SORTS = {
1295
+ position: byPosition,
1296
+ votes: (a, b) => b.votes - a.votes || byPosition(a, b),
1297
+ recent: (a, b) => b.created_at.localeCompare(a.created_at),
1298
+ priority: (a, b) => PRIORITY_RANK[a.priority] - PRIORITY_RANK[b.priority] || byPosition(a, b),
1299
+ due: (a, b) => (a.due_date ?? '9999').localeCompare(b.due_date ?? '9999') || byPosition(a, b),
1300
+ };
1301
+ /** Sem acento e minúsculo: "analise" acha "Análise". */
1302
+ const fold = (s) => s.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
1303
+ /** Intervalos do "há 3 dias" (segundos por unidade), do maior para o menor. */
1304
+ const AGES = [
1305
+ ['year', 31_536_000], ['month', 2_592_000], ['week', 604_800], ['day', 86_400], ['hour', 3_600], ['minute', 60],
1306
+ ];
1307
+ /**
1308
+ * O quadro de trabalho das sugestões do app (`hm-kanban`), montado na tela Sugestões do auto-serviço.
1309
+ *
1310
+ * - **Quadro**: o servidor manda o recorte (`board=true`): abertas e as concluídas dos últimos `done_days` dias;
1311
+ * recusadas e arquivadas saem. Indicadores do quadro inteiro, tudo calculado aqui com o que já vem (vencidas e as
1312
+ * que venceram ontem, vencendo em breve, em andamento por status, concluídas na janela e hoje, atribuídas a mim e as
1313
+ * abertas por prioridade; vencidas, vencendo, minhas e cada prioridade também filtram) e os filtros da tela — texto
1314
+ * (inclusive a chave "SUG-12"), responsável ("minhas", "sem responsável"), tipo, status (só aquela coluna),
1315
+ * prioridade, rótulo, prazo, e em "Mais filtros" tela e quem pediu, além da ordenação — sobre a lista inteira.
1316
+ * - **Coluna**: "Mais antiga há 4h 12min" (pelo `status_at`) e as vencidas abaixo do título, vazio explicado e
1317
+ * "+ Criar tarefa" no rodapé (os templates `hmKanbanColumnSummary`/`Empty`/`Footer` do `hm-kanban`).
1318
+ * - **Cartão**: capa (token da paleta), chave, selo de prioridade, título, contexto (tipo · tela), rótulos, trecho,
1319
+ * checklist (e os itens, com "Mostrar no cartão"), prazo por extenso (vencida em vermelho; hoje/amanhã em âmbar) e o
1320
+ * rodapé com comentários, checklist, apoios, a data e os responsáveis com "+N"; o "⋮": status e prioridade
1321
+ * (submenus), atribuir a mim, copiar link e chave, rótulos, capa (popover), arquivar e excluir (com confirmação).
1322
+ * - **Criar**: sempre o cadastro completo (a tarefa em branco no modal), já com o status da coluna: "Nova tarefa" no
1323
+ * topo (o "▾" escolhe a coluna), o "+" do cabeçalho e o "+ Criar tarefa" do rodapé de cada coluna.
1324
+ * - **Arrastar** (ponteiro, toque ou teclado; o `hm-kanban` cuida do gesto e dos anúncios) muda status e/ou posição: a
1325
+ * tela aplica na hora e grava; se o servidor recusar, avisa e relê. A posição gravada é sempre a da coluna INTEIRA
1326
+ * (pelo vizinho visível). Fora da "ordem do quadro", arrastar só troca de coluna (vai para o fim).
1327
+ * - **Arquivo**: arquivadas, recusadas, agrupadas (selo "Agrupada", com "Desagrupar") e concluídas antigas, paginado e
1328
+ * buscado no servidor, com "Reabrir"/"Restaurar".
1329
+ * - **Agrupar**: "Agrupar em…" no "⋯" do cartão e no detalhe abre o seletor (`spine-suggestion-merge-picker`) com as
1330
+ * abertas do quadro e a prévia do servidor; confirmar agrupa, tira a origem do quadro e abre o destino.
1331
+ * - **Tarefa**: `spine-suggestion-task-detail` (modal de duas colunas); o link "?sugestao=SUG-12" abre direto nela.
1332
+ */
1333
+ class AutoServiceSuggestionsComponent {
1334
+ constructor() {
1335
+ this.messages = inject(SPINE_MESSAGES);
1336
+ this.text = this.messages.suggestions;
1337
+ this.service = inject(AutoServiceSuggestionsService);
1338
+ this.toast = inject(HermesToastService);
1339
+ this.dialogs = inject(HermesDialogService);
1340
+ this.preferences = inject(SpinePreferencesService);
1341
+ this.userStorage = inject(SpineUserStorage, { optional: true });
1342
+ this.router = inject(Router, { optional: true });
1343
+ this.location = inject(Location, { optional: true });
1344
+ /** O client da sessão: a tela da sugestão só abre daqui se o app administrado for este. */
1345
+ this.sessionClientId = inject(AUTH_CONFIG, { optional: true })?.clientId ?? null;
1346
+ this.clientId = input.required(...(ngDevMode ? [{ debugName: "clientId" }] : /* istanbul ignore next */ []));
1347
+ this.kinds = SPINE_SUGGESTION_KINDS;
1348
+ this.statuses = SPINE_SUGGESTION_STATUSES;
1349
+ this.statusColors = SPINE_SUGGESTION_STATUS_COLORS;
1350
+ this.statusIcons = SPINE_SUGGESTION_STATUS_ICONS;
1351
+ this.kindColors = SPINE_SUGGESTION_KIND_COLORS;
1352
+ this.waitingValues = SPINE_SUGGESTION_WAITING;
1353
+ this.waitingColors = SPINE_SUGGESTION_WAITING_COLORS;
1354
+ this.newScreenValue = NEW_SCREEN_VALUE;
1355
+ this.priorities = TASK_PRIORITIES;
1356
+ this.sortKeys = ['position', 'votes', 'recent', 'priority', 'due'];
1357
+ this.today = todayIso(this.preferences.effectiveTimeZone());
1358
+ this.mode = signal('board', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
1359
+ this.modeOptions = [
1360
+ { value: 'board', label: this.text.viewBoard, icon: 'view-columns' },
1361
+ { value: 'archive', label: this.text.viewArchive, icon: 'archive' },
1362
+ ];
1363
+ // --- quadro ---
1364
+ this.all = signal([], ...(ngDevMode ? [{ debugName: "all" }] : /* istanbul ignore next */ []));
1365
+ this.catalog = signal([], ...(ngDevMode ? [{ debugName: "catalog" }] : /* istanbul ignore next */ []));
1366
+ this.doneDays = signal(AUTO_SERVICE_SUGGESTION_DONE_DAYS, ...(ngDevMode ? [{ debugName: "doneDays" }] : /* istanbul ignore next */ []));
1367
+ this.loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
1368
+ this.loadError = signal('', ...(ngDevMode ? [{ debugName: "loadError" }] : /* istanbul ignore next */ []));
1369
+ this.search = signal('', ...(ngDevMode ? [{ debugName: "search" }] : /* istanbul ignore next */ []));
1370
+ this.kind = signal('', ...(ngDevMode ? [{ debugName: "kind" }] : /* istanbul ignore next */ []));
1371
+ this.screen = signal('', ...(ngDevMode ? [{ debugName: "screen" }] : /* istanbul ignore next */ []));
1372
+ this.author = signal('', ...(ngDevMode ? [{ debugName: "author" }] : /* istanbul ignore next */ []));
1373
+ /** '' | 'me' | 'none' | user_id */
1374
+ this.assignee = signal('', ...(ngDevMode ? [{ debugName: "assignee" }] : /* istanbul ignore next */ []));
1375
+ this.label = signal('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1376
+ this.priority = signal('', ...(ngDevMode ? [{ debugName: "priority" }] : /* istanbul ignore next */ []));
1377
+ /** '' | 'overdue' | 'soon' */
1378
+ this.due = signal('', ...(ngDevMode ? [{ debugName: "due" }] : /* istanbul ignore next */ []));
1379
+ /** '' | um status do quadro: só essa coluna aparece. */
1380
+ this.status = signal('', ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
1381
+ /** '' | 'solicitante' | 'responsaveis': com quem está a vez na conversa. */
1382
+ this.waiting = signal('', ...(ngDevMode ? [{ debugName: "waiting" }] : /* istanbul ignore next */ []));
1383
+ /** '' | 'none' (criada pela equipe, sem grupo) | group_id: o grupo de quem pediu. */
1384
+ this.group = signal('', ...(ngDevMode ? [{ debugName: "group" }] : /* istanbul ignore next */ []));
1385
+ this.sort = signal('position', ...(ngDevMode ? [{ debugName: "sort" }] : /* istanbul ignore next */ []));
1386
+ /** "Mais filtros" aberto (quem pediu e o grupo). */
1387
+ this.moreFilters = signal(false, ...(ngDevMode ? [{ debugName: "moreFilters" }] : /* istanbul ignore next */ []));
1388
+ this.boardStatuses = BOARD_STATUSES;
1389
+ /** Os filtros escondidos em "Mais filtros" que estão ligados (o botão mostra a conta). */
1390
+ this.extraFilters = computed(() => [this.author(), this.group()].filter(Boolean).length, ...(ngDevMode ? [{ debugName: "extraFilters" }] : /* istanbul ignore next */ []));
1391
+ this.moreFiltersText = computed(() => (this.extraFilters()
1392
+ ? formatMessage(this.text.moreFiltersActive, { count: this.extraFilters() }) : this.text.moreFilters), ...(ngDevMode ? [{ debugName: "moreFiltersText" }] : /* istanbul ignore next */ []));
1393
+ this.me = computed(() => this.userStorage?.userId() ?? '', ...(ngDevMode ? [{ debugName: "me" }] : /* istanbul ignore next */ []));
1394
+ this.filtered = computed(() => {
1395
+ const term = fold(this.search().trim());
1396
+ const keyMatch = this.search().trim().match(/^(?:sug-?)?(\d{1,9})$/i);
1397
+ const [kind, screen, author, assignee, label, priority, due, waiting, group] = [this.kind(), this.screen(),
1398
+ this.author(), this.assignee(), this.label(), this.priority(), this.due(), this.waiting(), this.group()];
1399
+ const me = this.me();
1400
+ return this.all().filter((s) => (!kind || s.kind === kind)
1401
+ && (!screen || (screen === NEW_SCREEN_VALUE ? !!s.screen.new : !s.screen.new && s.screen.path === screen))
1402
+ && (!waiting || (s.waiting_on === waiting && OPEN_STATUSES.includes(s.status)))
1403
+ && (!group || (group === 'none' ? !s.groups?.length : !!s.groups?.some((g) => g.id === group)))
1404
+ && (!author || s.author?.user_id === author)
1405
+ && (!assignee || (assignee === 'none' ? s.assignees.length === 0 : assignedTo(s, assignee === 'me' ? me : assignee)))
1406
+ && (!label || s.labels.some((l) => l.id === label))
1407
+ && (!priority || s.priority === priority)
1408
+ && (!due || this.matchesDue(s, due))
1409
+ && (!term || (keyMatch && s.key_number === Number(keyMatch[1]))
1410
+ || fold(`${s.title} ${s.description} ${s.screen.label ?? ''} ${this.personName(s.author)} ${s.key}`).includes(term)));
1411
+ }, ...(ngDevMode ? [{ debugName: "filtered" }] : /* istanbul ignore next */ []));
1412
+ this.isFiltered = computed(() => !!(this.search().trim() || this.kind() || this.screen() || this.author()
1413
+ || this.assignee() || this.label() || this.priority() || this.due() || this.status() || this.waiting() || this.group()), ...(ngDevMode ? [{ debugName: "isFiltered" }] : /* istanbul ignore next */ []));
1414
+ this.filteredText = computed(() => formatMessage(this.text.boardFiltered, {
1415
+ shown: this.columns().reduce((n, c) => n + c.items.length, 0), total: this.all().length,
1416
+ }), ...(ngDevMode ? [{ debugName: "filteredText" }] : /* istanbul ignore next */ []));
1417
+ this.columns = computed(() => {
1418
+ const list = this.filtered();
1419
+ const order = SORTS[this.sort()];
1420
+ const only = this.status();
1421
+ return BOARD_STATUSES.filter((status) => !only || status === only).map((status) => ({
1422
+ id: status, title: this.text.statuses[status], items: list.filter((s) => s.status === status).sort(order),
1423
+ }));
1424
+ }, ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
1425
+ /**
1426
+ * Resumo de cada coluna (o que está à vista nela): há quanto tempo a mais antiga está no status ("Mais antiga há
1427
+ * 4h 12min", pelo `status_at`) e quantas vencidas.
1428
+ */
1429
+ this.columnSummaries = computed(() => {
1430
+ const now = Date.now();
1431
+ return new Map(this.columns().map((c) => {
1432
+ const since = c.items.map((s) => Date.parse(s.status_at ?? s.created_at)).filter((t) => !Number.isNaN(t));
1433
+ const oldest = since.length ? this.duration(now - Math.min(...since)) : '';
1434
+ const overdue = c.items.filter((s) => dueState(s, this.today) === 'overdue').length;
1435
+ return [c.id, {
1436
+ oldest: oldest ? formatMessage(this.text.columnOldest, { age: oldest }) : '',
1437
+ overdue: overdue ? pluralMessage(this.text.columnOverdue, overdue, this.messages.meta.locale) : '',
1438
+ }];
1439
+ }));
1440
+ }, ...(ngDevMode ? [{ debugName: "columnSummaries" }] : /* istanbul ignore next */ []));
1441
+ /**
1442
+ * Telas, pessoas, responsáveis e grupos que aparecem no quadro, para os filtros (só o que existe). As telas na ordem
1443
+ * do caminho ("Módulo › Tela"), então as de um módulo ficam juntas; as novas (que ainda não existem) são uma opção só.
1444
+ */
1445
+ this.screens = computed(() => {
1446
+ const byPath = new Map();
1447
+ for (const s of this.all())
1448
+ if (s.screen.path && !s.screen.new)
1449
+ byPath.set(s.screen.path, s.screen.label || s.screen.path);
1450
+ return [...byPath].map(([path, label]) => ({ path, label })).sort((a, b) => a.label.localeCompare(b.label));
1451
+ }, ...(ngDevMode ? [{ debugName: "screens" }] : /* istanbul ignore next */ []));
1452
+ this.hasNewScreens = computed(() => this.all().some((s) => s.screen.new), ...(ngDevMode ? [{ debugName: "hasNewScreens" }] : /* istanbul ignore next */ []));
1453
+ this.groupOptions = computed(() => {
1454
+ const byId = new Map();
1455
+ for (const s of this.all())
1456
+ for (const g of s.groups ?? [])
1457
+ byId.set(g.id, g.name);
1458
+ return [...byId].map(([id, name]) => ({ id, name })).sort((a, b) => a.name.localeCompare(b.name));
1459
+ }, ...(ngDevMode ? [{ debugName: "groupOptions" }] : /* istanbul ignore next */ []));
1460
+ this.authors = computed(() => this.people((s) => (s.author ? [s.author] : [])), ...(ngDevMode ? [{ debugName: "authors" }] : /* istanbul ignore next */ []));
1461
+ this.assigneeOptions = computed(() => this.people((s) => s.assignees), ...(ngDevMode ? [{ debugName: "assigneeOptions" }] : /* istanbul ignore next */ []));
1462
+ /** Indicadores do quadro inteiro (o filtro não muda: é o retrato do app). */
1463
+ this.stats = computed(() => {
1464
+ const list = this.all();
1465
+ const count = (status) => list.filter((s) => s.status === status).length;
1466
+ const open = list.filter((s) => OPEN_STATUSES.includes(s.status));
1467
+ const yesterday = addDays(this.today, -1);
1468
+ const zone = this.preferences.effectiveTimeZone();
1469
+ const overdue = open.filter((s) => dueState(s, this.today) === 'overdue');
1470
+ const done = list.filter((s) => s.status === 'concluida');
1471
+ return {
1472
+ open: open.length, em_analise: count('em_analise'), planejada: count('planejada'),
1473
+ em_desenvolvimento: count('em_desenvolvimento'), concluida: done.length,
1474
+ mine: open.filter((s) => assignedTo(s, this.me())).length,
1475
+ overdue: overdue.length,
1476
+ /** Venceram ontem: passaram a vencidas hoje (o "+N desde ontem"). */
1477
+ overdueNew: overdue.filter((s) => s.due_date === yesterday).length,
1478
+ soon: open.filter((s) => { const d = dueState(s, this.today); return d === 'today' || d === 'tomorrow'; }).length,
1479
+ /** A vez é da equipe (quem pediu escreveu) e as mensagens de quem pede ainda não lidas por quem consulta. */
1480
+ waitingTeam: open.filter((s) => s.waiting_on === 'responsaveis').length,
1481
+ unread: list.reduce((n, s) => n + (s.unread ?? 0), 0),
1482
+ doneToday: done.filter((s) => todayIso(zone, new Date(s.status_at ?? s.updated_at)) === this.today).length,
1483
+ byPriority: TASK_PRIORITIES.map((p) => ({ priority: p, count: open.filter((s) => s.priority === p).length })),
1484
+ };
1485
+ }, ...(ngDevMode ? [{ debugName: "stats" }] : /* istanbul ignore next */ []));
1486
+ this.doneLabel = computed(() => formatMessage(this.text.kpiDone, { days: this.doneDays() }), ...(ngDevMode ? [{ debugName: "doneLabel" }] : /* istanbul ignore next */ []));
1487
+ /** "Em análise 3 · Planejada 4 · Em desenvolvimento 5" no indicador das abertas. */
1488
+ this.openParts = computed(() => OPEN_STATUSES.map((st) => ({
1489
+ status: st,
1490
+ text: formatMessage(this.text.kpiStatusCount, { status: this.text.statuses[st], count: this.all().filter((s) => s.status === st).length }),
1491
+ })), ...(ngDevMode ? [{ debugName: "openParts" }] : /* istanbul ignore next */ []));
1492
+ this.overdueNote = computed(() => (this.stats().overdueNew
1493
+ ? formatMessage(this.text.kpiOverdueTrend, { count: this.stats().overdueNew }) : this.text.kpiOverdueSteady), ...(ngDevMode ? [{ debugName: "overdueNote" }] : /* istanbul ignore next */ []));
1494
+ this.doneNote = computed(() => (this.stats().doneToday
1495
+ ? formatMessage(this.text.kpiDoneToday, { count: this.stats().doneToday }) : this.text.kpiDoneNoneToday), ...(ngDevMode ? [{ debugName: "doneNote" }] : /* istanbul ignore next */ []));
1496
+ this.kpiLoading = computed(() => this.loading() && !this.all().length, ...(ngDevMode ? [{ debugName: "kpiLoading" }] : /* istanbul ignore next */ []));
1497
+ /** Itens do "▾" ao lado de "Nova tarefa": criar direto numa coluna. */
1498
+ this.newTaskItems = BOARD_STATUSES.map((st) => ({
1499
+ id: st, label: formatMessage(this.text.newTaskIn, { status: this.text.statuses[st] }), icon: SPINE_SUGGESTION_STATUS_ICONS[st],
1500
+ }));
1501
+ 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 */ []));
1502
+ /** O menu "⋯" de cada cartão, montado uma vez por versão da tarefa (o menu aberto não perde o foco). */
1503
+ this.menus = computed(() => new Map(this.all().map((t) => [t.id, this.buildMenu(t)])), ...(ngDevMode ? [{ debugName: "menus" }] : /* istanbul ignore next */ []));
1504
+ this.itemKey = (s) => s.id;
1505
+ this.itemLabel = (s) => `${s.key} ${s.title}`;
1506
+ // --- arquivo ---
1507
+ this.archived = signal([], ...(ngDevMode ? [{ debugName: "archived" }] : /* istanbul ignore next */ []));
1508
+ this.archiveTotal = signal(0, ...(ngDevMode ? [{ debugName: "archiveTotal" }] : /* istanbul ignore next */ []));
1509
+ this.archivePage = signal({ page: 1, size: ARCHIVE_PAGE }, ...(ngDevMode ? [{ debugName: "archivePage" }] : /* istanbul ignore next */ []));
1510
+ this.archiveSearch = signal('', ...(ngDevMode ? [{ debugName: "archiveSearch" }] : /* istanbul ignore next */ []));
1511
+ this.archiveKind = signal('', ...(ngDevMode ? [{ debugName: "archiveKind" }] : /* istanbul ignore next */ []));
1512
+ this.archiveStatus = signal('', ...(ngDevMode ? [{ debugName: "archiveStatus" }] : /* istanbul ignore next */ []));
1513
+ this.archiveLoading = signal(false, ...(ngDevMode ? [{ debugName: "archiveLoading" }] : /* istanbul ignore next */ []));
1514
+ this.archiveError = signal('', ...(ngDevMode ? [{ debugName: "archiveError" }] : /* istanbul ignore next */ []));
1515
+ this.reopening = signal(null, ...(ngDevMode ? [{ debugName: "reopening" }] : /* istanbul ignore next */ []));
1516
+ this.archiveCountText = computed(() => pluralMessage(this.text.archiveCount, this.archiveTotal(), this.messages.meta.locale), ...(ngDevMode ? [{ debugName: "archiveCountText" }] : /* istanbul ignore next */ []));
1517
+ this.archiveEmptyHint = computed(() => formatMessage(this.text.archiveEmptyHint, { days: this.doneDays() }), ...(ngDevMode ? [{ debugName: "archiveEmptyHint" }] : /* istanbul ignore next */ []));
1518
+ // --- tarefa aberta ---
1519
+ this.selected = signal(null, ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
1520
+ this.drafting = signal(false, ...(ngDevMode ? [{ debugName: "drafting" }] : /* istanbul ignore next */ []));
1521
+ this.draftStatus = signal('em_analise', ...(ngDevMode ? [{ debugName: "draftStatus" }] : /* istanbul ignore next */ []));
1522
+ this.canOpenScreen = computed(() => !!this.sessionClientId && this.sessionClientId.toLowerCase() === this.clientId().toLowerCase(), ...(ngDevMode ? [{ debugName: "canOpenScreen" }] : /* istanbul ignore next */ []));
1523
+ this.linkHandled = false;
1524
+ // --- agrupar ---
1525
+ /** A que está sendo agrupada (o seletor aberto); o detalhe fecha antes (um modal por vez). */
1526
+ this.mergeSource = signal(null, ...(ngDevMode ? [{ debugName: "mergeSource" }] : /* istanbul ignore next */ []));
1527
+ /** Checklist no cartão: os itens abertos (por cartão, só nesta tela; começa fechado, o rodapé diz quantos faltam). */
1528
+ this.openChecklists = signal(new Set(), ...(ngDevMode ? [{ debugName: "openChecklists" }] : /* istanbul ignore next */ []));
1529
+ effect(() => {
1530
+ const clientId = this.clientId();
1531
+ untracked(() => {
1532
+ this.selected.set(null);
1533
+ this.drafting.set(false);
1534
+ void this.loadLabels(clientId);
1535
+ if (this.mode() === 'archive')
1536
+ void this.loadArchive(1, clientId);
1537
+ else
1538
+ void this.load(clientId);
1539
+ });
1540
+ });
1541
+ }
1542
+ people(of) {
1543
+ const byId = new Map();
1544
+ for (const s of this.all())
1545
+ for (const p of of(s))
1546
+ byId.set(p.user_id, p);
1547
+ return [...byId.values()].sort((a, b) => this.personName(a).localeCompare(this.personName(b)));
1548
+ }
1549
+ matchesDue(s, due) {
1550
+ const state = dueState(s, this.today);
1551
+ return due === 'overdue' ? state === 'overdue' : state === 'today' || state === 'tomorrow';
1552
+ }
1553
+ setMode(value) {
1554
+ const mode = value === 'archive' ? 'archive' : 'board';
1555
+ if (mode === this.mode())
1556
+ return;
1557
+ this.mode.set(mode);
1558
+ // Cada troca relê: o que foi reaberto no arquivo aparece no quadro e vice-versa.
1559
+ if (mode === 'archive')
1560
+ void this.loadArchive(1);
1561
+ else
1562
+ void this.load();
1563
+ }
1564
+ refresh() {
1565
+ if (this.mode() === 'archive')
1566
+ void this.loadArchive(this.archivePage().page);
1567
+ else
1568
+ void this.load();
1569
+ }
1570
+ async load(clientId = this.clientId()) {
1571
+ this.loading.set(true);
1572
+ this.loadError.set('');
1573
+ try {
1574
+ const board = await this.service.board(clientId);
1575
+ this.all.set(board.suggestions);
1576
+ this.doneDays.set(board.doneDays);
1577
+ void this.openFromLink(clientId);
1578
+ }
1579
+ catch {
1580
+ this.loadError.set(this.text.loadError);
1581
+ }
1582
+ finally {
1583
+ this.loading.set(false);
1584
+ }
1585
+ }
1586
+ async loadLabels(clientId = this.clientId()) {
1587
+ try {
1588
+ this.catalog.set(await this.service.labels(clientId));
1589
+ }
1590
+ catch {
1591
+ this.catalog.set([]);
1592
+ }
1593
+ }
1594
+ /** "?sugestao=SUG-12" na URL: abre a tarefa (uma vez por tela). */
1595
+ async openFromLink(clientId) {
1596
+ if (this.linkHandled || !this.router)
1597
+ return;
1598
+ this.linkHandled = true;
1599
+ const key = this.router.parseUrl(this.router.url).queryParams[SUGGESTION_LINK_PARAM];
1600
+ if (typeof key !== 'string' || !key)
1601
+ return;
1602
+ const known = this.all().find((s) => s.key.toLowerCase() === key.toLowerCase());
1603
+ try {
1604
+ this.openDetail(known ?? (await this.service.lookup(clientId, key)));
1605
+ }
1606
+ catch {
1607
+ this.toast.error(this.text.loadError);
1608
+ }
1609
+ }
1610
+ clearFilters() {
1611
+ for (const s of [this.search, this.kind, this.screen, this.author, this.assignee, this.label, this.priority, this.due, this.status,
1612
+ this.waiting, this.group]) {
1613
+ s.set('');
1614
+ }
1615
+ }
1616
+ /**
1617
+ * Indicador que filtra ("atribuídas a mim", "vencidas", "vencendo em breve", "aguardando responsáveis"): liga/desliga
1618
+ * o filtro equivalente.
1619
+ */
1620
+ toggleKpi(which, on) {
1621
+ if (which === 'mine')
1622
+ this.assignee.set(on ? 'me' : '');
1623
+ else if (which === 'waiting')
1624
+ this.waiting.set(on ? 'responsaveis' : '');
1625
+ else
1626
+ this.due.set(on ? which : '');
1627
+ }
1628
+ /** Uma prioridade da legenda do indicador: filtra por ela (de novo, desliga). */
1629
+ togglePriority(p) {
1630
+ this.priority.update((current) => (current === p ? '' : p));
1631
+ }
1632
+ /** Largura de cada prioridade na barra empilhada (% das abertas). */
1633
+ priorityShare(count) {
1634
+ const total = this.stats().open;
1635
+ return total ? (count / total) * 100 : 0;
1636
+ }
1637
+ priorityPart(p, count) {
1638
+ return formatMessage(this.text.kpiPriorityPart, { priority: this.text.priorities[p], count });
1639
+ }
1640
+ /**
1641
+ * O quadro já moveu o cartão (na lista filtrada); aqui vira a posição na coluna INTEIRA — logo depois do vizinho de
1642
+ * cima que está visível (ou antes do de baixo; sozinho na coluna filtrada = fim) — aplica na lista e grava. Com
1643
+ * outra ordenação, só a coluna conta: vai para o fim dela, e reordenar na mesma coluna não grava.
1644
+ */
1645
+ async onMoved(event) {
1646
+ const item = event.item;
1647
+ const to = event.to;
1648
+ const changed = event.from !== to;
1649
+ let change;
1650
+ if (this.sort() !== 'position') {
1651
+ if (!changed) {
1652
+ this.all.update((list) => [...list]); // devolve o cartão ao lugar da ordenação
1653
+ return;
1654
+ }
1655
+ const end = this.all().filter((s) => s.status === to && s.id !== item.id).length;
1656
+ this.all.update((list) => list.map((s) => (s.id === item.id ? { ...s, status: to, position: end } : s)));
1657
+ change = { status: to };
1658
+ }
1659
+ else {
1660
+ const shown = event.columns.find((c) => c.id === to)?.items ?? [];
1661
+ const above = shown[event.toIndex - 1];
1662
+ const below = shown[event.toIndex + 1];
1663
+ const column = this.all().filter((s) => s.status === to && s.id !== item.id).sort(byPosition);
1664
+ let index = column.length;
1665
+ if (above)
1666
+ index = column.findIndex((s) => s.id === above.id) + 1;
1667
+ else if (below)
1668
+ index = Math.max(0, column.findIndex((s) => s.id === below.id));
1669
+ const order = [...column.slice(0, index).map((s) => s.id), item.id, ...column.slice(index).map((s) => s.id)];
1670
+ const position = new Map(order.map((id, i) => [id, i]));
1671
+ this.all.update((list) => list.map((s) => (position.has(s.id)
1672
+ ? { ...s, position: position.get(s.id), ...(s.id === item.id ? { status: to } : {}) } : s)));
1673
+ change = { ...(changed ? { status: to } : {}), position: index };
1674
+ }
1675
+ try {
1676
+ this.replace(await this.service.move(this.clientId(), item.id, change));
1677
+ }
1678
+ catch {
1679
+ this.toast.error(this.text.moveError);
1680
+ await this.load();
1681
+ }
1682
+ }
1683
+ // --- criação ---
1684
+ /**
1685
+ * Abre o cadastro completo (a tarefa em branco no modal) já na coluna escolhida: "Nova tarefa" do topo (Em análise),
1686
+ * o "▾" ao lado dele, o "+" do cabeçalho e o "+ Criar tarefa" do rodapé de cada coluna.
1687
+ */
1688
+ newTask(status = 'em_analise') {
1689
+ this.selected.set(null);
1690
+ this.draftStatus.set(BOARD_STATUSES.includes(status) ? status : 'em_analise');
1691
+ this.drafting.set(true);
1692
+ }
1693
+ /** Nome do "+" da coluna: "Nova tarefa em Planejada". */
1694
+ newTaskInLabel(status) {
1695
+ return formatMessage(this.text.newTaskIn, { status: this.text.statuses[status] ?? status });
1696
+ }
1697
+ // --- tarefa aberta ---
1698
+ openDetail(s) {
1699
+ this.drafting.set(false);
1700
+ this.selected.set(s);
1701
+ }
1702
+ closeDetail() {
1703
+ this.selected.set(null);
1704
+ this.drafting.set(false);
1705
+ if (this.mode() === 'archive')
1706
+ void this.loadArchive(this.archivePage().page);
1707
+ }
1708
+ onCreated(task) {
1709
+ if (this.mode() === 'board')
1710
+ this.all.update((list) => [...list, task]);
1711
+ this.drafting.set(false);
1712
+ this.selected.set(task);
1713
+ }
1714
+ onRemoved(task) {
1715
+ this.all.update((list) => list.filter((s) => s.id !== task.id));
1716
+ this.archived.update((list) => list.filter((s) => s.id !== task.id));
1717
+ this.archiveTotal.update((n) => Math.max(0, n - (this.mode() === 'archive' ? 1 : 0)));
1718
+ this.selected.set(null);
1719
+ }
1720
+ onLabelCreated(label) {
1721
+ this.catalog.update((list) => [...list, label].sort((a, b) => a.name.localeCompare(b.name)));
1722
+ }
1723
+ /**
1724
+ * Troca a tarefa nas listas (o que o servidor devolveu), mantendo o autor que já veio com a foto. Recusada,
1725
+ * arquivada e agrupada saem do quadro na hora (o servidor também as tira do próximo `board=true`).
1726
+ */
1727
+ replace(saved) {
1728
+ const merge = (s) => ({ ...s, ...saved, author: saved.author ?? s.author });
1729
+ const leaves = saved.status === 'recusada' || !!saved.archived_at || !!saved.merged_into;
1730
+ this.all.update((list) => (leaves
1731
+ ? list.filter((s) => s.id !== saved.id)
1732
+ : list.map((s) => (s.id === saved.id ? merge(s) : s))));
1733
+ this.archived.update((list) => list.map((s) => (s.id === saved.id ? merge(s) : s)));
1734
+ }
1735
+ // --- menu do cartão ---
1736
+ buildMenu(t) {
1737
+ const linked = new Set(t.labels.map((l) => l.id));
1738
+ const labels = this.catalog().length
1739
+ ? this.catalog().map((l) => ({ id: `label:${l.id}`, label: l.name, icon: linked.has(l.id) ? 'check' : 'tag' }))
1740
+ : [{ id: 'label:none', label: this.text.menuNoLabels, disabled: true }];
1741
+ return [
1742
+ {
1743
+ id: 'status', label: this.text.menuStatus, icon: 'view-columns',
1744
+ children: SPINE_SUGGESTION_STATUSES.map((st) => ({
1745
+ id: `status:${st}`, label: this.text.statuses[st], icon: st === t.status ? 'check' : SPINE_SUGGESTION_STATUS_ICONS[st], disabled: st === t.status,
1746
+ })),
1747
+ },
1748
+ {
1749
+ id: 'priority', label: this.text.menuPriority, icon: TASK_PRIORITY_ICONS[t.priority],
1750
+ children: TASK_PRIORITIES.map((p) => ({ id: `priority:${p}`, label: this.text.priorities[p], icon: p === t.priority ? 'check' : TASK_PRIORITY_ICONS[p] })),
1751
+ },
1752
+ {
1753
+ id: 'waiting', label: this.text.menuWaiting, icon: 'chat-bubble',
1754
+ children: [
1755
+ ...SPINE_SUGGESTION_WAITING.map((w) => ({
1756
+ id: `waiting:${w}`, label: this.text.waiting[w], icon: t.waiting_on === w ? 'check' : 'chat-bubble', disabled: t.waiting_on === w,
1757
+ })),
1758
+ { id: 'waiting:none', label: this.text.waitingClear, icon: t.waiting_on ? 'check-circle' : 'check', disabled: !t.waiting_on },
1759
+ ],
1760
+ },
1761
+ { id: 'assign-me', label: this.text.menuAssignMe, icon: 'user-plus', disabled: assignedTo(t, this.me()) },
1762
+ { id: 'labels', label: this.text.menuLabel, icon: 'tag', children: labels },
1763
+ { id: 'copy-link', label: this.text.menuCopyLink, icon: 'link', separatorAbove: true },
1764
+ { id: 'copy-key', label: this.text.menuCopyKey, icon: 'copy' },
1765
+ // Sem cadeia: a que já recebeu outras não pode ser agrupada (o seletor explica; aqui fica desligado).
1766
+ { id: 'merge', label: this.text.menuMerge, icon: 'layers', separatorAbove: true, disabled: !!t.merged_count },
1767
+ { id: 'archive', label: this.text.menuArchive, icon: 'archive', separatorAbove: true },
1768
+ { id: 'delete', label: this.text.menuDelete, icon: 'trash', danger: true },
1769
+ ];
1770
+ }
1771
+ menuOf(t) {
1772
+ return this.menus().get(t.id) ?? [];
1773
+ }
1774
+ menuLabel(t) {
1775
+ return formatMessage(this.text.cardMenu, { key: t.key });
1776
+ }
1777
+ /** Uma escolha do "⋯": cada uma grava e devolve a tarefa nova (ou copia, ou abre a capa ancorada no botão). */
1778
+ async onMenu(t, id, anchor) {
1779
+ const clientId = this.clientId();
1780
+ const [action, value] = id.split(':');
1781
+ try {
1782
+ if (action === 'status')
1783
+ this.replace(await this.service.move(clientId, t.id, { status: value }));
1784
+ else if (action === 'priority')
1785
+ this.replace(await this.service.update(clientId, t.id, { priority: value }));
1786
+ else if (action === 'waiting') {
1787
+ this.replace(await this.service.update(clientId, t.id, { waiting_on: value === 'none' ? null : value }));
1788
+ this.toast.success(this.text.waitingSaved);
1789
+ }
1790
+ else if (action === 'assign-me') {
1791
+ this.replace(await this.service.addPerson(clientId, t.id, 'me', 'responsavel'));
1792
+ this.toast.success(this.text.assignedToMe);
1793
+ }
1794
+ else if (action === 'label') {
1795
+ this.replace(await this.service.setLabel(clientId, t.id, value, !t.labels.some((l) => l.id === value)));
1796
+ }
1797
+ else if (action === 'copy-link') {
1798
+ if (await copyText(this.linkOf(t)))
1799
+ this.toast.success(this.text.linkCopied);
1800
+ else
1801
+ this.toast.error(this.text.copyError);
1802
+ }
1803
+ else if (action === 'copy-key') {
1804
+ if (await copyText(t.key))
1805
+ this.toast.success(this.text.keyCopied);
1806
+ else
1807
+ this.toast.error(this.text.copyError);
1808
+ }
1809
+ else if (action === 'merge') {
1810
+ this.startMerge(t);
1811
+ }
1812
+ else if (action === 'archive') {
1813
+ this.replace(await this.service.setArchived(clientId, t.id, true));
1814
+ this.toast.success(this.text.archivedToast);
1815
+ }
1816
+ else if (action === 'delete')
1817
+ await this.confirmDelete(t);
1818
+ }
1819
+ catch {
1820
+ this.toast.error(this.text.saveError);
1821
+ }
1822
+ }
1823
+ startMerge(t) {
1824
+ this.selected.set(null);
1825
+ this.drafting.set(false);
1826
+ this.mergeSource.set(t);
1827
+ }
1828
+ /** Agrupou: a origem sai do quadro, o destino é trocado pela versão do servidor e abre (mostra "Agrupadas aqui"). */
1829
+ onMerged(event) {
1830
+ this.mergeSource.set(null);
1831
+ this.all.update((list) => list.filter((s) => s.id !== event.source.id));
1832
+ this.replace(event.target);
1833
+ this.selected.set(event.target);
1834
+ }
1835
+ /** O detalhe pediu outra tarefa (o destino de uma agrupada, uma das agrupadas): abre a completa. */
1836
+ async openTask(id) {
1837
+ try {
1838
+ const task = await this.service.task(this.clientId(), id);
1839
+ this.openDetail(task);
1840
+ }
1841
+ catch {
1842
+ this.toast.error(this.text.loadError);
1843
+ }
1844
+ }
1845
+ /** Desagrupou: a origem volta ao quadro (relê o que estiver à vista). */
1846
+ onUnmerged() {
1847
+ if (this.mode() === 'archive')
1848
+ void this.loadArchive(this.archivePage().page);
1849
+ else
1850
+ void this.load();
1851
+ }
1852
+ mergedIntoText(key) {
1853
+ return formatMessage(this.text.mergedInto, { key });
1854
+ }
1855
+ /** "Desagrupar" no arquivo. */
1856
+ async unmergeFromArchive(t) {
1857
+ this.reopening.set(t.id);
1858
+ try {
1859
+ await this.service.unmerge(this.clientId(), t.id);
1860
+ this.toast.success(formatMessage(this.text.unmergedToast, { key: t.key }));
1861
+ void this.loadArchive(this.archivePage().page);
1862
+ }
1863
+ catch {
1864
+ this.toast.error(this.text.saveError);
1865
+ }
1866
+ finally {
1867
+ this.reopening.set(null);
1868
+ }
1869
+ }
1870
+ async confirmDelete(t) {
1871
+ const ok = await this.dialogs.confirm({
1872
+ heading: formatMessage(this.text.deleteHeading, { key: t.key }), message: this.text.deleteMessage,
1873
+ confirmLabel: this.text.deleteConfirm, danger: true,
1874
+ });
1875
+ if (!ok)
1876
+ return;
1877
+ await this.service.remove(this.clientId(), t.id);
1878
+ this.onRemoved(t);
1879
+ this.toast.success(this.text.deleted);
1880
+ }
1881
+ /** O link da tela com a tarefa: o caminho atual (sem a query) + "?sugestao=SUG-12". */
1882
+ linkOf(t) {
1883
+ const path = (this.location?.path() ?? '').split('?')[0];
1884
+ const base = this.location?.prepareExternalUrl(path) ?? path;
1885
+ const origin = typeof window !== 'undefined' ? window.location.origin : '';
1886
+ return `${origin}${base}?${SUGGESTION_LINK_PARAM}=${encodeURIComponent(t.key)}`;
1887
+ }
1888
+ /** Item do checklist marcado no próprio cartão. */
1889
+ async toggleCardItem(t, item) {
1890
+ try {
1891
+ this.replace(await this.service.updateChecklistItem(this.clientId(), t.id, item.id, { done: !item.done }));
1892
+ }
1893
+ catch {
1894
+ this.toast.error(this.text.saveError);
1895
+ }
1896
+ }
1897
+ // --- arquivo ---
1898
+ async loadArchive(page = 1, clientId = this.clientId()) {
1899
+ const size = this.archivePage().size;
1900
+ this.archiveLoading.set(true);
1901
+ this.archiveError.set('');
1902
+ try {
1903
+ const res = await this.service.archive(clientId, {
1904
+ limit: size, offset: (page - 1) * size, search: this.archiveSearch().trim(),
1905
+ kind: this.archiveKind(), status: this.archiveStatus(),
1906
+ });
1907
+ this.archived.set(res.suggestions);
1908
+ this.archiveTotal.set(res.total);
1909
+ this.archivePage.set({ page, size });
1910
+ }
1911
+ catch {
1912
+ this.archiveError.set(this.text.loadError);
1913
+ }
1914
+ finally {
1915
+ this.archiveLoading.set(false);
1916
+ }
1917
+ }
1918
+ onArchiveSearch(term) {
1919
+ this.archiveSearch.set(term);
1920
+ void this.loadArchive(1);
1921
+ }
1922
+ onArchiveFilter(which, value) {
1923
+ (which === 'kind' ? this.archiveKind : this.archiveStatus).set(value);
1924
+ void this.loadArchive(1);
1925
+ }
1926
+ onArchivePage(state) {
1927
+ this.archivePage.set({ page: state.page, size: state.size });
1928
+ void this.loadArchive(state.page);
1929
+ }
1930
+ /** Recusada volta para Em análise (fim da coluna); arquivada volta ao quadro com o status que tinha. */
1931
+ async reopen(s) {
1932
+ if (this.reopening())
1933
+ return;
1934
+ this.reopening.set(s.id);
1935
+ try {
1936
+ if (s.archived_at)
1937
+ await this.service.setArchived(this.clientId(), s.id, false);
1938
+ if (s.status === 'recusada')
1939
+ await this.service.move(this.clientId(), s.id, { status: 'em_analise' });
1940
+ this.toast.success(s.status === 'recusada' ? this.text.reopened : this.text.restoredToast);
1941
+ const page = this.archivePage().page;
1942
+ // Última da página: volta uma, para não cair numa página vazia.
1943
+ await this.loadArchive(this.archived().length === 1 && page > 1 ? page - 1 : page);
1944
+ }
1945
+ catch {
1946
+ this.toast.error(this.text.reopenError);
1947
+ }
1948
+ finally {
1949
+ this.reopening.set(null);
1950
+ }
1951
+ }
1952
+ // --- textos do cartão ---
1953
+ sortLabel(k) {
1954
+ if (k === 'priority')
1955
+ return this.text.priority;
1956
+ if (k === 'due')
1957
+ return this.text.dueDate;
1958
+ return this.text.sorts[k];
1959
+ }
1960
+ kindLabel(s) {
1961
+ return this.text.kinds[s.kind];
1962
+ }
1963
+ kindIcon(s) {
1964
+ return SPINE_SUGGESTION_KIND_ICONS[s.kind];
1965
+ }
1966
+ priorityIcon(s) {
1967
+ return TASK_PRIORITY_ICONS[s.priority];
1968
+ }
1969
+ priorityName(s) {
1970
+ return this.text.priorities[s.priority];
1971
+ }
1972
+ isTop(s) {
1973
+ return this.topIds().has(s.id);
1974
+ }
1975
+ excerpt(s) {
1976
+ const d = s.description.replace(/\s+/g, ' ').trim();
1977
+ return d.length > EXCERPT ? `${d.slice(0, EXCERPT - 1).trimEnd()}…` : d;
1978
+ }
1979
+ faces(s) {
1980
+ return s.assignees.slice(0, FACES);
1981
+ }
1982
+ moreFaces(s) {
1983
+ const rest = s.assignees.length - FACES;
1984
+ return rest > 0 ? formatMessage(this.text.moreAssignees, { count: rest }) : '';
1985
+ }
1986
+ dueOf(s) {
1987
+ return dueState(s, this.today);
1988
+ }
1989
+ /** "Vencida há 3 dias", "Vence hoje", "Vence amanhã", "No prazo · até 12 de nov." (concluída: "Até 12 de nov."). */
1990
+ dueText(s) {
1991
+ if (!s.due_date)
1992
+ return '';
1993
+ const state = this.dueOf(s);
1994
+ if (state === 'overdue') {
1995
+ return pluralMessage(this.text.dueLateDays, daysBetween(s.due_date, this.today), this.messages.meta.locale);
1996
+ }
1997
+ if (state === 'today')
1998
+ return this.text.dueToday;
1999
+ if (state === 'tomorrow')
2000
+ return this.text.dueTomorrow;
2001
+ if (state === 'later')
2002
+ return formatMessage(this.text.dueWithin, { date: this.day(s.due_date) });
2003
+ return formatMessage(this.text.dueOn, { date: this.day(s.due_date) });
2004
+ }
2005
+ dueIcon(s) {
2006
+ const state = this.dueOf(s);
2007
+ if (state === 'overdue')
2008
+ return 'alarm-clock';
2009
+ if (state === 'today' || state === 'tomorrow')
2010
+ return 'hourglass';
2011
+ return 'calendar';
2012
+ }
2013
+ /** O nome da tela no cartão: o último nível do caminho, o nome da nova ou "Sem tela". */
2014
+ screenNameOf(s) {
2015
+ if (s.screen.new)
2016
+ return s.screen.label || this.text.newScreenTag;
2017
+ return screenName(s.screen.label) || s.screen.path || this.text.noScreen;
2018
+ }
2019
+ /** Selo "Aguardando…" do cartão (o `let-item` do kanban chega sem tipo). */
2020
+ waitingName(s) {
2021
+ return s.waiting_on ? this.text.waiting[s.waiting_on] : '';
2022
+ }
2023
+ waitingColor(s) {
2024
+ return s.waiting_on ? SPINE_SUGGESTION_WAITING_COLORS[s.waiting_on] : 'neutral';
2025
+ }
2026
+ unreadText(count) {
2027
+ return pluralMessage(this.text.unread, count, this.messages.meta.locale);
2028
+ }
2029
+ createdText(s) {
2030
+ return formatMessage(this.text.createdOn, { date: this.date(s.created_at, true) });
2031
+ }
2032
+ /** Explicação da coluna vazia: com filtro, "nenhuma com estes filtros"; sem, o que costuma cair nela. */
2033
+ emptyHint(status) {
2034
+ if (this.isFiltered())
2035
+ return this.text.columnEmptyFiltered;
2036
+ const hint = this.text.columnEmptyHints[status] ?? '';
2037
+ return formatMessage(hint, { days: this.doneDays() });
2038
+ }
2039
+ emptyIcon(status) {
2040
+ return this.isFiltered() ? 'filter' : (SPINE_SUGGESTION_STATUS_ICONS[status] ?? 'inbox');
2041
+ }
2042
+ /** "3d 4h", "4h 12min", "12min" (o catálogo dá o formato). */
2043
+ duration(ms) {
2044
+ const { d, h, m } = splitDuration(ms);
2045
+ if (d)
2046
+ return formatMessage(this.text.duration.days, { d, h });
2047
+ if (h)
2048
+ return formatMessage(this.text.duration.hours, { h, m });
2049
+ return formatMessage(this.text.duration.minutes, { m });
2050
+ }
2051
+ checklistText(s) {
2052
+ return formatMessage(this.text.checklistSpoken, { done: s.checklist.done, total: s.checklist.total });
2053
+ }
2054
+ checklistOpen(s) {
2055
+ return this.openChecklists().has(s.id);
2056
+ }
2057
+ toggleChecklist(s) {
2058
+ this.openChecklists.update((set) => {
2059
+ const next = new Set(set);
2060
+ if (!next.delete(s.id))
2061
+ next.add(s.id);
2062
+ return next;
2063
+ });
2064
+ }
2065
+ /** Os rótulos na linha de selos do cartão, cada um com a cor do token ('cat-3' → 3). */
2066
+ labelTags(s) {
2067
+ return s.labels.map((l) => ({ id: l.id, name: l.name, color: Number(l.color.replace('cat-', '')) || undefined }));
2068
+ }
2069
+ /** "3 atividades (1/3)". */
2070
+ cardChecklistText(s) {
2071
+ return pluralMessage(this.text.cardChecklist, s.checklist.total, this.messages.meta.locale, { done: s.checklist.done });
2072
+ }
2073
+ /** "Pedida por Ana" (+N relatores das agrupadas): a dica da foto de quem pediu. */
2074
+ requestersText(s) {
2075
+ const who = formatMessage(this.text.requestedByName, { name: this.personName(s.author) });
2076
+ return s.reporters_count ? `${who} ${this.moreText(s.reporters_count)}` : who;
2077
+ }
2078
+ /** "+N". */
2079
+ moreText(count) {
2080
+ return formatMessage(this.text.moreAssignees, { count });
2081
+ }
2082
+ cardItems(s) {
2083
+ return s.checklist_on_card ? [...(s.checklist.items ?? [])].sort((a, b) => a.position - b.position) : [];
2084
+ }
2085
+ personName(p) {
2086
+ return p?.name || p?.username || this.text.removedAuthor;
2087
+ }
2088
+ votesText(count) {
2089
+ return pluralMessage(this.text.votes, count, this.messages.meta.locale);
2090
+ }
2091
+ commentsText(count) {
2092
+ return pluralMessage(this.text.comments, count, this.messages.meta.locale);
2093
+ }
2094
+ closedOn(s) {
2095
+ if (s.archived_at)
2096
+ return `${this.text.archivedTag} · ${this.date(s.archived_at)}`;
2097
+ const template = s.status === 'recusada' ? this.text.closedOn.recusada : this.text.closedOn.concluida;
2098
+ return formatMessage(template, { date: this.date(s.status_at ?? s.updated_at) });
2099
+ }
2100
+ /** "há 3 dias", "ontem", "há 2 horas" (Intl.RelativeTimeFormat no idioma do catálogo). */
2101
+ age(iso) {
2102
+ const seconds = Math.round((Date.now() - new Date(iso).getTime()) / 1000);
2103
+ try {
2104
+ const rtf = new Intl.RelativeTimeFormat(this.messages.meta.locale, { numeric: 'auto' });
2105
+ for (const [unit, size] of AGES)
2106
+ if (Math.abs(seconds) >= size)
2107
+ return rtf.format(-Math.floor(seconds / size), unit);
2108
+ return rtf.format(0, 'minute');
2109
+ }
2110
+ catch {
2111
+ return this.date(iso);
2112
+ }
2113
+ }
2114
+ date(iso, withTime = false) {
2115
+ try {
2116
+ return new Intl.DateTimeFormat(this.messages.meta.locale, {
2117
+ dateStyle: 'short', ...(withTime ? { timeStyle: 'short' } : {}), timeZone: this.preferences.effectiveTimeZone(),
2118
+ }).format(new Date(iso));
2119
+ }
2120
+ catch {
2121
+ return iso.slice(0, 10);
2122
+ }
2123
+ }
2124
+ /** Data sem hora (AAAA-MM-DD): o dia é o dia, sem fuso. */
2125
+ day(iso) {
2126
+ try {
2127
+ return new Intl.DateTimeFormat(this.messages.meta.locale, { day: '2-digit', month: 'short', timeZone: 'UTC' }).format(new Date(`${iso}T00:00:00Z`));
2128
+ }
2129
+ catch {
2130
+ return iso;
2131
+ }
2132
+ }
2133
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceSuggestionsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2134
+ 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 } }, 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 <!-- A vez \u00E9 da equipe: quem pediu escreveu na conversa. A nota conta as mensagens ainda n\u00E3o lidas. -->\n <button type=\"button\" class=\"as-kpi\" data-tone=\"accent\" [attr.aria-pressed]=\"waiting() === 'responsaveis'\"\n (click)=\"toggleKpi('waiting', waiting() !== 'responsaveis')\">\n <span class=\"as-kpi__icon\" aria-hidden=\"true\"><hm-icon shape=\"chat-bubble\" /></span>\n <span class=\"as-kpi__body\">\n <span class=\"as-kpi__label\">{{ text.waiting.responsaveis }}</span>\n <span class=\"as-kpi__value\">{{ kpiLoading() ? '\u2014' : stats().waitingTeam }}</span>\n <span class=\"as-kpi__note\" [class.as-kpi__note--up]=\"stats().unread > 0\">{{ unreadText(stats().unread) }}</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 <!-- A tela a mudar: \"M\u00F3dulo \u203A Tela\" (as de um m\u00F3dulo ficam juntas) e as novas, que ainda n\u00E3o existem. -->\n <hm-select class=\"as-filters__select as-filters__select--wide\" [label]=\"text.screenFilter\" labelHidden [(value)]=\"screen\">\n <option value=\"\">{{ text.allScreens }}</option>\n @if (hasNewScreens()) {\n <option [value]=\"newScreenValue\">{{ text.newScreenTag }}</option>\n }\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.waitingFilter\" labelHidden [(value)]=\"waiting\">\n <option value=\"\">{{ text.waitingFilter }}: {{ text.waitingAny }}</option>\n @for (w of waitingValues; track w) {\n <option [value]=\"w\">{{ text.waiting[w] }}</option>\n }\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.groupFilter\" labelHidden [(value)]=\"group\">\n <option value=\"\">{{ text.allGroups }}</option>\n @for (g of groupOptions(); track g.id) {\n <option [value]=\"g.id\">{{ g.name }}</option>\n }\n <option value=\"none\">{{ text.noGroups }}</option>\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 <div class=\"as-card__body\">\n <!-- Uma linha s\u00F3: chave, tipo, prioridade (e o selo de mais votada) e o \"\u22EF\". -->\n <div class=\"as-card__top\">\n <span class=\"as-card__key\">{{ item.key }}</span>\n <span class=\"as-card__kind\" [attr.data-kind]=\"item.kind\"><hm-icon [shape]=\"kindIcon(item)\" />{{ kindLabel(item) }}</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\" [attr.title]=\"text.topVoted\"><hm-icon shape=\"flame\" /><span class=\"as-sr-only\">{{ text.topVoted }}</span></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\n <!-- Tudo o que identifica e situa a tarefa, lado a lado (quebra quando falta largura): a tela (o caminho\n inteiro na dica), \"Nova tela\", r\u00F3tulos, com quem est\u00E1 a vez, mensagens novas e o prazo. -->\n <div class=\"as-card__chips\">\n <span class=\"as-card__screen\" [class.as-card__screen--new]=\"item.screen.new\" [attr.title]=\"item.screen.label || null\">\n <hm-icon [shape]=\"item.screen.new ? 'plus' : 'application'\" />{{ screenNameOf(item) }}\n </span>\n @if (item.screen.new) { <span class=\"as-card__screen-tag\">{{ text.newScreenTag }}</span> }\n <!-- Os r\u00F3tulos ocupam o resto da linha da tela; o que n\u00E3o cabe vira \"+N\" (n\u00E3o quebra a linha). -->\n @if (item.labels.length) {\n <spine-tag-row class=\"as-card__labels\" [label]=\"text.labels\" [items]=\"labelTags(item)\" />\n }\n @if (item.waiting_on && item.status !== 'concluida' && item.status !== 'recusada') {\n <hm-tag [name]=\"waitingName(item)\" [color]=\"waitingColor(item)\" />\n }\n @if (item.unread) { <span class=\"as-card__unread\">{{ unreadText(item.unread) }}</span> }\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>\n\n <!-- Com o checklist no cart\u00E3o, a descri\u00E7\u00E3o sai (o checklist j\u00E1 diz o que falta). -->\n @if (!cardItems(item).length && excerpt(item); as ex) {\n <p class=\"as-card__excerpt\">{{ ex }}</p>\n }\n @if (cardItems(item).length && checklistOpen(item)) {\n <ul class=\"as-card__checklist\" [id]=\"'as-card-checklist-' + item.id\">\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\n <!-- Rodap\u00E9: o checklist (abre e fecha os itens) \u00E0 esquerda; contadores, data, quem pediu e os respons\u00E1veis \u00E0 direita. -->\n <div class=\"as-card__foot\">\n @if (cardItems(item).length) {\n <button type=\"button\" class=\"as-card__checklist-toggle\" [class.as-card__count--full]=\"item.checklist.done === item.checklist.total\"\n [attr.aria-expanded]=\"checklistOpen(item)\" [attr.aria-controls]=\"checklistOpen(item) ? 'as-card-checklist-' + item.id : null\"\n (click)=\"toggleChecklist(item)\">\n <hm-icon shape=\"list-check\" />{{ cardChecklistText(item) }}<hm-icon [shape]=\"checklistOpen(item) ? 'angle-up' : 'angle-down'\" />\n </button>\n } @else 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__spacer\"></span>\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 <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 <!-- Quem pediu (a foto do autor e \"+N\" relatores das agrupadas) e, depois do fio, os respons\u00E1veis. -->\n @if (item.author) {\n <span class=\"as-card__requesters\" [attr.title]=\"requestersText(item)\">\n <hm-account-avatar size=\"small\" [name]=\"personName(item.author)\" [imageUrl]=\"item.author.picture\" />\n @if (item.reporters_count) { <span class=\"as-card__more\">{{ moreText(item.reporters_count) }}</span> }\n <span class=\"as-sr-only\">{{ requestersText(item) }}</span>\n </span>\n }\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\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-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(10.5rem,1fr));gap:var(--hm-space-xs)}.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:center;gap:var(--hm-space-xs);box-sizing:border-box;min-inline-size:0;margin:0;padding:var(--hm-space-xs) 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--wide .as-kpi__label{grid-column:1}.as-kpi--wide .as-kpi__bar{grid-row:1;grid-column:2;align-self:center}.as-kpi__icon{display:inline-flex;flex:none;align-items:center;justify-content:center;inline-size:var(--hm-control-height);block-size:var(--hm-control-height);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) * .55)}.as-kpi__body{display:grid;flex:1;grid-template-columns:auto minmax(0,1fr);align-items:baseline;column-gap:var(--hm-space-xs);min-inline-size:0}.as-kpi__label{grid-row:1;grid-column:2;overflow:hidden;color:var(--hm-muted);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-medium);text-overflow:ellipsis;white-space:nowrap}.as-kpi__note,.as-kpi__parts,.as-kpi__legend{grid-column:1/-1;min-inline-size:0}.as-kpi__value{grid-row:1;grid-column:1;color:var(--hm-text);font-size:var(--hm-font-size-lg);font-weight:var(--hm-font-weight-bold);font-variant-numeric:tabular-nums;line-height:var(--hm-line-height-tight)}.as-kpi__note{overflow:hidden;color:var(--hm-muted);font-size:var(--hm-font-size-xs);text-overflow:ellipsis;white-space:nowrap}.as-kpi__note hm-icon{margin-inline-end:var(--hm-space-micro);vertical-align:middle}.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);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__select--wide{max-inline-size:18rem}.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__body{display:flex;flex-direction:column;gap:var(--hm-space-xxxs);padding:var(--hm-space-xs) var(--hm-space-sm);min-inline-size:0}.as-card__top{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-micro) var(--hm-space-xxxs);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__kind,.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__kind hm-icon,.as-card__priority hm-icon{--hm-icon-size: .875rem;color:var(--hm-muted)}.as-card__kind{color:var(--hm-action)}.as-card__kind[data-kind=melhoria] hm-icon{color:var(--hm-accent)}.as-card__kind[data-kind=nova_funcionalidade] hm-icon{color:var(--hm-info)}.as-card__kind[data-kind=problema] hm-icon{color:var(--hm-danger)}.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;padding:0 var(--hm-space-micro);border-radius:var(--hm-radius-pill);background:var(--hm-warning-bg);color:var(--hm-warning);line-height:1.6}.as-card__badge hm-icon{--hm-icon-size: .875rem}.as-card__title{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__chips{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-micro);min-inline-size:0}.as-card__labels{flex:1 1 5rem;min-inline-size:5rem}.as-card__screen{display:inline-flex;align-items:center;gap:var(--hm-space-micro);max-inline-size:100%;padding:0 var(--hm-space-xxxs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-micro);background:var(--hm-surface-muted);color:var(--hm-text);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);line-height:1.6;overflow-wrap:anywhere}.as-card__screen>hm-icon{--hm-icon-size: .875rem;flex:none;color:var(--hm-muted)}.as-card__screen--new{border-color:transparent;background:var(--hm-info-bg)}.as-card__screen--new>hm-icon{color:var(--hm-info)}.as-card__screen-tag{flex:none;padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:var(--hm-info);color:var(--hm-on-status);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);line-height:1.6;white-space:nowrap}.as-card__unread{color:var(--hm-accent);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold)}.as-card__excerpt{margin:0;overflow:hidden;color:var(--hm-muted);font-size:var(--hm-font-size-sm);white-space:nowrap;text-overflow:ellipsis}.as-card__checklist{display:flex;flex-direction:column;gap:1px;margin: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__checklist-toggle{display:inline-flex;align-items:center;gap:var(--hm-space-micro);padding:0 var(--hm-space-micro);border:0;border-radius:var(--hm-radius-control);background:none;color:var(--hm-muted);font:inherit;font-size:var(--hm-font-size-xs);font-variant-numeric:tabular-nums;white-space:nowrap;cursor:pointer}.as-card__checklist-toggle:hover{background:var(--hm-hover);color:var(--hm-text)}.as-card__checklist-toggle:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color)}.as-card__due{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-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-micro) var(--hm-space-xs);margin-block-start:var(--hm-space-micro);padding-block-start:var(--hm-space-xxxs);border-block-start:var(--hm-border-hairline) solid var(--hm-border)}.as-card__spacer{flex:1 1 auto}.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__requesters{display:inline-flex;align-items:center;padding-inline-end:var(--hm-space-xxxs);border-inline-end:var(--hm-border-hairline) solid var(--hm-border)}.as-card__faces{display:inline-flex;align-items:center}.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-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: 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: SpineTagRowComponent, selector: "spine-tag-row", inputs: ["items", "label", "empty"] }, { 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 }); }
2135
+ }
2136
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceSuggestionsComponent, decorators: [{
2137
+ type: Component,
2138
+ args: [{ selector: 'spine-auto-service-suggestions', imports: [
2139
+ AccountAvatarComponent, AlertComponent, ButtonComponent, EmptyComponent, IconButtonComponent, IconComponent,
2140
+ KanbanCardDirective, KanbanColumnActionsDirective, KanbanColumnEmptyDirective, KanbanColumnFooterDirective,
2141
+ KanbanColumnSummaryDirective, KanbanComponent, MenuButtonComponent, PaginatorComponent,
2142
+ SearchFieldComponent, SegmentedControlComponent, SelectComponent, SpineTagRowComponent, SuggestionMergePickerComponent,
2143
+ SuggestionTaskDetailComponent, TagComponent,
2144
+ ], 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 <!-- A vez \u00E9 da equipe: quem pediu escreveu na conversa. A nota conta as mensagens ainda n\u00E3o lidas. -->\n <button type=\"button\" class=\"as-kpi\" data-tone=\"accent\" [attr.aria-pressed]=\"waiting() === 'responsaveis'\"\n (click)=\"toggleKpi('waiting', waiting() !== 'responsaveis')\">\n <span class=\"as-kpi__icon\" aria-hidden=\"true\"><hm-icon shape=\"chat-bubble\" /></span>\n <span class=\"as-kpi__body\">\n <span class=\"as-kpi__label\">{{ text.waiting.responsaveis }}</span>\n <span class=\"as-kpi__value\">{{ kpiLoading() ? '\u2014' : stats().waitingTeam }}</span>\n <span class=\"as-kpi__note\" [class.as-kpi__note--up]=\"stats().unread > 0\">{{ unreadText(stats().unread) }}</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 <!-- A tela a mudar: \"M\u00F3dulo \u203A Tela\" (as de um m\u00F3dulo ficam juntas) e as novas, que ainda n\u00E3o existem. -->\n <hm-select class=\"as-filters__select as-filters__select--wide\" [label]=\"text.screenFilter\" labelHidden [(value)]=\"screen\">\n <option value=\"\">{{ text.allScreens }}</option>\n @if (hasNewScreens()) {\n <option [value]=\"newScreenValue\">{{ text.newScreenTag }}</option>\n }\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.waitingFilter\" labelHidden [(value)]=\"waiting\">\n <option value=\"\">{{ text.waitingFilter }}: {{ text.waitingAny }}</option>\n @for (w of waitingValues; track w) {\n <option [value]=\"w\">{{ text.waiting[w] }}</option>\n }\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.groupFilter\" labelHidden [(value)]=\"group\">\n <option value=\"\">{{ text.allGroups }}</option>\n @for (g of groupOptions(); track g.id) {\n <option [value]=\"g.id\">{{ g.name }}</option>\n }\n <option value=\"none\">{{ text.noGroups }}</option>\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 <div class=\"as-card__body\">\n <!-- Uma linha s\u00F3: chave, tipo, prioridade (e o selo de mais votada) e o \"\u22EF\". -->\n <div class=\"as-card__top\">\n <span class=\"as-card__key\">{{ item.key }}</span>\n <span class=\"as-card__kind\" [attr.data-kind]=\"item.kind\"><hm-icon [shape]=\"kindIcon(item)\" />{{ kindLabel(item) }}</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\" [attr.title]=\"text.topVoted\"><hm-icon shape=\"flame\" /><span class=\"as-sr-only\">{{ text.topVoted }}</span></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\n <!-- Tudo o que identifica e situa a tarefa, lado a lado (quebra quando falta largura): a tela (o caminho\n inteiro na dica), \"Nova tela\", r\u00F3tulos, com quem est\u00E1 a vez, mensagens novas e o prazo. -->\n <div class=\"as-card__chips\">\n <span class=\"as-card__screen\" [class.as-card__screen--new]=\"item.screen.new\" [attr.title]=\"item.screen.label || null\">\n <hm-icon [shape]=\"item.screen.new ? 'plus' : 'application'\" />{{ screenNameOf(item) }}\n </span>\n @if (item.screen.new) { <span class=\"as-card__screen-tag\">{{ text.newScreenTag }}</span> }\n <!-- Os r\u00F3tulos ocupam o resto da linha da tela; o que n\u00E3o cabe vira \"+N\" (n\u00E3o quebra a linha). -->\n @if (item.labels.length) {\n <spine-tag-row class=\"as-card__labels\" [label]=\"text.labels\" [items]=\"labelTags(item)\" />\n }\n @if (item.waiting_on && item.status !== 'concluida' && item.status !== 'recusada') {\n <hm-tag [name]=\"waitingName(item)\" [color]=\"waitingColor(item)\" />\n }\n @if (item.unread) { <span class=\"as-card__unread\">{{ unreadText(item.unread) }}</span> }\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>\n\n <!-- Com o checklist no cart\u00E3o, a descri\u00E7\u00E3o sai (o checklist j\u00E1 diz o que falta). -->\n @if (!cardItems(item).length && excerpt(item); as ex) {\n <p class=\"as-card__excerpt\">{{ ex }}</p>\n }\n @if (cardItems(item).length && checklistOpen(item)) {\n <ul class=\"as-card__checklist\" [id]=\"'as-card-checklist-' + item.id\">\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\n <!-- Rodap\u00E9: o checklist (abre e fecha os itens) \u00E0 esquerda; contadores, data, quem pediu e os respons\u00E1veis \u00E0 direita. -->\n <div class=\"as-card__foot\">\n @if (cardItems(item).length) {\n <button type=\"button\" class=\"as-card__checklist-toggle\" [class.as-card__count--full]=\"item.checklist.done === item.checklist.total\"\n [attr.aria-expanded]=\"checklistOpen(item)\" [attr.aria-controls]=\"checklistOpen(item) ? 'as-card-checklist-' + item.id : null\"\n (click)=\"toggleChecklist(item)\">\n <hm-icon shape=\"list-check\" />{{ cardChecklistText(item) }}<hm-icon [shape]=\"checklistOpen(item) ? 'angle-up' : 'angle-down'\" />\n </button>\n } @else 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__spacer\"></span>\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 <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 <!-- Quem pediu (a foto do autor e \"+N\" relatores das agrupadas) e, depois do fio, os respons\u00E1veis. -->\n @if (item.author) {\n <span class=\"as-card__requesters\" [attr.title]=\"requestersText(item)\">\n <hm-account-avatar size=\"small\" [name]=\"personName(item.author)\" [imageUrl]=\"item.author.picture\" />\n @if (item.reporters_count) { <span class=\"as-card__more\">{{ moreText(item.reporters_count) }}</span> }\n <span class=\"as-sr-only\">{{ requestersText(item) }}</span>\n </span>\n }\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\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-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(10.5rem,1fr));gap:var(--hm-space-xs)}.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:center;gap:var(--hm-space-xs);box-sizing:border-box;min-inline-size:0;margin:0;padding:var(--hm-space-xs) 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--wide .as-kpi__label{grid-column:1}.as-kpi--wide .as-kpi__bar{grid-row:1;grid-column:2;align-self:center}.as-kpi__icon{display:inline-flex;flex:none;align-items:center;justify-content:center;inline-size:var(--hm-control-height);block-size:var(--hm-control-height);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) * .55)}.as-kpi__body{display:grid;flex:1;grid-template-columns:auto minmax(0,1fr);align-items:baseline;column-gap:var(--hm-space-xs);min-inline-size:0}.as-kpi__label{grid-row:1;grid-column:2;overflow:hidden;color:var(--hm-muted);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-medium);text-overflow:ellipsis;white-space:nowrap}.as-kpi__note,.as-kpi__parts,.as-kpi__legend{grid-column:1/-1;min-inline-size:0}.as-kpi__value{grid-row:1;grid-column:1;color:var(--hm-text);font-size:var(--hm-font-size-lg);font-weight:var(--hm-font-weight-bold);font-variant-numeric:tabular-nums;line-height:var(--hm-line-height-tight)}.as-kpi__note{overflow:hidden;color:var(--hm-muted);font-size:var(--hm-font-size-xs);text-overflow:ellipsis;white-space:nowrap}.as-kpi__note hm-icon{margin-inline-end:var(--hm-space-micro);vertical-align:middle}.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);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__select--wide{max-inline-size:18rem}.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__body{display:flex;flex-direction:column;gap:var(--hm-space-xxxs);padding:var(--hm-space-xs) var(--hm-space-sm);min-inline-size:0}.as-card__top{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-micro) var(--hm-space-xxxs);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__kind,.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__kind hm-icon,.as-card__priority hm-icon{--hm-icon-size: .875rem;color:var(--hm-muted)}.as-card__kind{color:var(--hm-action)}.as-card__kind[data-kind=melhoria] hm-icon{color:var(--hm-accent)}.as-card__kind[data-kind=nova_funcionalidade] hm-icon{color:var(--hm-info)}.as-card__kind[data-kind=problema] hm-icon{color:var(--hm-danger)}.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;padding:0 var(--hm-space-micro);border-radius:var(--hm-radius-pill);background:var(--hm-warning-bg);color:var(--hm-warning);line-height:1.6}.as-card__badge hm-icon{--hm-icon-size: .875rem}.as-card__title{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__chips{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-micro);min-inline-size:0}.as-card__labels{flex:1 1 5rem;min-inline-size:5rem}.as-card__screen{display:inline-flex;align-items:center;gap:var(--hm-space-micro);max-inline-size:100%;padding:0 var(--hm-space-xxxs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-micro);background:var(--hm-surface-muted);color:var(--hm-text);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);line-height:1.6;overflow-wrap:anywhere}.as-card__screen>hm-icon{--hm-icon-size: .875rem;flex:none;color:var(--hm-muted)}.as-card__screen--new{border-color:transparent;background:var(--hm-info-bg)}.as-card__screen--new>hm-icon{color:var(--hm-info)}.as-card__screen-tag{flex:none;padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:var(--hm-info);color:var(--hm-on-status);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);line-height:1.6;white-space:nowrap}.as-card__unread{color:var(--hm-accent);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold)}.as-card__excerpt{margin:0;overflow:hidden;color:var(--hm-muted);font-size:var(--hm-font-size-sm);white-space:nowrap;text-overflow:ellipsis}.as-card__checklist{display:flex;flex-direction:column;gap:1px;margin: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__checklist-toggle{display:inline-flex;align-items:center;gap:var(--hm-space-micro);padding:0 var(--hm-space-micro);border:0;border-radius:var(--hm-radius-control);background:none;color:var(--hm-muted);font:inherit;font-size:var(--hm-font-size-xs);font-variant-numeric:tabular-nums;white-space:nowrap;cursor:pointer}.as-card__checklist-toggle:hover{background:var(--hm-hover);color:var(--hm-text)}.as-card__checklist-toggle:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color)}.as-card__due{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-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-micro) var(--hm-space-xs);margin-block-start:var(--hm-space-micro);padding-block-start:var(--hm-space-xxxs);border-block-start:var(--hm-border-hairline) solid var(--hm-border)}.as-card__spacer{flex:1 1 auto}.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__requesters{display:inline-flex;align-items:center;padding-inline-end:var(--hm-space-xxxs);border-inline-end:var(--hm-border-hairline) solid var(--hm-border)}.as-card__faces{display:inline-flex;align-items:center}.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-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"] }]
2145
+ }], ctorParameters: () => [], propDecorators: { clientId: [{ type: i0.Input, args: [{ isSignal: true, alias: "clientId", required: true }] }] } });
2146
+
2147
+ /**
2148
+ * Tela "Sugestões" do auto-serviço (router `AUTO_SERVICE_SUGGESTIONS_TEMPLATE_ROUTER_ID`, na raiz do menu; migration
2149
+ * 0036 do argos): o seletor de client + o quadro de sugestões que era a aba Cliente › Sugestões. Carregada só por
2150
+ * `AUTO_SERVICE_SCREENS.suggestions` -- nunca exportada pelo ponto de entrada (ver `public-api.ts`).
2151
+ */
2152
+ class AutoServiceSuggestionsPageComponent {
2153
+ constructor() {
2154
+ this.client = signal(null, ...(ngDevMode ? [{ debugName: "client" }] : /* istanbul ignore next */ []));
2155
+ }
2156
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceSuggestionsPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2157
+ 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: `
2158
+ <div class="as-stack">
2159
+ <spine-auto-service-client-picker (clientChange)="client.set($event)" />
2160
+ @if (client(); as c) {
2161
+ <spine-auto-service-suggestions [clientId]="c.id" />
2162
+ }
2163
+ </div>
2164
+ `, 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 }); }
2165
+ }
2166
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceSuggestionsPageComponent, decorators: [{
2167
+ type: Component,
2168
+ args: [{ selector: 'spine-auto-service-suggestions-page', imports: [AutoServiceClientPickerComponent, AutoServiceSuggestionsComponent], template: `
2169
+ <div class="as-stack">
2170
+ <spine-auto-service-client-picker (clientChange)="client.set($event)" />
2171
+ @if (client(); as c) {
2172
+ <spine-auto-service-suggestions [clientId]="c.id" />
2173
+ }
2174
+ </div>
2175
+ `, 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"] }]
2176
+ }] });
2177
+
2178
+ export { AutoServiceSuggestionsPageComponent };
2179
+ //# sourceMappingURL=trismegisto-spine-auto-service-auto-service-suggestions-page.component-mMjE3JOL.mjs.map