@trismegisto/spine 3.3.3 → 3.3.5

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 (47) hide show
  1. package/README.md +15 -2
  2. package/fesm2022/trismegisto-spine-audit.mjs +6 -6
  3. package/fesm2022/trismegisto-spine-audit.mjs.map +1 -1
  4. package/fesm2022/{trismegisto-spine-auto-service-auto-service-client.component-BAfeLyTB.mjs → trismegisto-spine-auto-service-auto-service-client.component-kAXaCM5W.mjs} +8 -8
  5. package/fesm2022/{trismegisto-spine-auto-service-auto-service-client.component-BAfeLyTB.mjs.map → trismegisto-spine-auto-service-auto-service-client.component-kAXaCM5W.mjs.map} +1 -1
  6. package/fesm2022/{trismegisto-spine-auto-service-auto-service-groups.component-BYjIQ9ID.mjs → trismegisto-spine-auto-service-auto-service-groups.component-DE2QIGRH.mjs} +251 -85
  7. package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-DE2QIGRH.mjs.map +1 -0
  8. package/fesm2022/{trismegisto-spine-auto-service-auto-service-news-page.component-CBemH4hr.mjs → trismegisto-spine-auto-service-auto-service-news-page.component-BW7BJ0oD.mjs} +2 -2
  9. package/fesm2022/{trismegisto-spine-auto-service-auto-service-news-page.component-CBemH4hr.mjs.map → trismegisto-spine-auto-service-auto-service-news-page.component-BW7BJ0oD.mjs.map} +1 -1
  10. package/fesm2022/{trismegisto-spine-auto-service-auto-service-ratings-page.component-C_THGTkw.mjs → trismegisto-spine-auto-service-auto-service-ratings-page.component-CHPU-QLJ.mjs} +2 -2
  11. package/fesm2022/{trismegisto-spine-auto-service-auto-service-ratings-page.component-C_THGTkw.mjs.map → trismegisto-spine-auto-service-auto-service-ratings-page.component-CHPU-QLJ.mjs.map} +1 -1
  12. package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-CkiCTmXD.mjs +3733 -0
  13. package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-CkiCTmXD.mjs.map +1 -0
  14. package/fesm2022/{trismegisto-spine-auto-service-auto-service.component-QsIQ61w2.mjs → trismegisto-spine-auto-service-auto-service.component-D58Ht9PH.mjs} +7 -7
  15. package/fesm2022/{trismegisto-spine-auto-service-auto-service.component-QsIQ61w2.mjs.map → trismegisto-spine-auto-service-auto-service.component-D58Ht9PH.mjs.map} +1 -1
  16. package/fesm2022/{trismegisto-spine-auto-service-groups.technical-_jQrg1Qh.mjs → trismegisto-spine-auto-service-groups.technical-CB1D3ANC.mjs} +12 -3
  17. package/fesm2022/trismegisto-spine-auto-service-groups.technical-CB1D3ANC.mjs.map +1 -0
  18. package/fesm2022/{trismegisto-spine-auto-service-groups.usage-OvWjNeeT.mjs → trismegisto-spine-auto-service-groups.usage-De2MhlE7.mjs} +7 -2
  19. package/fesm2022/{trismegisto-spine-auto-service-groups.usage-OvWjNeeT.mjs.map → trismegisto-spine-auto-service-groups.usage-De2MhlE7.mjs.map} +1 -1
  20. package/fesm2022/{trismegisto-spine-auto-service-suggestions.technical-BtUK6sKD.mjs → trismegisto-spine-auto-service-suggestions.technical-CGBBzAR8.mjs} +14 -1
  21. package/fesm2022/{trismegisto-spine-auto-service-suggestions.technical-BtUK6sKD.mjs.map → trismegisto-spine-auto-service-suggestions.technical-CGBBzAR8.mjs.map} +1 -1
  22. package/fesm2022/{trismegisto-spine-auto-service-suggestions.usage-BwO9Hjsx.mjs → trismegisto-spine-auto-service-suggestions.usage-DnAJS-Z0.mjs} +26 -3
  23. package/fesm2022/trismegisto-spine-auto-service-suggestions.usage-DnAJS-Z0.mjs.map +1 -0
  24. package/fesm2022/trismegisto-spine-auto-service.mjs +24 -11
  25. package/fesm2022/trismegisto-spine-auto-service.mjs.map +1 -1
  26. package/fesm2022/{trismegisto-spine-layout-suggestions-page.component-xI2gDMok.mjs → trismegisto-spine-layout-suggestions-page.component-CEKMqera.mjs} +3 -3
  27. package/fesm2022/{trismegisto-spine-layout-suggestions-page.component-xI2gDMok.mjs.map → trismegisto-spine-layout-suggestions-page.component-CEKMqera.mjs.map} +1 -1
  28. package/fesm2022/{trismegisto-spine-layout-trismegisto-spine-layout-Clphrsgc.mjs → trismegisto-spine-layout-trismegisto-spine-layout-DiAfxIGP.mjs} +428 -93
  29. package/fesm2022/trismegisto-spine-layout-trismegisto-spine-layout-DiAfxIGP.mjs.map +1 -0
  30. package/fesm2022/trismegisto-spine-layout.mjs +1 -1
  31. package/fesm2022/trismegisto-spine-side-panel.mjs +1 -1
  32. package/fesm2022/trismegisto-spine-side-panel.mjs.map +1 -1
  33. package/fesm2022/trismegisto-spine.mjs +528 -10
  34. package/fesm2022/trismegisto-spine.mjs.map +1 -1
  35. package/package.json +6 -6
  36. package/types/trismegisto-spine-auto-service.d.ts +20 -4
  37. package/types/trismegisto-spine-auto-service.d.ts.map +1 -1
  38. package/types/trismegisto-spine-layout.d.ts +327 -63
  39. package/types/trismegisto-spine-layout.d.ts.map +1 -1
  40. package/types/trismegisto-spine.d.ts +221 -3
  41. package/types/trismegisto-spine.d.ts.map +1 -1
  42. package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-BYjIQ9ID.mjs.map +0 -1
  43. package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-mMjE3JOL.mjs +0 -2179
  44. package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-mMjE3JOL.mjs.map +0 -1
  45. package/fesm2022/trismegisto-spine-auto-service-groups.technical-_jQrg1Qh.mjs.map +0 -1
  46. package/fesm2022/trismegisto-spine-auto-service-suggestions.usage-BwO9Hjsx.mjs.map +0 -1
  47. package/fesm2022/trismegisto-spine-layout-trismegisto-spine-layout-Clphrsgc.mjs.map +0 -1
@@ -0,0 +1,3733 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, Injectable, input, output, signal, computed, effect, untracked, ChangeDetectionStrategy, Component, model, linkedSignal, 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, AssigneeSelectComponent, ComboboxComponent, InputComponent, LevelSelectComponent, ModalComponent, PopconfirmComponent, PopconfirmTriggerDirective, SelectComponent, TextareaComponent, HermesDialogService, AlertComponent, ComboboxMultiComponent, DatePickerComponent, IconButtonComponent, IconComponent, LevelBadgeComponent, PopoverComponent, PopoverTriggerDirective, RichTextComponent, SwitchComponent, TabComponent, TabsComponent, TagComponent, BacklogComponent, ProgressComponent, RiskRegisterComponent, GanttComponent, WorkloadComponent, DeadlineComponent, TaskListComponent, DataGridComponent, EmptyComponent, SegmentedControlComponent, WorkflowDiagramComponent, KanbanCardDirective, KanbanColumnActionsDirective, KanbanColumnEmptyDirective, KanbanColumnFooterDirective, KanbanColumnSummaryDirective, KanbanComponent, MenuButtonComponent, PaginatorComponent, SkeletonComponent } from '@trismegisto/angular';
8
+ import { urlPath, formatMessage, pluralMessage, LEVEL_SCALE_PRESETS, riskLevel, riskScore, burndownSeries, taskBurndownInput, taskGanttItems, taskWorkloadAllocations, sumEstimates } 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, HttpErrorResponse } 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
+ /**
63
+ * Todas as tarefas do app, sem o recorte do quadro (até o teto do servidor): o planejamento precisa das concluídas
64
+ * antigas (burndown da sprint) e das que estão em sprint. Arquivadas e agrupadas são filtradas por quem usa.
65
+ */
66
+ async tasks(clientId) {
67
+ const res = await this.read(this.url `/${clientId}/suggestions`);
68
+ return res.suggestions ?? [];
69
+ }
70
+ /** A tarefa inteira (checklist completo, pessoas, rótulos, última atividade). */
71
+ task(clientId, id) {
72
+ return this.read(this.url `/${clientId}/suggestions/${id}`);
73
+ }
74
+ /** Pela chave ("SUG-12"): o link copiado abre a tarefa. */
75
+ lookup(clientId, key) {
76
+ return this.read(this.url `/${clientId}/suggestions/lookup`, new HttpParams().set('key', key));
77
+ }
78
+ create(clientId, draft) {
79
+ return firstValueFrom(this.http.post(this.url `/${clientId}/suggestions`, draft, { headers: this.headers }));
80
+ }
81
+ update(clientId, id, edit) {
82
+ return firstValueFrom(this.http.patch(this.url `/${clientId}/suggestions/${id}`, edit, { headers: this.headers }));
83
+ }
84
+ /**
85
+ * Arrastar ou mudar pelo detalhe: status (coluna) e/ou posição (índice na coluna de destino, contando a coluna
86
+ * inteira); `comment` vai junto com a mudança de status (`internal` = só a equipe vê).
87
+ */
88
+ move(clientId, id, change) {
89
+ return firstValueFrom(this.http.patch(this.url `/${clientId}/suggestions/${id}`, change, { headers: this.headers }));
90
+ }
91
+ remove(clientId, id) {
92
+ return firstValueFrom(this.http.delete(this.url `/${clientId}/suggestions/${id}`, { headers: this.headers }));
93
+ }
94
+ setArchived(clientId, id, archived) {
95
+ const url = archived ? this.url `/${clientId}/suggestions/${id}/archive` : this.url `/${clientId}/suggestions/${id}/restore`;
96
+ return firstValueFrom(this.http.post(url, {}, { headers: this.headers }));
97
+ }
98
+ /** Prévia do agrupamento em `into`: quantos relatores e apoios passariam (nada é gravado). */
99
+ mergePreview(clientId, id, into) {
100
+ return this.read(this.url `/${clientId}/suggestions/${id}/merge`, new HttpParams().set('into', into));
101
+ }
102
+ /** Agrupa esta em `into`: responde o destino inteiro (com os relatores e as agrupadas). */
103
+ merge(clientId, id, into) {
104
+ return firstValueFrom(this.http.post(this.url `/${clientId}/suggestions/${id}/merge`, { into }, { headers: this.headers }));
105
+ }
106
+ /** Desagrupa esta (a origem): ela volta ao quadro; responde ela inteira. */
107
+ unmerge(clientId, id) {
108
+ return firstValueFrom(this.http.post(this.url `/${clientId}/suggestions/${id}/unmerge`, {}, { headers: this.headers }));
109
+ }
110
+ /** Comentários ou atividades, do mais novo, paginado. */
111
+ events(clientId, id, q) {
112
+ const params = new HttpParams().set('type', q.type).set('limit', q.limit).set('offset', q.offset);
113
+ return this.read(this.url `/${clientId}/suggestions/${id}/events`, params);
114
+ }
115
+ comment(clientId, id, comment, internal = false) {
116
+ return firstValueFrom(this.http.post(this.url `/${clientId}/suggestions/${id}/comments`, { comment, internal }, { headers: this.headers }));
117
+ }
118
+ /** Marca a conversa como lida por quem consulta (zera o `unread` da tarefa). */
119
+ markRead(clientId, id) {
120
+ return firstValueFrom(this.http.post(this.url `/${clientId}/suggestions/${id}/read`, {}, { headers: this.headers }));
121
+ }
122
+ /** Responsável/envolvido: `userId` ou "me" (atribuir a mim). */
123
+ addPerson(clientId, id, userId, role) {
124
+ return firstValueFrom(this.http.post(this.url `/${clientId}/suggestions/${id}/people`, { user_id: userId, role }, { headers: this.headers }));
125
+ }
126
+ removePerson(clientId, id, userId, role) {
127
+ return firstValueFrom(this.http.delete(this.url `/${clientId}/suggestions/${id}/people/${userId}`, {
128
+ headers: this.headers, params: new HttpParams().set('role', role),
129
+ }));
130
+ }
131
+ addChecklistItem(clientId, id, text) {
132
+ return firstValueFrom(this.http.post(this.url `/${clientId}/suggestions/${id}/checklist`, { text }, { headers: this.headers }));
133
+ }
134
+ updateChecklistItem(clientId, id, itemId, change) {
135
+ return firstValueFrom(this.http.patch(this.url `/${clientId}/suggestions/${id}/checklist/${itemId}`, change, { headers: this.headers }));
136
+ }
137
+ removeChecklistItem(clientId, id, itemId) {
138
+ return firstValueFrom(this.http.delete(this.url `/${clientId}/suggestions/${id}/checklist/${itemId}`, { headers: this.headers }));
139
+ }
140
+ // --- rótulos ---
141
+ async labels(clientId) {
142
+ const res = await this.read(this.url `/${clientId}/suggestions/labels`);
143
+ return res.labels ?? [];
144
+ }
145
+ createLabel(clientId, name, color) {
146
+ return firstValueFrom(this.http.post(this.url `/${clientId}/suggestions/labels`, { name, color }, { headers: this.headers }));
147
+ }
148
+ setLabel(clientId, id, labelId, linked) {
149
+ const url = this.url `/${clientId}/suggestions/${id}/labels/${labelId}`;
150
+ return firstValueFrom(linked
151
+ ? this.http.put(url, {}, { headers: this.headers })
152
+ : this.http.delete(url, { headers: this.headers }));
153
+ }
154
+ // --- planejamento: dependências, sprints, marcos e riscos ---
155
+ /** "Esta depende daquela" (liga ou desliga); ciclo responde 409 `dependency_cycle`. Responde esta tarefa inteira. */
156
+ setDependency(clientId, id, dependsOnId, linked) {
157
+ const url = this.url `/${clientId}/suggestions/${id}/dependencies/${dependsOnId}`;
158
+ return firstValueFrom(linked
159
+ ? this.http.put(url, {}, { headers: this.headers })
160
+ : this.http.delete(url, { headers: this.headers }));
161
+ }
162
+ async sprints(clientId) {
163
+ const res = await this.read(this.url `/${clientId}/suggestions/sprints`);
164
+ return res.sprints ?? [];
165
+ }
166
+ createSprint(clientId, input) {
167
+ return firstValueFrom(this.http.post(this.url `/${clientId}/suggestions/sprints`, input, { headers: this.headers }));
168
+ }
169
+ /** Edita; `state: 'closed'` encerra e o servidor devolve ao backlog as tarefas não concluídas. */
170
+ updateSprint(clientId, sprintId, input) {
171
+ return firstValueFrom(this.http.patch(this.url `/${clientId}/suggestions/sprints/${sprintId}`, input, { headers: this.headers }));
172
+ }
173
+ removeSprint(clientId, sprintId) {
174
+ return firstValueFrom(this.http.delete(this.url `/${clientId}/suggestions/sprints/${sprintId}`, { headers: this.headers }));
175
+ }
176
+ async milestones(clientId) {
177
+ const res = await this.read(this.url `/${clientId}/suggestions/milestones`);
178
+ return res.milestones ?? [];
179
+ }
180
+ createMilestone(clientId, title, date) {
181
+ return firstValueFrom(this.http.post(this.url `/${clientId}/suggestions/milestones`, { title, date }, { headers: this.headers }));
182
+ }
183
+ updateMilestone(clientId, milestoneId, change) {
184
+ return firstValueFrom(this.http.patch(this.url `/${clientId}/suggestions/milestones/${milestoneId}`, change, { headers: this.headers }));
185
+ }
186
+ removeMilestone(clientId, milestoneId) {
187
+ return firstValueFrom(this.http.delete(this.url `/${clientId}/suggestions/milestones/${milestoneId}`, { headers: this.headers }));
188
+ }
189
+ async risks(clientId) {
190
+ const res = await this.read(this.url `/${clientId}/suggestions/risks`);
191
+ return res.risks ?? [];
192
+ }
193
+ createRisk(clientId, input) {
194
+ return firstValueFrom(this.http.post(this.url `/${clientId}/suggestions/risks`, input, { headers: this.headers }));
195
+ }
196
+ updateRisk(clientId, riskId, input) {
197
+ return firstValueFrom(this.http.patch(this.url `/${clientId}/suggestions/risks/${riskId}`, input, { headers: this.headers }));
198
+ }
199
+ removeRisk(clientId, riskId) {
200
+ return firstValueFrom(this.http.delete(this.url `/${clientId}/suggestions/risks/${riskId}`, { headers: this.headers }));
201
+ }
202
+ // --- pessoas do realm do app (a mesma busca da aba Grupos, com a foto) ---
203
+ async searchPeople(clientId, search) {
204
+ let params = new HttpParams().set('limit', 10);
205
+ if (search.trim())
206
+ params = params.set('search', search.trim());
207
+ const res = await this.read(this.url `/${clientId}/users`, params);
208
+ return res.users ?? [];
209
+ }
210
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceSuggestionsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
211
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceSuggestionsService, providedIn: 'root' }); }
212
+ }
213
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceSuggestionsService, decorators: [{
214
+ type: Injectable,
215
+ args: [{ providedIn: 'root' }]
216
+ }] });
217
+
218
+ // 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
219
+ // tarefa -- chave, prioridade, datas, capa, descrição rica, pessoas, rótulos e checklist. Constantes e contas puras
220
+ // (prazo, capa) ficam aqui, fora dos componentes, para o cartão, o detalhe e os testes usarem as mesmas.
221
+ const TASK_PRIORITIES = ['urgente', 'alta', 'media', 'baixa'];
222
+ /** Ícone e intenção de cada prioridade (o texto vem do catálogo; a cor nunca é a única pista). */
223
+ const TASK_PRIORITY_ICONS = {
224
+ urgente: 'exclamation-triangle', alta: 'arrow-up', media: 'minus', baixa: 'arrow-bottom',
225
+ };
226
+ /** Cores de rótulo: tokens da paleta categórica do tema (o servidor só aceita estes). */
227
+ const TASK_LABEL_COLORS = ['cat-1', 'cat-2', 'cat-3', 'cat-4', 'cat-5', 'cat-6', 'cat-7', 'cat-8'];
228
+ const RISK_STATUSES = ['identified', 'mitigating', 'closed'];
229
+ /** Hoje como AAAA-MM-DD no fuso dado (o do perfil da pessoa). */
230
+ function todayIso(timeZone, now = new Date()) {
231
+ try {
232
+ return new Intl.DateTimeFormat('en-CA', { timeZone, year: 'numeric', month: '2-digit', day: '2-digit' }).format(now);
233
+ }
234
+ catch {
235
+ return now.toISOString().slice(0, 10);
236
+ }
237
+ }
238
+ /** AAAA-MM-DD mais `days` dias (negativo volta). */
239
+ function addDays(iso, days) {
240
+ const d = new Date(`${iso}T00:00:00Z`);
241
+ d.setUTCDate(d.getUTCDate() + days);
242
+ return d.toISOString().slice(0, 10);
243
+ }
244
+ /** O prazo da tarefa contra hoje. Concluída/recusada não vence. */
245
+ function dueState(task, today) {
246
+ if (!task.due_date || task.status === 'concluida' || task.status === 'recusada')
247
+ return null;
248
+ if (task.due_date < today)
249
+ return 'overdue';
250
+ if (task.due_date === today)
251
+ return 'today';
252
+ if (task.due_date === addDays(today, 1))
253
+ return 'tomorrow';
254
+ return 'later';
255
+ }
256
+ /** Dias inteiros entre duas datas AAAA-MM-DD (`to` - `from`). */
257
+ function daysBetween(from, to) {
258
+ return Math.round((Date.parse(`${to}T00:00:00Z`) - Date.parse(`${from}T00:00:00Z`)) / 86_400_000);
259
+ }
260
+ /** Duração em dias, horas e minutos inteiros (o "4h 12min" do quadro). Negativa vira zero. */
261
+ function splitDuration(ms) {
262
+ const minutes = Math.max(0, Math.floor(ms / 60_000));
263
+ return { d: Math.floor(minutes / 1440), h: Math.floor((minutes % 1440) / 60), m: minutes % 60 };
264
+ }
265
+ /** Responsável por mim? (o `sub` da sessão) */
266
+ const assignedTo = (task, userId) => !!userId && task.assignees.some((p) => p.user_id === userId);
267
+
268
+ // Os pequenos seletores do quadro de sugestões, para ir dentro de um `hm-popover` (o cartão e o detalhe da tarefa):
269
+ // pessoas do realm do app (com foto) e rótulos do catálogo do app.
270
+ /**
271
+ * Pessoa do realm do app: busca (a mesma do auto-serviço, com a foto), as já escolhidas de fora. Lê na abertura (as
272
+ * primeiras) e a cada termo, com espera.
273
+ */
274
+ class SuggestionPeoplePickerComponent {
275
+ constructor() {
276
+ this.text = inject(SPINE_MESSAGES).suggestions;
277
+ this.service = inject(AutoServiceSuggestionsService);
278
+ this.clientId = input.required(...(ngDevMode ? [{ debugName: "clientId" }] : /* istanbul ignore next */ []));
279
+ /** Quem já está no papel (não aparece de novo). */
280
+ this.exclude = input([], ...(ngDevMode ? [{ debugName: "exclude" }] : /* istanbul ignore next */ []));
281
+ this.picked = output();
282
+ this.found = signal([], ...(ngDevMode ? [{ debugName: "found" }] : /* istanbul ignore next */ []));
283
+ this.loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
284
+ this.options = computed(() => this.found().filter((p) => !this.exclude().includes(p.id)), ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
285
+ this.seq = 0;
286
+ effect(() => {
287
+ this.clientId();
288
+ untracked(() => void this.load(''));
289
+ });
290
+ }
291
+ async load(term) {
292
+ const mine = ++this.seq;
293
+ this.loading.set(true);
294
+ try {
295
+ const list = await this.service.searchPeople(this.clientId(), term);
296
+ if (mine === this.seq)
297
+ this.found.set(list);
298
+ }
299
+ catch {
300
+ if (mine === this.seq)
301
+ this.found.set([]);
302
+ }
303
+ finally {
304
+ if (mine === this.seq)
305
+ this.loading.set(false);
306
+ }
307
+ }
308
+ nameOf(p) {
309
+ return p.name || p.username || p.email || '';
310
+ }
311
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionPeoplePickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
312
+ 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: `
313
+ <div class="as-picker">
314
+ <hm-search-field [label]="text.peopleSearch" labelHidden [placeholder]="text.peopleSearch" [landmark]="false"
315
+ [debounce]="250" [loading]="loading()" (search)="load($event)" />
316
+ <ul class="as-picker__list" [attr.aria-busy]="loading() ? 'true' : null">
317
+ @for (p of options(); track p.id) {
318
+ <li>
319
+ <button type="button" class="as-picker__person" (click)="picked.emit(p)">
320
+ <hm-account-avatar size="small" [name]="nameOf(p)" [imageUrl]="p.picture" />
321
+ <span class="as-picker__person-text">
322
+ <span class="as-picker__person-name">{{ nameOf(p) }}</span>
323
+ @if (p.username && p.name) { <span class="as-picker__muted">{{ p.username }}</span> }
324
+ </span>
325
+ </button>
326
+ </li>
327
+ } @empty {
328
+ @if (!loading()) { <li class="as-picker__muted">{{ text.peopleEmpty }}</li> }
329
+ }
330
+ </ul>
331
+ </div>
332
+ `, 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;font-size:var(--hm-font-size-sm);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", "presence", "texts"] }, { 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 }); }
333
+ }
334
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionPeoplePickerComponent, decorators: [{
335
+ type: Component,
336
+ args: [{ selector: 'spine-suggestion-people-picker', imports: [AccountAvatarComponent, SearchFieldComponent], template: `
337
+ <div class="as-picker">
338
+ <hm-search-field [label]="text.peopleSearch" labelHidden [placeholder]="text.peopleSearch" [landmark]="false"
339
+ [debounce]="250" [loading]="loading()" (search)="load($event)" />
340
+ <ul class="as-picker__list" [attr.aria-busy]="loading() ? 'true' : null">
341
+ @for (p of options(); track p.id) {
342
+ <li>
343
+ <button type="button" class="as-picker__person" (click)="picked.emit(p)">
344
+ <hm-account-avatar size="small" [name]="nameOf(p)" [imageUrl]="p.picture" />
345
+ <span class="as-picker__person-text">
346
+ <span class="as-picker__person-name">{{ nameOf(p) }}</span>
347
+ @if (p.username && p.name) { <span class="as-picker__muted">{{ p.username }}</span> }
348
+ </span>
349
+ </button>
350
+ </li>
351
+ } @empty {
352
+ @if (!loading()) { <li class="as-picker__muted">{{ text.peopleEmpty }}</li> }
353
+ }
354
+ </ul>
355
+ </div>
356
+ `, 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;font-size:var(--hm-font-size-sm);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"] }]
357
+ }], 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"] }] } });
358
+ /**
359
+ * 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
360
+ * existe, "Criar o rótulo" com a cor escolhida.
361
+ */
362
+ class SuggestionLabelPickerComponent {
363
+ constructor() {
364
+ this.text = inject(SPINE_MESSAGES).suggestions;
365
+ this.labels = input([], ...(ngDevMode ? [{ debugName: "labels" }] : /* istanbul ignore next */ []));
366
+ this.selected = input([], ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
367
+ this.toggled = output();
368
+ this.created = output();
369
+ this.colors = TASK_LABEL_COLORS;
370
+ this.term = signal('', ...(ngDevMode ? [{ debugName: "term" }] : /* istanbul ignore next */ []));
371
+ this.color = signal('cat-1', ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
372
+ this.shown = computed(() => {
373
+ const t = this.term().trim().toLocaleLowerCase();
374
+ return this.labels().filter((l) => !t || l.name.toLocaleLowerCase().includes(t));
375
+ }, ...(ngDevMode ? [{ debugName: "shown" }] : /* istanbul ignore next */ []));
376
+ /** Termo que ainda não é rótulo: dá para criar. */
377
+ this.creatable = computed(() => {
378
+ const t = this.term().trim().toLocaleLowerCase();
379
+ return !!t && t.length <= 40 && !this.labels().some((l) => l.name.toLocaleLowerCase() === t);
380
+ }, ...(ngDevMode ? [{ debugName: "creatable" }] : /* istanbul ignore next */ []));
381
+ this.createText = computed(() => formatMessage(this.text.createLabel, { name: this.term().trim() }), ...(ngDevMode ? [{ debugName: "createText" }] : /* istanbul ignore next */ []));
382
+ }
383
+ colorName(i) {
384
+ return formatMessage(this.text.labelColorName, { n: i + 1 });
385
+ }
386
+ create() {
387
+ const name = this.term().trim();
388
+ if (!name)
389
+ return;
390
+ this.created.emit({ name, color: this.color() });
391
+ this.term.set('');
392
+ }
393
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionLabelPickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
394
+ 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: `
395
+ <div class="as-picker">
396
+ <hm-search-field [label]="text.labelFilter" labelHidden [placeholder]="text.labelFilter" [landmark]="false"
397
+ [value]="term()" (valueChange)="term.set($event)" />
398
+ <ul class="as-picker__list">
399
+ @for (l of shown(); track l.id) {
400
+ <li class="as-picker__label-row">
401
+ <span class="as-picker__dot" [class]="'as-tok--' + l.color" aria-hidden="true"></span>
402
+ <hm-checkbox [label]="l.name" [checked]="selected().includes(l.id)" (checkedChange)="toggled.emit({ label: l, linked: $event })" />
403
+ </li>
404
+ } @empty {
405
+ @if (!creatable()) { <li class="as-picker__muted">{{ text.noLabels }}</li> }
406
+ }
407
+ </ul>
408
+ @if (creatable()) {
409
+ <span class="as-picker__heading">{{ text.labelColor }}</span>
410
+ <div class="as-picker__swatches">
411
+ @for (c of colors; track c; let i = $index) {
412
+ <button type="button" class="as-picker__swatch as-picker__swatch--small" [class]="'as-tok--' + c"
413
+ [attr.aria-label]="colorName(i)" [attr.aria-pressed]="color() === c" (click)="color.set(c)"></button>
414
+ }
415
+ </div>
416
+ <hm-button size="small" emphasis="medium" icon="plus" (clicked)="create()">{{ createText() }}</hm-button>
417
+ }
418
+ </div>
419
+ `, 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;font-size:var(--hm-font-size-sm);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 }); }
420
+ }
421
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionLabelPickerComponent, decorators: [{
422
+ type: Component,
423
+ args: [{ selector: 'spine-suggestion-label-picker', imports: [ButtonComponent, CheckboxComponent, SearchFieldComponent], template: `
424
+ <div class="as-picker">
425
+ <hm-search-field [label]="text.labelFilter" labelHidden [placeholder]="text.labelFilter" [landmark]="false"
426
+ [value]="term()" (valueChange)="term.set($event)" />
427
+ <ul class="as-picker__list">
428
+ @for (l of shown(); track l.id) {
429
+ <li class="as-picker__label-row">
430
+ <span class="as-picker__dot" [class]="'as-tok--' + l.color" aria-hidden="true"></span>
431
+ <hm-checkbox [label]="l.name" [checked]="selected().includes(l.id)" (checkedChange)="toggled.emit({ label: l, linked: $event })" />
432
+ </li>
433
+ } @empty {
434
+ @if (!creatable()) { <li class="as-picker__muted">{{ text.noLabels }}</li> }
435
+ }
436
+ </ul>
437
+ @if (creatable()) {
438
+ <span class="as-picker__heading">{{ text.labelColor }}</span>
439
+ <div class="as-picker__swatches">
440
+ @for (c of colors; track c; let i = $index) {
441
+ <button type="button" class="as-picker__swatch as-picker__swatch--small" [class]="'as-tok--' + c"
442
+ [attr.aria-label]="colorName(i)" [attr.aria-pressed]="color() === c" (click)="color.set(c)"></button>
443
+ }
444
+ </div>
445
+ <hm-button size="small" emphasis="medium" icon="plus" (clicked)="create()">{{ createText() }}</hm-button>
446
+ }
447
+ </div>
448
+ `, 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;font-size:var(--hm-font-size-sm);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"] }]
449
+ }], 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"] }] } });
450
+
451
+ /** Nota de risco de um risco aberto (probabilidade x impacto); o encerrado não conta. */
452
+ const openScore = (r) => (r.status === 'closed' ? null : r.likelihood * r.impact);
453
+ /** A prioridade da sugestão no degrau da escala `priority` da lib (a que `hm-task-list`, `hm-backlog` e o Gantt leem). */
454
+ const PRIORITY_LEVEL = { urgente: 'urgent', alta: 'high', media: 'medium', baixa: 'low' };
455
+ /** As abertas (o que ainda dá trabalho): as três primeiras colunas do quadro. */
456
+ const OPEN_STATUSES$1 = ['em_analise', 'planejada', 'em_desenvolvimento'];
457
+ /** Tom e categoria de cada status no fluxo (as mesmas cores do quadro, no vocabulário da lib). */
458
+ const STATUS_FLOW = {
459
+ em_analise: { category: 'todo', tone: 'info' },
460
+ planejada: { category: 'todo', tone: 'neutral' },
461
+ em_desenvolvimento: { category: 'active', tone: 'warning' },
462
+ concluida: { category: 'done', tone: 'success' },
463
+ recusada: { category: 'canceled', tone: 'neutral' },
464
+ };
465
+ /** A pessoa da sugestão no formato da lib (id, nome e foto). */
466
+ function toAssignee(person) {
467
+ return { id: person.user_id, name: person.name || person.username || person.user_id, imageUrl: person.picture };
468
+ }
469
+ function toPlanningTask(t) {
470
+ const done = t.status === 'concluida';
471
+ return {
472
+ id: t.id,
473
+ key: t.key,
474
+ title: t.title,
475
+ description: t.description,
476
+ status: t.status,
477
+ priority: PRIORITY_LEVEL[t.priority],
478
+ assignee: t.assignees[0] ? toAssignee(t.assignees[0]) : null,
479
+ start: t.start_date,
480
+ due: t.due_date,
481
+ estimate: t.estimate ?? null,
482
+ dependsOn: t.depends_on ?? [],
483
+ done,
484
+ task: t,
485
+ sprintId: t.sprint_id ?? null,
486
+ createdAt: t.created_at,
487
+ completedAt: done ? (t.status_at ?? t.updated_at) : null,
488
+ };
489
+ }
490
+ /**
491
+ * Estado do planejamento do quadro de sugestões (uma instância por tela, nos `providers` do quadro): as tarefas sem o
492
+ * recorte do quadro, as sprints, os marcos e os riscos do app, e as gravações (cada uma aplica a resposta do servidor;
493
+ * falha vira toast e nada muda). As visões (Resumo, Tarefas, Planejamento, Cronograma, Riscos e Fluxo) só leem daqui e
494
+ * chamam as ações; abrir a tarefa fica com o quadro (o detalhe é dele).
495
+ */
496
+ class SuggestionPlanningStore {
497
+ constructor() {
498
+ this.service = inject(AutoServiceSuggestionsService);
499
+ this.toast = inject(HermesToastService);
500
+ this.messages = inject(SPINE_MESSAGES);
501
+ this.text = this.messages.suggestions;
502
+ this.plan = this.text.planning;
503
+ this.locale = this.messages.meta.locale;
504
+ this.clientId = signal('', ...(ngDevMode ? [{ debugName: "clientId" }] : /* istanbul ignore next */ []));
505
+ this.rawTasks = signal([], ...(ngDevMode ? [{ debugName: "rawTasks" }] : /* istanbul ignore next */ []));
506
+ this.sprints = signal([], ...(ngDevMode ? [{ debugName: "sprints" }] : /* istanbul ignore next */ []));
507
+ this.milestones = signal([], ...(ngDevMode ? [{ debugName: "milestones" }] : /* istanbul ignore next */ []));
508
+ this.risks = signal([], ...(ngDevMode ? [{ debugName: "risks" }] : /* istanbul ignore next */ []));
509
+ /** Os riscos já vieram do servidor (no planejamento, ou quando um detalhe de tarefa abriu fora dele). */
510
+ this.risksLoaded = signal(false, ...(ngDevMode ? [{ debugName: "risksLoaded" }] : /* istanbul ignore next */ []));
511
+ this.#risksReading = null;
512
+ /** Nota de risco de cada tarefa: a maior entre os riscos abertos dela. */
513
+ this.scoreBy = computed(() => {
514
+ const map = new Map();
515
+ for (const r of this.risks()) {
516
+ const score = openScore(r);
517
+ if (r.suggestion_id && score !== null)
518
+ map.set(r.suggestion_id, Math.max(map.get(r.suggestion_id) ?? 0, score));
519
+ }
520
+ return map;
521
+ }, ...(ngDevMode ? [{ debugName: "scoreBy" }] : /* istanbul ignore next */ []));
522
+ this.loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
523
+ this.loadError = signal('', ...(ngDevMode ? [{ debugName: "loadError" }] : /* istanbul ignore next */ []));
524
+ /** As sprints já vieram do servidor (o detalhe da tarefa lê as sprints mesmo fora das visões de planejamento). */
525
+ this.sprintsLoaded = signal(false, ...(ngDevMode ? [{ debugName: "sprintsLoaded" }] : /* istanbul ignore next */ []));
526
+ /** Quem consulta (o `sub` da sessão), para "Minhas tarefas". */
527
+ this.me = signal('', ...(ngDevMode ? [{ debugName: "me" }] : /* istanbul ignore next */ []));
528
+ /** As tarefas do quadro de trabalho: sem arquivadas e sem agrupadas (saíram do quadro). */
529
+ this.tasks = computed(() => this.rawTasks().filter((t) => !t.archived_at && !t.merged_into), ...(ngDevMode ? [{ debugName: "tasks" }] : /* istanbul ignore next */ []));
530
+ this.items = computed(() => this.tasks().map(toPlanningTask), ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
531
+ this.open = computed(() => this.items().filter((t) => OPEN_STATUSES$1.includes(t.task.status)), ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
532
+ this.activeSprint = computed(() => this.sprints().find((s) => s.state === 'active') ?? null, ...(ngDevMode ? [{ debugName: "activeSprint" }] : /* istanbul ignore next */ []));
533
+ this.sprintName = computed(() => new Map(this.sprints().map((s) => [s.id, s.name])), ...(ngDevMode ? [{ debugName: "sprintName" }] : /* istanbul ignore next */ []));
534
+ /** As pessoas que já aparecem no app (responsáveis, envolvidos, quem pediu e os donos de risco), por nome. */
535
+ this.people = computed(() => {
536
+ const byId = new Map();
537
+ for (const t of this.tasks()) {
538
+ for (const p of [...t.assignees, ...t.watchers, ...(t.author ? [t.author] : [])])
539
+ byId.set(p.user_id, toAssignee(p));
540
+ }
541
+ for (const r of this.risks())
542
+ if (r.owner)
543
+ byId.set(r.owner.user_id, toAssignee(r.owner));
544
+ return [...byId.values()].sort((a, b) => a.name.localeCompare(b.name));
545
+ }, ...(ngDevMode ? [{ debugName: "people" }] : /* istanbul ignore next */ []));
546
+ /** Os 5 status como a lib entende (o texto é o do catálogo), com quantas há em cada um no quadro. */
547
+ this.statuses = computed(() => {
548
+ const counts = new Map();
549
+ for (const t of this.tasks())
550
+ counts.set(t.status, (counts.get(t.status) ?? 0) + 1);
551
+ return SPINE_SUGGESTION_STATUSES.map((id) => ({
552
+ id,
553
+ label: this.text.statuses[id],
554
+ ...STATUS_FLOW[id],
555
+ description: pluralMessage(this.plan.workflow.count, counts.get(id) ?? 0, this.locale),
556
+ }));
557
+ }, ...(ngDevMode ? [{ debugName: "statuses" }] : /* istanbul ignore next */ []));
558
+ /** O fluxo fixo das sugestões (só leitura): o caminho principal, recusar uma aberta e reabrir a concluída. */
559
+ this.workflow = computed(() => {
560
+ const w = this.plan.workflow;
561
+ return {
562
+ statuses: this.statuses(),
563
+ initial: 'em_analise',
564
+ transitions: [
565
+ { from: 'em_analise', to: 'planejada', label: w.plan },
566
+ { from: 'planejada', to: 'em_desenvolvimento', label: w.start },
567
+ { from: 'em_desenvolvimento', to: 'concluida', label: w.finish },
568
+ ...OPEN_STATUSES$1.map((from) => ({ from, to: 'recusada', label: w.refuse, danger: true, reason: 'optional' })),
569
+ { from: 'concluida', to: 'em_desenvolvimento', label: w.reopen },
570
+ { from: 'recusada', to: 'em_analise', label: w.reopen },
571
+ ],
572
+ };
573
+ }, ...(ngDevMode ? [{ debugName: "workflow" }] : /* istanbul ignore next */ []));
574
+ /** O status do risco no vocabulário da lib (o `hm-risk-register` agrupa por ele). */
575
+ this.riskStatuses = computed(() => RISK_STATUSES.map((id) => ({
576
+ id,
577
+ label: this.plan.risks.statuses[id],
578
+ category: id === 'closed' ? 'done' : id === 'mitigating' ? 'active' : 'todo',
579
+ ...(id === 'identified' ? { tone: 'warning' } : {}),
580
+ })), ...(ngDevMode ? [{ debugName: "riskStatuses" }] : /* istanbul ignore next */ []));
581
+ /** Os riscos no formato do `RiskItem` (o dono como pessoa da lib). */
582
+ this.riskItems = computed(() => this.risks().map((r) => ({
583
+ id: r.id, title: r.title, likelihood: r.likelihood, impact: r.impact, status: r.status, mitigation: r.mitigation,
584
+ owner: r.owner ? toAssignee(r.owner) : null, risk: r,
585
+ // A tarefa ameaçada sai na linha de baixo do título, no registro.
586
+ category: r.suggestion_title ?? this.plan.risks.noTask,
587
+ })), ...(ngDevMode ? [{ debugName: "riskItems" }] : /* istanbul ignore next */ []));
588
+ /** Uma leitura de sprints em andamento: o detalhe e o backlog pedem juntos, sai uma só. */
589
+ this.sprintsReading = false;
590
+ }
591
+ #risksReading;
592
+ /**
593
+ * Nota de risco da tarefa: com os riscos já lidos, a conta é daqui (acompanha cada gravação na hora); sem eles, a
594
+ * que veio do servidor junto com a tarefa.
595
+ */
596
+ riskScoreOf(task) {
597
+ return this.risksLoaded() ? (this.scoreBy().get(task.id) ?? null) : (task.risk_score ?? null);
598
+ }
599
+ /** Os riscos de uma tarefa, os abertos e maiores primeiro. */
600
+ risksOf(suggestionId) {
601
+ return this.risks().filter((r) => r.suggestion_id === suggestionId)
602
+ .sort((a, b) => (openScore(b) ?? -1) - (openScore(a) ?? -1) || a.title.localeCompare(b.title));
603
+ }
604
+ /** Lê os riscos uma vez (o detalhe da tarefa abre também fora das visões de planejamento). */
605
+ async ensureRisks(clientId = this.clientId()) {
606
+ if (!clientId || this.risksLoaded())
607
+ return;
608
+ this.clientId.set(clientId);
609
+ this.#risksReading ??= this.service.risks(clientId)
610
+ .then((risks) => { if (clientId === this.clientId()) {
611
+ this.risks.set(risks);
612
+ this.risksLoaded.set(true);
613
+ } })
614
+ .catch(() => undefined)
615
+ .finally(() => { this.#risksReading = null; });
616
+ return this.#risksReading;
617
+ }
618
+ async load(clientId = this.clientId()) {
619
+ if (!clientId)
620
+ return;
621
+ this.clientId.set(clientId);
622
+ this.loading.set(true);
623
+ this.loadError.set('');
624
+ try {
625
+ const [tasks, sprints, milestones, risks] = await Promise.all([
626
+ this.service.tasks(clientId), this.service.sprints(clientId), this.service.milestones(clientId), this.service.risks(clientId),
627
+ ]);
628
+ if (clientId !== this.clientId())
629
+ return;
630
+ this.rawTasks.set(tasks);
631
+ this.sprints.set(sprints);
632
+ this.sprintsLoaded.set(true);
633
+ this.milestones.set(milestones);
634
+ this.risks.set(risks);
635
+ this.risksLoaded.set(true);
636
+ }
637
+ catch {
638
+ this.loadError.set(this.plan.loadError);
639
+ }
640
+ finally {
641
+ this.loading.set(false);
642
+ }
643
+ }
644
+ /** A tarefa que o servidor devolveu (detalhe, quadro): troca aqui, sem reler tudo. */
645
+ replace(task) {
646
+ this.rawTasks.update((list) => (list.some((t) => t.id === task.id) ? list.map((t) => (t.id === task.id ? task : t)) : [...list, task]));
647
+ }
648
+ remove(id) {
649
+ this.rawTasks.update((list) => list.filter((t) => t.id !== id));
650
+ }
651
+ /** Grava um campo da tarefa (datas, pontos, sprint). true = gravou. */
652
+ async updateTask(id, edit) {
653
+ return this.run(async () => this.replace(await this.service.update(this.clientId(), id, edit)));
654
+ }
655
+ // --- sprints ---
656
+ async saveSprint(id, input) {
657
+ return this.run(async () => {
658
+ const saved = id ? await this.service.updateSprint(this.clientId(), id, input) : await this.service.createSprint(this.clientId(), input);
659
+ this.upsertSprint(saved);
660
+ });
661
+ }
662
+ async startSprint(sprint) {
663
+ return this.run(async () => this.upsertSprint(await this.service.updateSprint(this.clientId(), sprint.id, { state: 'active' })));
664
+ }
665
+ /** Encerrar: o servidor devolve ao backlog as não concluídas; relê as tarefas para mostrar isso. */
666
+ async closeSprint(sprint) {
667
+ return this.run(async () => {
668
+ this.upsertSprint(await this.service.updateSprint(this.clientId(), sprint.id, { state: 'closed' }));
669
+ this.rawTasks.set(await this.service.tasks(this.clientId()));
670
+ });
671
+ }
672
+ async removeSprint(sprint) {
673
+ return this.run(async () => {
674
+ await this.service.removeSprint(this.clientId(), sprint.id);
675
+ this.sprints.update((list) => list.filter((s) => s.id !== sprint.id));
676
+ this.rawTasks.update((list) => list.map((t) => (t.sprint_id === sprint.id ? { ...t, sprint_id: null } : t)));
677
+ });
678
+ }
679
+ /** Relê as sprints (as contas de pontos são do servidor) depois de mover tarefas entre elas. */
680
+ async refreshSprints() {
681
+ if (!this.clientId() || this.sprintsReading)
682
+ return;
683
+ this.sprintsReading = true;
684
+ try {
685
+ this.sprints.set(await this.service.sprints(this.clientId()));
686
+ this.sprintsLoaded.set(true);
687
+ }
688
+ catch {
689
+ /* as contas ficam as de antes; a próxima leitura corrige */
690
+ }
691
+ finally {
692
+ this.sprintsReading = false;
693
+ }
694
+ }
695
+ upsertSprint(saved) {
696
+ this.sprints.update((list) => (list.some((s) => s.id === saved.id)
697
+ ? list.map((s) => (s.id === saved.id ? saved : s))
698
+ : [...list, saved]).sort((a, b) => a.start_date.localeCompare(b.start_date)));
699
+ }
700
+ // --- marcos ---
701
+ async createMilestone(title, date) {
702
+ return this.run(async () => {
703
+ const saved = await this.service.createMilestone(this.clientId(), title, date);
704
+ this.milestones.update((list) => [...list, saved].sort((a, b) => a.date.localeCompare(b.date)));
705
+ });
706
+ }
707
+ async moveMilestone(id, date) {
708
+ return this.run(async () => {
709
+ const saved = await this.service.updateMilestone(this.clientId(), id, { date });
710
+ this.milestones.update((list) => list.map((m) => (m.id === id ? saved : m)).sort((a, b) => a.date.localeCompare(b.date)));
711
+ });
712
+ }
713
+ async removeMilestone(id) {
714
+ return this.run(async () => {
715
+ await this.service.removeMilestone(this.clientId(), id);
716
+ this.milestones.update((list) => list.filter((m) => m.id !== id));
717
+ });
718
+ }
719
+ // --- riscos ---
720
+ async saveRisk(id, input) {
721
+ return this.run(async () => {
722
+ const saved = id ? await this.service.updateRisk(this.clientId(), id, input) : await this.service.createRisk(this.clientId(), input);
723
+ this.risks.update((list) => (list.some((r) => r.id === saved.id) ? list.map((r) => (r.id === saved.id ? saved : r)) : [...list, saved]));
724
+ });
725
+ }
726
+ async removeRisk(id) {
727
+ return this.run(async () => {
728
+ await this.service.removeRisk(this.clientId(), id);
729
+ this.risks.update((list) => list.filter((r) => r.id !== id));
730
+ });
731
+ }
732
+ /** Executa uma gravação; falha = toast (a mensagem do servidor para os casos conhecidos) e false. */
733
+ async run(action) {
734
+ try {
735
+ await action();
736
+ return true;
737
+ }
738
+ catch (err) {
739
+ this.toast.error(this.errorText(err));
740
+ return false;
741
+ }
742
+ }
743
+ errorText(err) {
744
+ const code = err instanceof HttpErrorResponse ? err.error?.error : null;
745
+ if (code === 'active_sprint_exists')
746
+ return this.plan.sprints.activeExists;
747
+ if (code === 'dependency_cycle')
748
+ return this.plan.detail.dependencyCycle;
749
+ if (code === 'invalid_dates')
750
+ return this.plan.sprints.datesOrder;
751
+ if (code === 'invalid_suggestion' || code === 'suggestion_id_required')
752
+ return this.plan.risks.taskRequired;
753
+ return this.plan.saveError;
754
+ }
755
+ /** "5 pts" (ou "Sem estimativa"). */
756
+ pointsText(points) {
757
+ return points === null || points === undefined ? this.plan.noEstimate : pluralMessage(this.plan.pointsValue, points, this.locale);
758
+ }
759
+ /** "2 a 13 de nov." no idioma da tela. */
760
+ rangeText(start, end) {
761
+ const format = new Intl.DateTimeFormat(this.locale, { day: 'numeric', month: 'short', timeZone: 'UTC' });
762
+ const day = (iso) => format.format(new Date(`${iso}T00:00:00Z`));
763
+ return formatMessage(this.plan.dateRange, { start: day(start), end: day(end) });
764
+ }
765
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionPlanningStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
766
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionPlanningStore }); }
767
+ }
768
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionPlanningStore, decorators: [{
769
+ type: Injectable
770
+ }] });
771
+
772
+ /** Valores das escalas prontas de probabilidade e impacto, do degrau 1 ao 5. */
773
+ const LIKELIHOOD = LEVEL_SCALE_PRESETS.likelihood.map(([value]) => value);
774
+ const IMPACT = LEVEL_SCALE_PRESETS.impact.map(([value]) => value);
775
+ /**
776
+ * Cadastro e edição de um risco num `hm-modal`, usado pela visão Riscos e pelo detalhe da tarefa. Todo risco é de uma
777
+ * tarefa: na visão Riscos ela se escolhe (`hm-combobox`, obrigatória); no detalhe vem fixa (`task`). Probabilidade e
778
+ * impacto pelo `hm-level-select` segmentado (as escalas prontas, convertidas para os degraus 1..5), o dono pelo
779
+ * `hm-assignee-select` (as pessoas que já aparecem no app), o status e a mitigação.
780
+ */
781
+ class SuggestionRiskFormComponent {
782
+ constructor() {
783
+ this.s = inject(SuggestionPlanningStore);
784
+ this.p = this.s.plan;
785
+ this.statuses = RISK_STATUSES;
786
+ this.likelihoodValues = LIKELIHOOD;
787
+ this.impactValues = IMPACT;
788
+ this.open = model(false, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
789
+ /** O risco a editar; null = risco novo. */
790
+ this.risk = input(null, ...(ngDevMode ? [{ debugName: "risk" }] : /* istanbul ignore next */ []));
791
+ /** A tarefa fixa (aberto pelo detalhe dela); sem ela, a tarefa se escolhe no formulário. */
792
+ this.task = input(null, ...(ngDevMode ? [{ debugName: "task" }] : /* istanbul ignore next */ []));
793
+ this.submitted = linkedSignal({ ...(ngDevMode ? { debugName: "submitted" } : /* istanbul ignore next */ {}), source: this.open, computation: () => false });
794
+ this.saving = signal(false, ...(ngDevMode ? [{ debugName: "saving" }] : /* istanbul ignore next */ []));
795
+ /** Reabre do zero a cada abertura (ou troca de risco). */
796
+ this.draft = linkedSignal({ ...(ngDevMode ? { debugName: "draft" } : /* istanbul ignore next */ {}), source: () => ({ open: this.open(), risk: this.risk() }),
797
+ computation: ({ risk }) => (risk ? this.fromRisk(risk) : this.empty()) });
798
+ this.likelihood = computed(() => LIKELIHOOD[this.draft().likelihood - 1] ?? null, ...(ngDevMode ? [{ debugName: "likelihood" }] : /* istanbul ignore next */ []));
799
+ this.impact = computed(() => IMPACT[this.draft().impact - 1] ?? null, ...(ngDevMode ? [{ debugName: "impact" }] : /* istanbul ignore next */ []));
800
+ /** As tarefas que podem ter risco: as do quadro (a sugestão agrupada some com a de destino). */
801
+ this.taskOptions = computed(() => this.s.items().map((t) => ({ value: t.id, label: t.key ? `${t.key} · ${t.title}` : t.title })), ...(ngDevMode ? [{ debugName: "taskOptions" }] : /* istanbul ignore next */ []));
802
+ }
803
+ /** Valor da escala -> degrau 1..5 (o servidor guarda o número). */
804
+ level(values, value) {
805
+ return Math.max(1, values.indexOf(value ?? '') + 1);
806
+ }
807
+ patch(change) {
808
+ this.draft.update((d) => ({ ...d, ...change }));
809
+ }
810
+ async save() {
811
+ this.submitted.set(true);
812
+ const d = this.draft();
813
+ const suggestionId = this.task()?.id ?? d.suggestionId;
814
+ if (!d.title.trim() || !suggestionId)
815
+ return;
816
+ this.saving.set(true);
817
+ const ok = await this.s.saveRisk(d.id, {
818
+ title: d.title.trim(), likelihood: d.likelihood, impact: d.impact, owner_id: d.ownerId, status: d.status,
819
+ mitigation: d.mitigation.trim(), suggestion_id: suggestionId,
820
+ });
821
+ this.saving.set(false);
822
+ if (ok)
823
+ this.open.set(false);
824
+ }
825
+ async remove() {
826
+ const id = this.draft().id;
827
+ if (id && (await this.s.removeRisk(id)))
828
+ this.open.set(false);
829
+ }
830
+ fromRisk(risk) {
831
+ return {
832
+ id: risk.id, title: risk.title, suggestionId: risk.suggestion_id, likelihood: risk.likelihood, impact: risk.impact,
833
+ ownerId: risk.owner?.user_id ?? null, status: risk.status, mitigation: risk.mitigation,
834
+ };
835
+ }
836
+ empty() {
837
+ return {
838
+ // Sem rastrear: a tarefa fixa não reabre o rascunho enquanto se digita.
839
+ id: null, title: '', suggestionId: untracked(this.task)?.id ?? null, likelihood: 3, impact: 3, ownerId: this.s.me() || null,
840
+ status: 'identified', mitigation: '',
841
+ };
842
+ }
843
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionRiskFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
844
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SuggestionRiskFormComponent, isStandalone: true, selector: "spine-suggestion-risk-form", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, risk: { classPropertyName: "risk", publicName: "risk", isSignal: true, isRequired: false, transformFunction: null }, task: { classPropertyName: "task", publicName: "task", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange" }, ngImport: i0, template: `
845
+ <hm-modal [heading]="draft().id ? p.risks.editHeading : p.risks.newHeading" size="large" [(open)]="open">
846
+ <form class="plan-form" (submit)="$event.preventDefault(); save()" novalidate>
847
+ <hm-input [label]="p.risks.title" required [maxLength]="200" [value]="draft().title" (valueChange)="patch({ title: $any($event) })"
848
+ [error]="submitted() && !draft().title.trim() ? p.risks.titleRequired : ''" />
849
+ @if (task(); as fixed) {
850
+ <p class="plan-muted">{{ p.risks.task }}: <strong>{{ fixed.title }}</strong></p>
851
+ } @else {
852
+ <hm-combobox [label]="p.risks.task" required [placeholder]="p.risks.taskPlaceholder" [options]="taskOptions()"
853
+ [value]="draft().suggestionId ?? ''" (valueChange)="patch({ suggestionId: $event || null })"
854
+ [error]="submitted() && !draft().suggestionId ? p.risks.taskRequired : ''" />
855
+ }
856
+ <hm-level-select scale="likelihood" appearance="segmented" [value]="likelihood()" (valueChange)="patch({ likelihood: level(likelihoodValues, $event) })" />
857
+ <hm-level-select scale="impact" appearance="segmented" [value]="impact()" (valueChange)="patch({ impact: level(impactValues, $event) })" />
858
+ <div class="plan-row">
859
+ <hm-assignee-select [label]="p.risks.owner" [people]="s.people()" [me]="s.me()" [value]="draft().ownerId"
860
+ (valueChange)="patch({ ownerId: $event || null })" />
861
+ <hm-select [label]="p.risks.status" [value]="draft().status" (valueChange)="patch({ status: $any($event) })">
862
+ @for (st of statuses; track st) {
863
+ <option [value]="st" [selected]="st === draft().status">{{ p.risks.statuses[st] }}</option>
864
+ }
865
+ </hm-select>
866
+ </div>
867
+ <hm-textarea [label]="p.risks.mitigation" [rows]="3" autoGrow [maxLength]="2000" [value]="draft().mitigation" (valueChange)="patch({ mitigation: $event })" />
868
+ </form>
869
+ <div footer class="plan-footer">
870
+ @if (draft().id) {
871
+ <hm-button class="plan-footer__start" intent="danger" emphasis="minimal" icon="trash" [hmPopconfirmTrigger]="removeConfirm">{{ p.delete }}</hm-button>
872
+ <hm-popconfirm #removeConfirm [heading]="p.risks.deleteHeading" [confirmLabel]="p.delete" intent="danger" (confirmed)="remove()" />
873
+ }
874
+ <hm-button emphasis="minimal" (clicked)="open.set(false)">{{ p.cancel }}</hm-button>
875
+ <hm-button [loading]="saving()" (clicked)="save()">{{ p.save }}</hm-button>
876
+ </div>
877
+ </hm-modal>
878
+ `, isInline: true, styles: [":host{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--hm-space-md);min-inline-size:0}.plan-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,20rem),1fr));gap:var(--hm-space-md)}.plan-card{display:grid;grid-template-columns:minmax(0,1fr);align-content:start;gap:var(--hm-space-sm);min-inline-size:0;padding:var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.plan-card h3,.plan-card h4{margin:0;font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold);line-height:var(--hm-line-height-snug)}.plan-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.plan-toolbar__end{margin-inline-start:auto}.plan-muted{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-normal)}.plan-stats{display:flex;flex-wrap:wrap;gap:var(--hm-space-md);margin:0}.plan-stats div{display:grid;gap:var(--hm-space-nano);min-inline-size:0}.plan-stats dt{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.plan-stats dd{margin:0;font-size:var(--hm-font-size-lg);font-weight:var(--hm-font-weight-semibold)}.plan-text{margin:0;font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-snug)}.plan-title{font-weight:var(--hm-font-weight-medium)}.plan-rows{display:grid;gap:var(--hm-space-xs);margin:0;padding:0;font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-snug);list-style:none}.plan-rows li{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs) var(--hm-space-sm);min-inline-size:0}.plan-link{min-inline-size:0;padding:0;border:0;background:none;color:var(--hm-accent-strong);font:inherit;font-weight:var(--hm-font-weight-medium);text-align:start;cursor:pointer}.plan-link:hover{text-decoration:underline}.plan-link:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:var(--hm-border-thin)}.plan-key{color:var(--hm-muted);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-medium)}.plan-form{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--hm-space-sm)}.plan-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,12rem),1fr));gap:var(--hm-space-sm)}.plan-footer{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.plan-footer__start{margin-inline-end:auto}.plan-table-wrap{overflow-x:auto}.plan-table{inline-size:100%;border-collapse:collapse;font-size:var(--hm-font-size-sm);font-variant-numeric:tabular-nums}.plan-table caption{padding-block-end:var(--hm-space-xs);color:var(--hm-muted);text-align:start}.plan-table th,.plan-table td{padding:var(--hm-space-nano) var(--hm-space-xs);border-block-end:var(--hm-border-hairline) solid var(--hm-border);text-align:end;white-space:nowrap}.plan-table th:first-child,.plan-table td:first-child{text-align:start}\n"], dependencies: [{ kind: "component", type: AssigneeSelectComponent, selector: "hm-assignee-select", inputs: ["people", "value", "label", "hint", "error", "allowUnassigned", "me", "disabled", "required", "texts"], outputs: ["valueChange", "assign"] }, { 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: ComboboxComponent, selector: "hm-combobox", inputs: ["label", "options", "value", "placeholder", "icon", "hint", "size", "labelHidden", "clearable", "loading", "allowCustomValue"], outputs: ["valueChange"] }, { 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: LevelSelectComponent, selector: "hm-level-select", inputs: ["scale", "value", "label", "hint", "appearance", "allowEmpty", "disabled", "texts"], outputs: ["valueChange"] }, { kind: "component", type: ModalComponent, selector: "hm-modal", inputs: ["open", "heading", "size", "closeLabel", "closable", "beforeClose"], outputs: ["openChange", "closed"] }, { kind: "component", type: PopconfirmComponent, selector: "hm-popconfirm", inputs: ["open", "heading", "headingLevel", "message", "confirmLabel", "cancelLabel", "intent", "side", "align", "texts"], outputs: ["openChange", "confirmed", "cancelled"] }, { kind: "directive", type: PopconfirmTriggerDirective, selector: "[hmPopconfirmTrigger]", inputs: ["hmPopconfirmTrigger"] }, { kind: "component", type: SelectComponent, selector: "hm-select", inputs: ["value", "label", "labelHidden", "hint"], outputs: ["valueChange"] }, { kind: "component", type: TextareaComponent, selector: "hm-textarea", inputs: ["value", "label", "hint", "placeholder", "maxLength", "rows", "autoGrow", "labelHidden", "loading", "maxRows", "submitKey", "mentionCandidates", "mentionTrigger", "texts"], outputs: ["valueChange", "submitted", "mentioned"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
879
+ }
880
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionRiskFormComponent, decorators: [{
881
+ type: Component,
882
+ args: [{ selector: 'spine-suggestion-risk-form', imports: [
883
+ AssigneeSelectComponent, ButtonComponent, ComboboxComponent, InputComponent, LevelSelectComponent, ModalComponent,
884
+ PopconfirmComponent, PopconfirmTriggerDirective, SelectComponent, TextareaComponent,
885
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: `
886
+ <hm-modal [heading]="draft().id ? p.risks.editHeading : p.risks.newHeading" size="large" [(open)]="open">
887
+ <form class="plan-form" (submit)="$event.preventDefault(); save()" novalidate>
888
+ <hm-input [label]="p.risks.title" required [maxLength]="200" [value]="draft().title" (valueChange)="patch({ title: $any($event) })"
889
+ [error]="submitted() && !draft().title.trim() ? p.risks.titleRequired : ''" />
890
+ @if (task(); as fixed) {
891
+ <p class="plan-muted">{{ p.risks.task }}: <strong>{{ fixed.title }}</strong></p>
892
+ } @else {
893
+ <hm-combobox [label]="p.risks.task" required [placeholder]="p.risks.taskPlaceholder" [options]="taskOptions()"
894
+ [value]="draft().suggestionId ?? ''" (valueChange)="patch({ suggestionId: $event || null })"
895
+ [error]="submitted() && !draft().suggestionId ? p.risks.taskRequired : ''" />
896
+ }
897
+ <hm-level-select scale="likelihood" appearance="segmented" [value]="likelihood()" (valueChange)="patch({ likelihood: level(likelihoodValues, $event) })" />
898
+ <hm-level-select scale="impact" appearance="segmented" [value]="impact()" (valueChange)="patch({ impact: level(impactValues, $event) })" />
899
+ <div class="plan-row">
900
+ <hm-assignee-select [label]="p.risks.owner" [people]="s.people()" [me]="s.me()" [value]="draft().ownerId"
901
+ (valueChange)="patch({ ownerId: $event || null })" />
902
+ <hm-select [label]="p.risks.status" [value]="draft().status" (valueChange)="patch({ status: $any($event) })">
903
+ @for (st of statuses; track st) {
904
+ <option [value]="st" [selected]="st === draft().status">{{ p.risks.statuses[st] }}</option>
905
+ }
906
+ </hm-select>
907
+ </div>
908
+ <hm-textarea [label]="p.risks.mitigation" [rows]="3" autoGrow [maxLength]="2000" [value]="draft().mitigation" (valueChange)="patch({ mitigation: $event })" />
909
+ </form>
910
+ <div footer class="plan-footer">
911
+ @if (draft().id) {
912
+ <hm-button class="plan-footer__start" intent="danger" emphasis="minimal" icon="trash" [hmPopconfirmTrigger]="removeConfirm">{{ p.delete }}</hm-button>
913
+ <hm-popconfirm #removeConfirm [heading]="p.risks.deleteHeading" [confirmLabel]="p.delete" intent="danger" (confirmed)="remove()" />
914
+ }
915
+ <hm-button emphasis="minimal" (clicked)="open.set(false)">{{ p.cancel }}</hm-button>
916
+ <hm-button [loading]="saving()" (clicked)="save()">{{ p.save }}</hm-button>
917
+ </div>
918
+ </hm-modal>
919
+ `, styles: [":host{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--hm-space-md);min-inline-size:0}.plan-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,20rem),1fr));gap:var(--hm-space-md)}.plan-card{display:grid;grid-template-columns:minmax(0,1fr);align-content:start;gap:var(--hm-space-sm);min-inline-size:0;padding:var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.plan-card h3,.plan-card h4{margin:0;font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold);line-height:var(--hm-line-height-snug)}.plan-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.plan-toolbar__end{margin-inline-start:auto}.plan-muted{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-normal)}.plan-stats{display:flex;flex-wrap:wrap;gap:var(--hm-space-md);margin:0}.plan-stats div{display:grid;gap:var(--hm-space-nano);min-inline-size:0}.plan-stats dt{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.plan-stats dd{margin:0;font-size:var(--hm-font-size-lg);font-weight:var(--hm-font-weight-semibold)}.plan-text{margin:0;font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-snug)}.plan-title{font-weight:var(--hm-font-weight-medium)}.plan-rows{display:grid;gap:var(--hm-space-xs);margin:0;padding:0;font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-snug);list-style:none}.plan-rows li{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs) var(--hm-space-sm);min-inline-size:0}.plan-link{min-inline-size:0;padding:0;border:0;background:none;color:var(--hm-accent-strong);font:inherit;font-weight:var(--hm-font-weight-medium);text-align:start;cursor:pointer}.plan-link:hover{text-decoration:underline}.plan-link:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:var(--hm-border-thin)}.plan-key{color:var(--hm-muted);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-medium)}.plan-form{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--hm-space-sm)}.plan-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,12rem),1fr));gap:var(--hm-space-sm)}.plan-footer{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.plan-footer__start{margin-inline-end:auto}.plan-table-wrap{overflow-x:auto}.plan-table{inline-size:100%;border-collapse:collapse;font-size:var(--hm-font-size-sm);font-variant-numeric:tabular-nums}.plan-table caption{padding-block-end:var(--hm-space-xs);color:var(--hm-muted);text-align:start}.plan-table th,.plan-table td{padding:var(--hm-space-nano) var(--hm-space-xs);border-block-end:var(--hm-border-hairline) solid var(--hm-border);text-align:end;white-space:nowrap}.plan-table th:first-child,.plan-table td:first-child{text-align:start}\n"] }]
920
+ }], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], risk: [{ type: i0.Input, args: [{ isSignal: true, alias: "risk", required: false }] }], task: [{ type: i0.Input, args: [{ isSignal: true, alias: "task", required: false }] }] } });
921
+
922
+ const EVENTS_PAGE = 20;
923
+ /** A escala de pontos do seletor (a sequência usual de planejamento); vazio = sem estimativa. */
924
+ const POINT_SCALE = [1, 2, 3, 5, 8, 13, 21];
925
+ /** Copia para a área de transferência; false se o navegador recusar. */
926
+ async function copyText(value) {
927
+ try {
928
+ await navigator.clipboard.writeText(value);
929
+ return true;
930
+ }
931
+ catch {
932
+ return false;
933
+ }
934
+ }
935
+ /**
936
+ * A tarefa aberta (modal grande, duas colunas, como os quadros de tarefas conhecidos), montada pelo quadro.
937
+ *
938
+ * - **Esquerda**: título editável no lugar (Enter salva, Esc desiste), descrição em `hm-rich-text` (ler; "Editar"
939
+ * abre o editor e "Salvar" grava o HTML, que o servidor saneia), checklist (marcar, renomear, subir/descer, remover,
940
+ * "Mostrar no cartão") e as abas Comentários e Atividades (do mais novo, "Carregar mais"). Comentário é texto simples
941
+ * e pode ser interno (só a equipe vê; quem pediu não lê em "Minhas sugestões").
942
+ * - **Direita**: status, tipo, responsáveis ("Atribuir a mim") e envolvidos (busca de pessoas do realm, com foto),
943
+ * relator, prioridade, início e conclusão, rótulos (catálogo do app; cria na hora), capa, tela de origem ("Abrir a
944
+ * tela" quando o app administrado é o desta sessão), apoios, criada/atualizada e "Última alteração há X por Fulano";
945
+ * a chave com copiar; arquivar/restaurar e excluir (com confirmação).
946
+ * - **Agrupar**: "Relatores" (o autor e quem veio das agrupadas, com foto) no lugar do relator; "Agrupadas aqui" (chave,
947
+ * título e autor de cada origem, com link e "Desagrupar"); "Agrupar em…" pede o seletor ao quadro (`mergeRequested`).
948
+ * Na agrupada, o aviso "Agrupada em SUG-12" no topo, com "Abrir SUG-12" (`openTask`) e "Desagrupar" (`unmerged`).
949
+ * - **Nova tarefa** (`task` nulo, `draft`): título, tipo, coluna, prioridade, datas, descrição, rótulos e "Atribuir a
950
+ * mim"; criar troca para a tarefa completa.
951
+ *
952
+ * Toda mudança vai direto ao servidor, que devolve a tarefa inteira: ela vira a nova verdade aqui (`taskChange`) e no
953
+ * quadro (`changed`).
954
+ */
955
+ class SuggestionTaskDetailComponent {
956
+ openRisk(risk) {
957
+ this.riskEditing.set(risk);
958
+ this.riskOpen.set(true);
959
+ }
960
+ /** Faixa do risco (baixo, médio, alto, crítico) para o selo. */
961
+ riskLevelOf(r) {
962
+ return riskLevel(riskScore(r.likelihood, r.impact));
963
+ }
964
+ constructor() {
965
+ this.messages = inject(SPINE_MESSAGES);
966
+ this.text = this.messages.suggestions;
967
+ this.service = inject(AutoServiceSuggestionsService);
968
+ this.toast = inject(HermesToastService);
969
+ this.dialogs = inject(HermesDialogService);
970
+ this.preferences = inject(SpinePreferencesService);
971
+ this.userStorage = inject(SpineUserStorage, { optional: true });
972
+ this.router = inject(Router, { optional: true });
973
+ this.location = inject(Location, { optional: true });
974
+ /** O planejamento do quadro (sprints): só existe dentro dele. */
975
+ this.planning = inject(SuggestionPlanningStore, { optional: true });
976
+ this.clientId = input.required(...(ngDevMode ? [{ debugName: "clientId" }] : /* istanbul ignore next */ []));
977
+ /** A tarefa aberta (leve, do quadro: a completa chega do servidor). null + `draft` = nova tarefa. */
978
+ this.task = input(null, ...(ngDevMode ? [{ debugName: "task" }] : /* istanbul ignore next */ []));
979
+ this.draft = input(false, ...(ngDevMode ? [{ debugName: "draft" }] : /* istanbul ignore next */ []));
980
+ /** Coluna da nova tarefa. */
981
+ this.draftStatus = input('em_analise', ...(ngDevMode ? [{ debugName: "draftStatus" }] : /* istanbul ignore next */ []));
982
+ this.labels = input([], ...(ngDevMode ? [{ debugName: "labels" }] : /* istanbul ignore next */ []));
983
+ /** As tarefas que podem ser dependência desta (as do quadro). */
984
+ this.candidates = input([], ...(ngDevMode ? [{ debugName: "candidates" }] : /* istanbul ignore next */ []));
985
+ /** A tela da sugestão abre daqui (o app administrado é o desta sessão). */
986
+ this.canOpenScreen = input(false, ...(ngDevMode ? [{ debugName: "canOpenScreen" }] : /* istanbul ignore next */ []));
987
+ /** A tarefa mudou (o servidor devolveu a nova versão). */
988
+ this.changed = output();
989
+ this.created = output();
990
+ this.removed = output();
991
+ this.closed = output();
992
+ /** Um rótulo novo entrou no catálogo. */
993
+ this.labelCreated = output();
994
+ /** "Agrupar em…": o quadro fecha este modal e abre o seletor. */
995
+ this.mergeRequested = output();
996
+ /** Abrir outra tarefa pelo id (o destino de uma agrupada, uma das agrupadas). */
997
+ this.openTask = output();
998
+ /** Desagrupou (daqui): o quadro relê, a origem volta para ele. */
999
+ this.unmerged = output();
1000
+ this.statuses = SPINE_SUGGESTION_STATUSES;
1001
+ this.statusColors = SPINE_SUGGESTION_STATUS_COLORS;
1002
+ this.waitingValues = SPINE_SUGGESTION_WAITING;
1003
+ this.waitingColors = SPINE_SUGGESTION_WAITING_COLORS;
1004
+ this.kinds = SPINE_SUGGESTION_KINDS;
1005
+ this.kindColors = SPINE_SUGGESTION_KIND_COLORS;
1006
+ this.priorities = TASK_PRIORITIES;
1007
+ this.priorityIcons = TASK_PRIORITY_ICONS;
1008
+ this.limits = SPINE_SUGGESTION_LIMITS;
1009
+ this.today = todayIso(this.preferences.effectiveTimeZone());
1010
+ this.current = signal(null, ...(ngDevMode ? [{ debugName: "current" }] : /* istanbul ignore next */ []));
1011
+ this.loadError = signal('', ...(ngDevMode ? [{ debugName: "loadError" }] : /* istanbul ignore next */ []));
1012
+ this.busy = signal(false, ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
1013
+ this.open = computed(() => !!this.current() || this.draft(), ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
1014
+ // --- planejamento: pontos, sprint e dependências ---
1015
+ this.pointScale = POINT_SCALE;
1016
+ this.plan = this.messages.suggestions.planning;
1017
+ /** As sprints que aceitam tarefa (não encerradas), mais a da tarefa se ela já estiver numa encerrada. */
1018
+ this.sprintOptions = computed(() => {
1019
+ const current = this.current()?.sprint_id ?? null;
1020
+ return (this.planning?.sprints() ?? []).filter((s) => s.state !== 'closed' || s.id === current);
1021
+ }, ...(ngDevMode ? [{ debugName: "sprintOptions" }] : /* istanbul ignore next */ []));
1022
+ this.dependencyOptions = computed(() => {
1023
+ const self = this.current()?.id;
1024
+ return this.candidates().filter((c) => c.id !== self && !c.merged_into).map((c) => ({ value: c.id, label: `${c.key} ${c.title}` }));
1025
+ }, ...(ngDevMode ? [{ debugName: "dependencyOptions" }] : /* istanbul ignore next */ []));
1026
+ /** As abertas de que esta depende: "Bloqueada por SUG-3, SUG-7". */
1027
+ this.blockedBy = computed(() => {
1028
+ const ids = new Set(this.current()?.depends_on ?? []);
1029
+ const keys = this.candidates().filter((c) => ids.has(c.id) && c.status !== 'concluida' && c.status !== 'recusada').map((c) => c.key);
1030
+ return keys.length ? formatMessage(this.plan.detail.blockedBy, { list: keys.join(', ') }) : '';
1031
+ }, ...(ngDevMode ? [{ debugName: "blockedBy" }] : /* istanbul ignore next */ []));
1032
+ this.heading = computed(() => {
1033
+ const t = this.current();
1034
+ return t ? `${t.key} · ${t.title}` : this.text.newTaskHeading;
1035
+ }, ...(ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []));
1036
+ // título e descrição
1037
+ this.editingTitle = signal(false, ...(ngDevMode ? [{ debugName: "editingTitle" }] : /* istanbul ignore next */ []));
1038
+ this.titleDraft = signal('', ...(ngDevMode ? [{ debugName: "titleDraft" }] : /* istanbul ignore next */ []));
1039
+ this.editingDescription = signal(false, ...(ngDevMode ? [{ debugName: "editingDescription" }] : /* istanbul ignore next */ []));
1040
+ this.descriptionDraft = signal('', ...(ngDevMode ? [{ debugName: "descriptionDraft" }] : /* istanbul ignore next */ []));
1041
+ // checklist
1042
+ this.newItem = signal('', ...(ngDevMode ? [{ debugName: "newItem" }] : /* istanbul ignore next */ []));
1043
+ this.renaming = signal(null, ...(ngDevMode ? [{ debugName: "renaming" }] : /* istanbul ignore next */ []));
1044
+ this.renameDraft = signal('', ...(ngDevMode ? [{ debugName: "renameDraft" }] : /* istanbul ignore next */ []));
1045
+ // comentários e atividades
1046
+ /** Resumo (descrição e checklist), Comentários (a conversa com quem pediu) e Histórico (tudo o que mudou). */
1047
+ this.tab = signal('summary', ...(ngDevMode ? [{ debugName: "tab" }] : /* istanbul ignore next */ []));
1048
+ this.comments = signal([], ...(ngDevMode ? [{ debugName: "comments" }] : /* istanbul ignore next */ []));
1049
+ this.commentsTotal = signal(0, ...(ngDevMode ? [{ debugName: "commentsTotal" }] : /* istanbul ignore next */ []));
1050
+ this.activity = signal([], ...(ngDevMode ? [{ debugName: "activity" }] : /* istanbul ignore next */ []));
1051
+ this.activityTotal = signal(0, ...(ngDevMode ? [{ debugName: "activityTotal" }] : /* istanbul ignore next */ []));
1052
+ this.eventsLoading = signal(false, ...(ngDevMode ? [{ debugName: "eventsLoading" }] : /* istanbul ignore next */ []));
1053
+ this.chat = viewChild(SpineSuggestionChatComponent, ...(ngDevMode ? [{ debugName: "chat" }] : /* istanbul ignore next */ []));
1054
+ /**
1055
+ * A aba Comentários como chat (do mais antigo ao mais novo; a lista vem do mais novo). À direita, o que a pessoa
1056
+ * escreveu como equipe; o que ela escreveu como quem pede (pela página) fica à esquerda, como os outros.
1057
+ */
1058
+ this.chatItems = computed(() => [...this.comments()].reverse().map((e) => ({
1059
+ id: e.id, text: e.comment ?? '', created_at: e.created_at, author: e.actor, from_requester: !!e.from_requester,
1060
+ internal: !!e.internal, status: e.kind === 'status' && e.to_status !== 'nova' ? e.to_status : null,
1061
+ })), ...(ngDevMode ? [{ debugName: "chatItems" }] : /* istanbul ignore next */ []));
1062
+ /** As abas de eventos já lidas desta tarefa (vazia também conta: trocar de aba não relê). */
1063
+ this.loadedTabs = new Set();
1064
+ this.comment = signal('', ...(ngDevMode ? [{ debugName: "comment" }] : /* istanbul ignore next */ []));
1065
+ this.commentInternal = signal(false, ...(ngDevMode ? [{ debugName: "commentInternal" }] : /* istanbul ignore next */ []));
1066
+ this.commentError = signal('', ...(ngDevMode ? [{ debugName: "commentError" }] : /* istanbul ignore next */ []));
1067
+ this.commenting = signal(false, ...(ngDevMode ? [{ debugName: "commenting" }] : /* istanbul ignore next */ []));
1068
+ // nova tarefa
1069
+ this.draftTitle = signal('', ...(ngDevMode ? [{ debugName: "draftTitle" }] : /* istanbul ignore next */ []));
1070
+ this.draftTitleError = signal('', ...(ngDevMode ? [{ debugName: "draftTitleError" }] : /* istanbul ignore next */ []));
1071
+ this.draftKind = signal('melhoria', ...(ngDevMode ? [{ debugName: "draftKind" }] : /* istanbul ignore next */ []));
1072
+ this.draftColumn = signal('em_analise', ...(ngDevMode ? [{ debugName: "draftColumn" }] : /* istanbul ignore next */ []));
1073
+ this.draftPriority = signal('media', ...(ngDevMode ? [{ debugName: "draftPriority" }] : /* istanbul ignore next */ []));
1074
+ this.draftStart = signal(null, ...(ngDevMode ? [{ debugName: "draftStart" }] : /* istanbul ignore next */ []));
1075
+ this.draftDue = signal(null, ...(ngDevMode ? [{ debugName: "draftDue" }] : /* istanbul ignore next */ []));
1076
+ this.draftDescription = signal('', ...(ngDevMode ? [{ debugName: "draftDescription" }] : /* istanbul ignore next */ []));
1077
+ this.draftLabels = signal([], ...(ngDevMode ? [{ debugName: "draftLabels" }] : /* istanbul ignore next */ []));
1078
+ this.draftAssignMe = signal(true, ...(ngDevMode ? [{ debugName: "draftAssignMe" }] : /* istanbul ignore next */ []));
1079
+ this.selectedLabels = computed(() => this.current()?.labels.map((l) => l.id) ?? this.draftLabels(), ...(ngDevMode ? [{ debugName: "selectedLabels" }] : /* istanbul ignore next */ []));
1080
+ this.assigneeIds = computed(() => this.current()?.assignees.map((p) => p.user_id) ?? [], ...(ngDevMode ? [{ debugName: "assigneeIds" }] : /* istanbul ignore next */ []));
1081
+ this.watcherIds = computed(() => this.current()?.watchers.map((p) => p.user_id) ?? [], ...(ngDevMode ? [{ debugName: "watcherIds" }] : /* istanbul ignore next */ []));
1082
+ this.checklistItems = computed(() => [...(this.current()?.checklist.items ?? [])].sort((a, b) => a.position - b.position), ...(ngDevMode ? [{ debugName: "checklistItems" }] : /* istanbul ignore next */ []));
1083
+ this.dueSituation = computed(() => {
1084
+ const t = this.current();
1085
+ return t ? dueState(t, this.today) : null;
1086
+ }, ...(ngDevMode ? [{ debugName: "dueSituation" }] : /* istanbul ignore next */ []));
1087
+ this.isMine = computed(() => {
1088
+ const me = this.me();
1089
+ return !!me && this.assigneeIds().includes(me);
1090
+ }, ...(ngDevMode ? [{ debugName: "isMine" }] : /* istanbul ignore next */ []));
1091
+ // --- riscos da tarefa ---
1092
+ /** A tarefa aberta, para o formulário de risco (só depois de criada: risco novo precisa da tarefa gravada). */
1093
+ this.riskTask = computed(() => {
1094
+ const t = this.current();
1095
+ return t?.id ? { id: t.id, title: t.title } : null;
1096
+ }, { ...(ngDevMode ? { debugName: "riskTask" } : /* istanbul ignore next */ {}), equal: (a, b) => a?.id === b?.id && a?.title === b?.title });
1097
+ this.taskRisks = computed(() => {
1098
+ const id = this.riskTask()?.id;
1099
+ return id && this.planning ? this.planning.risksOf(id) : [];
1100
+ }, ...(ngDevMode ? [{ debugName: "taskRisks" }] : /* istanbul ignore next */ []));
1101
+ this.riskOpen = signal(false, ...(ngDevMode ? [{ debugName: "riskOpen" }] : /* istanbul ignore next */ []));
1102
+ this.riskEditing = signal(null, ...(ngDevMode ? [{ debugName: "riskEditing" }] : /* istanbul ignore next */ []));
1103
+ // A sprint e os riscos da tarefa saem do planejamento: lê uma vez quando o detalhe abre, se ninguém leu ainda.
1104
+ effect(() => {
1105
+ if (!this.open() || !this.planning)
1106
+ return;
1107
+ if (!untracked(() => this.planning.sprintsLoaded()))
1108
+ void this.planning.refreshSprints();
1109
+ if (!untracked(() => this.planning.risksLoaded()))
1110
+ void this.planning.ensureRisks();
1111
+ });
1112
+ effect(() => {
1113
+ const task = this.task();
1114
+ const draft = this.draft();
1115
+ untracked(() => {
1116
+ this.reset();
1117
+ if (task)
1118
+ void this.load(task);
1119
+ else if (draft)
1120
+ this.startDraft();
1121
+ });
1122
+ });
1123
+ }
1124
+ me() {
1125
+ return this.userStorage?.userId() ?? '';
1126
+ }
1127
+ reset() {
1128
+ this.current.set(null);
1129
+ this.loadError.set('');
1130
+ this.editingTitle.set(false);
1131
+ this.editingDescription.set(false);
1132
+ this.renaming.set(null);
1133
+ this.newItem.set('');
1134
+ this.comments.set([]);
1135
+ this.activity.set([]);
1136
+ this.commentsTotal.set(0);
1137
+ this.activityTotal.set(0);
1138
+ this.comment.set('');
1139
+ this.commentInternal.set(false);
1140
+ this.commentError.set('');
1141
+ this.tab.set('summary');
1142
+ this.loadedTabs.clear();
1143
+ }
1144
+ startDraft() {
1145
+ this.draftTitle.set('');
1146
+ this.draftTitleError.set('');
1147
+ this.draftKind.set('melhoria');
1148
+ this.draftColumn.set(this.draftStatus());
1149
+ this.draftPriority.set('media');
1150
+ this.draftStart.set(null);
1151
+ this.draftDue.set(null);
1152
+ this.draftDescription.set('');
1153
+ this.draftLabels.set([]);
1154
+ this.draftAssignMe.set(true);
1155
+ }
1156
+ async load(light) {
1157
+ this.current.set(light);
1158
+ try {
1159
+ const full = await this.service.task(this.clientId(), light.id);
1160
+ if (this.task()?.id !== light.id)
1161
+ return;
1162
+ this.current.set({ ...full, author: full.author ?? light.author });
1163
+ void this.loadEvents('comments', true);
1164
+ if (full.unread || light.unread)
1165
+ this.markRead({ ...full, author: full.author ?? light.author });
1166
+ }
1167
+ catch {
1168
+ this.loadError.set(this.text.loadError);
1169
+ }
1170
+ }
1171
+ setOpen(open) {
1172
+ if (!open)
1173
+ this.closed.emit();
1174
+ }
1175
+ /** Aplica a versão do servidor aqui e avisa o quadro. */
1176
+ apply(task) {
1177
+ const prev = this.current();
1178
+ const next = { ...task, author: task.author ?? prev?.author ?? null };
1179
+ this.current.set(next);
1180
+ this.changed.emit(next);
1181
+ }
1182
+ /** Roda uma mudança no servidor; erro = aviso no canto. */
1183
+ async run(call, success) {
1184
+ if (this.busy())
1185
+ return false;
1186
+ this.busy.set(true);
1187
+ try {
1188
+ this.apply(await call());
1189
+ if (success)
1190
+ this.toast.success(success);
1191
+ if (this.tab() === 'activity')
1192
+ void this.loadEvents('activity', true);
1193
+ return true;
1194
+ }
1195
+ catch (err) {
1196
+ const cycle = err instanceof HttpErrorResponse && err.error?.error === 'dependency_cycle';
1197
+ this.toast.error(cycle ? this.plan.detail.dependencyCycle : this.text.saveError);
1198
+ return false;
1199
+ }
1200
+ finally {
1201
+ this.busy.set(false);
1202
+ }
1203
+ }
1204
+ changeEstimate(value) {
1205
+ const estimate = value === '' ? null : Number(value);
1206
+ if (estimate !== (this.current()?.estimate ?? null))
1207
+ void this.edit({ estimate });
1208
+ }
1209
+ changeSprint(value) {
1210
+ const sprintId = value || null;
1211
+ if (sprintId !== (this.current()?.sprint_id ?? null))
1212
+ void this.edit({ sprint_id: sprintId });
1213
+ }
1214
+ /** O combobox devolve a lista inteira: liga as que entraram e desliga as que saíram, uma por vez. */
1215
+ async changeDependencies(next) {
1216
+ const t = this.current();
1217
+ if (!t)
1218
+ return;
1219
+ const before = new Set(t.depends_on ?? []);
1220
+ const after = new Set(next);
1221
+ for (const id of after)
1222
+ if (!before.has(id))
1223
+ await this.run(() => this.service.setDependency(this.clientId(), t.id, id, true));
1224
+ for (const id of before)
1225
+ if (!after.has(id))
1226
+ await this.run(() => this.service.setDependency(this.clientId(), t.id, id, false));
1227
+ }
1228
+ edit(patch) {
1229
+ const t = this.current();
1230
+ return t ? this.run(() => this.service.update(this.clientId(), t.id, patch)) : Promise.resolve(false);
1231
+ }
1232
+ // --- título e descrição ---
1233
+ startTitle() {
1234
+ this.titleDraft.set(this.current()?.title ?? '');
1235
+ this.editingTitle.set(true);
1236
+ }
1237
+ async saveTitle() {
1238
+ const title = this.titleDraft().trim();
1239
+ if (!title || title === this.current()?.title) {
1240
+ this.editingTitle.set(false);
1241
+ return;
1242
+ }
1243
+ if (await this.edit({ title }))
1244
+ this.editingTitle.set(false);
1245
+ }
1246
+ onTitleKey(event) {
1247
+ if (event.key === 'Enter') {
1248
+ event.preventDefault();
1249
+ void this.saveTitle();
1250
+ }
1251
+ else if (event.key === 'Escape') {
1252
+ event.stopPropagation();
1253
+ this.editingTitle.set(false);
1254
+ }
1255
+ }
1256
+ startDescription() {
1257
+ this.descriptionDraft.set(this.current()?.description_html ?? '');
1258
+ this.editingDescription.set(true);
1259
+ }
1260
+ async saveDescription() {
1261
+ if (await this.edit({ description_html: this.descriptionDraft() }))
1262
+ this.editingDescription.set(false);
1263
+ }
1264
+ // --- campos da direita ---
1265
+ /** Abrir a tarefa lê a conversa: zera o "N novas" (aqui e no quadro); falhar não atrapalha. */
1266
+ markRead(t) {
1267
+ const read = { ...t, unread: 0 };
1268
+ this.current.set(read);
1269
+ this.changed.emit(read);
1270
+ this.service.markRead(this.clientId(), t.id).catch(() => undefined);
1271
+ }
1272
+ /** Com quem está a vez (o servidor também muda sozinho: comentário público da equipe = quem pediu; resposta = equipe). */
1273
+ changeWaiting(value) {
1274
+ const t = this.current();
1275
+ const waiting = (value || null);
1276
+ if (!t || waiting === (t.waiting_on ?? null))
1277
+ return;
1278
+ void this.edit({ waiting_on: waiting }).then((ok) => { if (ok)
1279
+ this.toast.success(this.text.waitingSaved); });
1280
+ }
1281
+ /** Os rótulos na linha de selos, cada um com a cor do token ('cat-3' → 3). */
1282
+ labelTags(t) {
1283
+ return t.labels.map((l) => ({ id: l.id, name: l.name, color: Number(l.color.replace('cat-', '')) || undefined }));
1284
+ }
1285
+ unreadText(count) {
1286
+ return pluralMessage(this.text.unread, count, this.messages.meta.locale);
1287
+ }
1288
+ changeStatus(status) {
1289
+ const t = this.current();
1290
+ if (!t || status === t.status)
1291
+ return;
1292
+ void this.run(() => this.service.move(this.clientId(), t.id, { status: status }), this.text.statusSaved);
1293
+ }
1294
+ changeKind(kind) {
1295
+ if (kind !== this.current()?.kind)
1296
+ void this.edit({ kind: kind });
1297
+ }
1298
+ changePriority(priority) {
1299
+ if (priority !== this.current()?.priority)
1300
+ void this.edit({ priority: priority });
1301
+ }
1302
+ changeDate(field, value) {
1303
+ const t = this.current();
1304
+ if (!t || (value || null) === t[field])
1305
+ return;
1306
+ void this.edit({ [field]: value || null });
1307
+ }
1308
+ toggleChecklistOnCard(on) {
1309
+ if (on !== this.current()?.checklist_on_card)
1310
+ void this.edit({ checklist_on_card: on });
1311
+ }
1312
+ assignMe() {
1313
+ const t = this.current();
1314
+ if (t)
1315
+ void this.run(() => this.service.addPerson(this.clientId(), t.id, 'me', 'responsavel'), this.text.assignedToMe);
1316
+ }
1317
+ addPerson(person, role, popover) {
1318
+ popover.close();
1319
+ const t = this.current();
1320
+ if (t)
1321
+ void this.run(() => this.service.addPerson(this.clientId(), t.id, person.id, role));
1322
+ }
1323
+ removePerson(person, role) {
1324
+ const t = this.current();
1325
+ if (t)
1326
+ void this.run(() => this.service.removePerson(this.clientId(), t.id, person.user_id, role));
1327
+ }
1328
+ toggleLabel(label, linked) {
1329
+ const t = this.current();
1330
+ if (!t) {
1331
+ this.draftLabels.update((ids) => (linked ? [...new Set([...ids, label.id])] : ids.filter((id) => id !== label.id)));
1332
+ return;
1333
+ }
1334
+ void this.run(() => this.service.setLabel(this.clientId(), t.id, label.id, linked));
1335
+ }
1336
+ async createLabel(name, color) {
1337
+ try {
1338
+ const label = await this.service.createLabel(this.clientId(), name, color);
1339
+ const entry = { ...label, uses: 0 };
1340
+ this.labelCreated.emit(entry);
1341
+ this.toggleLabel(entry, true);
1342
+ }
1343
+ catch {
1344
+ this.toast.error(this.text.saveError);
1345
+ }
1346
+ }
1347
+ async copyKey() {
1348
+ const t = this.current();
1349
+ if (!t)
1350
+ return;
1351
+ if (await copyText(t.key))
1352
+ this.toast.success(this.text.keyCopied);
1353
+ else
1354
+ this.toast.error(this.text.copyError);
1355
+ }
1356
+ screenHref(path) {
1357
+ return this.location?.prepareExternalUrl(path) ?? path;
1358
+ }
1359
+ /** Clique simples navega no app (e fecha); com Ctrl/Cmd/Shift ou botão do meio, o navegador abre a aba. */
1360
+ openScreen(event, path) {
1361
+ if (!this.router || event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey)
1362
+ return;
1363
+ event.preventDefault();
1364
+ this.closed.emit();
1365
+ void this.router.navigateByUrl(path);
1366
+ }
1367
+ toggleArchived() {
1368
+ const t = this.current();
1369
+ if (!t)
1370
+ return;
1371
+ const archive = !t.archived_at;
1372
+ void this.run(() => this.service.setArchived(this.clientId(), t.id, archive), archive ? this.text.archivedToast : this.text.restoredToast);
1373
+ }
1374
+ async remove() {
1375
+ const t = this.current();
1376
+ if (!t)
1377
+ return;
1378
+ const ok = await this.dialogs.confirm({
1379
+ heading: formatMessage(this.text.deleteHeading, { key: t.key }), message: this.text.deleteMessage,
1380
+ confirmLabel: this.text.deleteConfirm, danger: true,
1381
+ });
1382
+ if (!ok)
1383
+ return;
1384
+ try {
1385
+ await this.service.remove(this.clientId(), t.id);
1386
+ this.toast.success(this.text.deleted);
1387
+ this.removed.emit(t);
1388
+ }
1389
+ catch {
1390
+ this.toast.error(this.text.saveError);
1391
+ }
1392
+ }
1393
+ // --- agrupar ---
1394
+ /** Desagrupa esta (a agrupada): ela volta ao quadro. */
1395
+ async unmergeSelf() {
1396
+ const t = this.current();
1397
+ if (!t?.merged_into || this.busy())
1398
+ return;
1399
+ if (await this.run(() => this.service.unmerge(this.clientId(), t.id), formatMessage(this.text.unmergedToast, { key: t.key }))) {
1400
+ this.unmerged.emit();
1401
+ }
1402
+ }
1403
+ /** Desagrupa uma das agrupadas nesta: relê esta (perde os relatores e apoios que vieram dela). */
1404
+ async unmergeOrigin(origin) {
1405
+ const t = this.current();
1406
+ if (!t)
1407
+ return;
1408
+ const ok = await this.run(async () => {
1409
+ await this.service.unmerge(this.clientId(), origin.id);
1410
+ return this.service.task(this.clientId(), t.id);
1411
+ }, formatMessage(this.text.unmergedToast, { key: origin.key }));
1412
+ if (ok)
1413
+ this.unmerged.emit();
1414
+ }
1415
+ /** Quem pediu: o autor e os relatores extras (do detalhe); sem a lista ainda, só o autor. */
1416
+ reportersOf(t) {
1417
+ if (t.reporters?.length)
1418
+ return t.reporters;
1419
+ return [{ ...(t.author ?? { user_id: '', username: null, name: null, picture: null }), role: 'author', from_suggestion_id: null }];
1420
+ }
1421
+ /** "Autor" ou "via SUG-30" (a agrupada de onde o relator veio). */
1422
+ reporterHint(t, r) {
1423
+ if (r.role === 'author')
1424
+ return this.text.reporterAuthor;
1425
+ const origin = t.merged?.find((m) => m.id === r.from_suggestion_id);
1426
+ return origin ? formatMessage(this.text.reporterVia, { key: origin.key }) : '';
1427
+ }
1428
+ mergedIntoText(key) {
1429
+ return formatMessage(this.text.mergedInto, { key });
1430
+ }
1431
+ openMergedText(key) {
1432
+ return formatMessage(this.text.openMerged, { key });
1433
+ }
1434
+ unmergeLabel(key) {
1435
+ return formatMessage(this.text.unmergeLabel, { key });
1436
+ }
1437
+ // --- checklist ---
1438
+ addItem() {
1439
+ const t = this.current();
1440
+ const text = this.newItem().trim();
1441
+ if (!t || !text)
1442
+ return;
1443
+ void this.run(() => this.service.addChecklistItem(this.clientId(), t.id, text)).then((ok) => ok && this.newItem.set(''));
1444
+ }
1445
+ onNewItemKey(event) {
1446
+ if (event.key === 'Enter') {
1447
+ event.preventDefault();
1448
+ this.addItem();
1449
+ }
1450
+ }
1451
+ toggleItem(item, done) {
1452
+ const t = this.current();
1453
+ if (t && done !== item.done)
1454
+ void this.run(() => this.service.updateChecklistItem(this.clientId(), t.id, item.id, { done }));
1455
+ }
1456
+ startRename(item) {
1457
+ this.renaming.set(item.id);
1458
+ this.renameDraft.set(item.text);
1459
+ }
1460
+ saveRename(item) {
1461
+ const text = this.renameDraft().trim();
1462
+ this.renaming.set(null);
1463
+ const t = this.current();
1464
+ if (t && text && text !== item.text)
1465
+ void this.run(() => this.service.updateChecklistItem(this.clientId(), t.id, item.id, { text }));
1466
+ }
1467
+ onRenameKey(event, item) {
1468
+ if (event.key === 'Enter') {
1469
+ event.preventDefault();
1470
+ this.saveRename(item);
1471
+ }
1472
+ else if (event.key === 'Escape') {
1473
+ event.stopPropagation();
1474
+ this.renaming.set(null);
1475
+ }
1476
+ }
1477
+ moveItem(item, delta) {
1478
+ const t = this.current();
1479
+ const index = this.checklistItems().findIndex((i) => i.id === item.id);
1480
+ const target = index + delta;
1481
+ if (!t || index < 0 || target < 0 || target >= this.checklistItems().length)
1482
+ return;
1483
+ void this.run(() => this.service.updateChecklistItem(this.clientId(), t.id, item.id, { position: target }));
1484
+ }
1485
+ removeItem(item) {
1486
+ const t = this.current();
1487
+ if (t)
1488
+ void this.run(() => this.service.removeChecklistItem(this.clientId(), t.id, item.id));
1489
+ }
1490
+ // --- comentários e atividades ---
1491
+ selectTab(tab) {
1492
+ const next = tab === 'activity' || tab === 'comments' ? tab : 'summary';
1493
+ this.tab.set(next);
1494
+ if (next === 'summary')
1495
+ return;
1496
+ if (!this.loadedTabs.has(next))
1497
+ void this.loadEvents(next, true);
1498
+ }
1499
+ async loadEvents(type, reset) {
1500
+ const t = this.current();
1501
+ if (!t)
1502
+ return;
1503
+ const list = type === 'comments' ? this.comments : this.activity;
1504
+ const total = type === 'comments' ? this.commentsTotal : this.activityTotal;
1505
+ this.eventsLoading.set(true);
1506
+ try {
1507
+ const res = await this.service.events(this.clientId(), t.id, { type, limit: EVENTS_PAGE, offset: reset ? 0 : list().length });
1508
+ if (this.current()?.id !== t.id)
1509
+ return;
1510
+ list.set(reset ? res.events : [...list(), ...res.events]);
1511
+ total.set(res.total);
1512
+ this.loadedTabs.add(type);
1513
+ }
1514
+ catch {
1515
+ this.loadError.set(this.text.loadError);
1516
+ }
1517
+ finally {
1518
+ this.eventsLoading.set(false);
1519
+ }
1520
+ }
1521
+ async sendComment(text) {
1522
+ const t = this.current();
1523
+ const comment = (text ?? this.comment()).trim();
1524
+ if (!t || this.commenting())
1525
+ return;
1526
+ this.commentError.set(comment ? '' : this.text.commentRequired);
1527
+ if (!comment)
1528
+ return;
1529
+ this.commenting.set(true);
1530
+ try {
1531
+ const event = await this.service.comment(this.clientId(), t.id, comment, this.commentInternal());
1532
+ this.comments.update((list) => [event, ...list]);
1533
+ this.commentsTotal.update((n) => n + 1);
1534
+ this.comment.set('');
1535
+ this.commentInternal.set(false);
1536
+ const updated = {
1537
+ ...t, comments_count: t.comments_count + 1,
1538
+ last_activity: { at: event.created_at, actor: event.actor },
1539
+ ...(event.internal ? {} : { last_comment: { comment: event.comment ?? comment, created_at: event.created_at } }),
1540
+ ...(event.internal || t.status === 'concluida' || t.status === 'recusada'
1541
+ ? {} : { waiting_on: 'solicitante', waiting_since: event.created_at }),
1542
+ };
1543
+ this.current.set(updated);
1544
+ this.changed.emit(updated);
1545
+ this.chat()?.scrollToEnd();
1546
+ }
1547
+ catch {
1548
+ this.commentError.set(this.text.commentError);
1549
+ }
1550
+ finally {
1551
+ this.commenting.set(false);
1552
+ }
1553
+ }
1554
+ // --- nova tarefa ---
1555
+ async create() {
1556
+ const title = this.draftTitle().trim();
1557
+ this.draftTitleError.set(title ? '' : this.text.titleRequired);
1558
+ if (!title || this.busy())
1559
+ return;
1560
+ this.busy.set(true);
1561
+ try {
1562
+ const task = await this.service.create(this.clientId(), {
1563
+ title, kind: this.draftKind(), status: this.draftColumn(),
1564
+ priority: this.draftPriority(), start_date: this.draftStart(), due_date: this.draftDue(),
1565
+ description_html: this.draftDescription(), label_ids: this.draftLabels(),
1566
+ people: this.draftAssignMe() ? [{ user_id: 'me', role: 'responsavel' }] : [],
1567
+ });
1568
+ this.toast.success(this.text.taskCreated);
1569
+ this.created.emit(task);
1570
+ }
1571
+ catch {
1572
+ this.toast.error(this.text.createError);
1573
+ }
1574
+ finally {
1575
+ this.busy.set(false);
1576
+ }
1577
+ }
1578
+ // --- textos ---
1579
+ personName(p) {
1580
+ return p?.name || p?.username || this.text.removedAuthor;
1581
+ }
1582
+ statusLabel(status) {
1583
+ if (!status)
1584
+ return '—';
1585
+ return this.text.statuses[status] ?? this.text.legacyStatuses[status] ?? status;
1586
+ }
1587
+ /** O valor de um evento 'change' legível: prioridade e tipo pelo catálogo, data no formato local. */
1588
+ valueText(field, value) {
1589
+ if (!value)
1590
+ return '—';
1591
+ if (field === 'priority')
1592
+ return this.text.priorities[value] ?? value;
1593
+ if (field === 'kind')
1594
+ return this.text.kinds[value] ?? value;
1595
+ if (field === 'start_date' || field === 'due_date')
1596
+ return this.day(value);
1597
+ return value;
1598
+ }
1599
+ eventText(e) {
1600
+ const actor = e.actor ? this.personName(e.actor) : this.text.someone;
1601
+ if (e.kind === 'comment')
1602
+ return formatMessage(this.text.eventComment, { actor });
1603
+ if (e.kind === 'status')
1604
+ return formatMessage(this.text.eventStatus, { actor, from: this.statusLabel(e.from_status), to: this.statusLabel(e.to_status) });
1605
+ const field = e.field ?? 'other';
1606
+ const removed = ['start_date', 'due_date', 'cover'].includes(field) && !e.to_value ? `${field}_removed` : field;
1607
+ const template = this.text.activity[removed] ?? this.text.activity.other;
1608
+ return formatMessage(template, { actor, from: this.valueText(e.field, e.from_value), to: this.valueText(e.field, e.to_value) });
1609
+ }
1610
+ lastChange(t) {
1611
+ if (!t.last_activity)
1612
+ return '';
1613
+ const when = this.age(t.last_activity.at);
1614
+ return t.last_activity.actor
1615
+ ? formatMessage(this.text.lastChange, { when, actor: this.personName(t.last_activity.actor) })
1616
+ : formatMessage(this.text.lastChangeNoActor, { when });
1617
+ }
1618
+ dueText(t) {
1619
+ if (!t.due_date)
1620
+ return '';
1621
+ const s = this.dueSituation();
1622
+ if (s === 'overdue')
1623
+ return formatMessage(this.text.dueLate, { date: this.day(t.due_date) });
1624
+ if (s === 'today')
1625
+ return this.text.dueToday;
1626
+ if (s === 'tomorrow')
1627
+ return this.text.dueTomorrow;
1628
+ return formatMessage(this.text.dueOn, { date: this.day(t.due_date) });
1629
+ }
1630
+ removePersonLabel(p) {
1631
+ return formatMessage(this.text.removePerson, { name: this.personName(p) });
1632
+ }
1633
+ itemLabel(template, item) {
1634
+ return formatMessage(template, { text: item.text });
1635
+ }
1636
+ age(iso) {
1637
+ const seconds = Math.round((Date.now() - new Date(iso).getTime()) / 1000);
1638
+ const units = [['year', 31_536_000], ['month', 2_592_000], ['week', 604_800], ['day', 86_400], ['hour', 3_600], ['minute', 60]];
1639
+ try {
1640
+ const rtf = new Intl.RelativeTimeFormat(this.messages.meta.locale, { numeric: 'auto' });
1641
+ for (const [unit, size] of units)
1642
+ if (Math.abs(seconds) >= size)
1643
+ return rtf.format(-Math.floor(seconds / size), unit);
1644
+ return rtf.format(0, 'minute');
1645
+ }
1646
+ catch {
1647
+ return this.date(iso);
1648
+ }
1649
+ }
1650
+ date(iso, withTime = false) {
1651
+ try {
1652
+ return new Intl.DateTimeFormat(this.messages.meta.locale, {
1653
+ dateStyle: 'short', ...(withTime ? { timeStyle: 'short' } : {}), timeZone: this.preferences.effectiveTimeZone(),
1654
+ }).format(new Date(iso));
1655
+ }
1656
+ catch {
1657
+ return iso.slice(0, 10);
1658
+ }
1659
+ }
1660
+ /** Data sem hora (AAAA-MM-DD), sem fuso: o dia é o dia. */
1661
+ day(iso) {
1662
+ try {
1663
+ return new Intl.DateTimeFormat(this.messages.meta.locale, { dateStyle: 'short', timeZone: 'UTC' }).format(new Date(`${iso}T00:00:00Z`));
1664
+ }
1665
+ catch {
1666
+ return iso;
1667
+ }
1668
+ }
1669
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionTaskDetailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1670
+ 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 }, candidates: { classPropertyName: "candidates", publicName: "candidates", 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 <!-- Planejamento: pontos, sprint e as tarefas de que esta depende (Cronograma e Planejamento leem daqui). -->\n <div class=\"as-field__dates\">\n <hm-select [label]=\"plan.detail.estimate\" [value]=\"t.estimate === null || t.estimate === undefined ? '' : '' + t.estimate\" [disabled]=\"busy()\"\n (valueChange)=\"changeEstimate($any($event))\">\n <option value=\"\">{{ plan.noEstimate }}</option>\n @for (n of pointScale; track n) { <option [value]=\"'' + n\">{{ n }}</option> }\n @if (t.estimate !== null && t.estimate !== undefined && !pointScale.includes($any(t.estimate))) {\n <option [value]=\"'' + t.estimate\">{{ t.estimate }}</option>\n }\n </hm-select>\n <hm-select [label]=\"plan.detail.sprint\" [value]=\"t.sprint_id ?? ''\" [disabled]=\"busy()\" (valueChange)=\"changeSprint($any($event))\">\n <option value=\"\">{{ plan.detail.backlogOption }}</option>\n @for (sp of sprintOptions(); track sp.id) { <option [value]=\"sp.id\">{{ sp.name }}</option> }\n </hm-select>\n </div>\n <hm-combobox-multi [label]=\"plan.detail.dependsOn\" [placeholder]=\"plan.detail.dependsOnPlaceholder\" [options]=\"dependencyOptions()\"\n [selected]=\"t.depends_on ?? []\" [visibleChipsLimit]=\"null\" (selectedChange)=\"changeDependencies($event)\" />\n @if (blockedBy()) {\n <span class=\"as-due\" data-due=\"today\"><hm-icon shape=\"link\" />{{ blockedBy() }}</span>\n }\n\n <!-- Riscos da tarefa: todo risco \u00E9 de uma tarefa; a nota de risco dela \u00E9 a maior entre os abertos. -->\n @if (planning && riskTask(); as rt) {\n <div class=\"as-field\">\n <div class=\"as-field__head\">\n <span class=\"as-field__label\">{{ plan.risks.ofTask }}</span>\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"plus\" [label]=\"plan.risks.add\" (clicked)=\"openRisk(null)\" />\n </div>\n @if (taskRisks().length) {\n <ul class=\"as-risks\">\n @for (r of taskRisks(); track r.id) {\n <li>\n <button type=\"button\" class=\"as-risks__item\" (click)=\"openRisk(r)\">\n <hm-level-badge scale=\"risk\" [value]=\"riskLevelOf(r)\" />\n <span class=\"as-risks__title\" [class.as-risks__title--closed]=\"r.status === 'closed'\">{{ r.title }}</span>\n <span class=\"as-risks__status\">{{ plan.risks.statuses[r.status] }}</span>\n </button>\n </li>\n }\n </ul>\n } @else {\n <p class=\"as-field__hint\">{{ plan.risks.empty }}</p>\n }\n </div>\n <spine-suggestion-risk-form [(open)]=\"riskOpen\" [risk]=\"riskEditing()\" [task]=\"rt\" />\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: ["@charset \"UTF-8\";.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);font-size:var(--hm-font-size-sm)}.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}.as-risks{display:grid;gap:var(--hm-space-micro);margin:0;padding:0;list-style:none}.as-risks__item{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);background:transparent;color:var(--hm-text);font:inherit;font-size:var(--hm-font-size-sm);text-align:start;cursor:pointer}.as-risks__item:hover{background:var(--hm-hover)}.as-risks__item:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:calc(var(--hm-border-thin) * -1)}.as-risks__title{flex:1;min-inline-size:0;overflow-wrap:anywhere}.as-risks__title--closed{color:var(--hm-muted);text-decoration:line-through}.as-risks__status{flex:none;color:var(--hm-muted);font-size:var(--hm-font-size-xs)}\n"], dependencies: [{ kind: "component", type: AccountAvatarComponent, selector: "hm-account-avatar", inputs: ["name", "imageUrl", "size", "loading", "presence", "texts"] }, { 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: ComboboxMultiComponent, selector: "hm-combobox-multi", inputs: ["label", "labelHidden", "options", "selected", "extraOptions", "unknownLabel", "placeholder", "hint", "max", "visibleChipsLimit"], outputs: ["selectedChange"] }, { 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: LevelBadgeComponent, selector: "hm-level-badge", inputs: ["scale", "value", "compact", "showScale", "texts"] }, { 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", "describedBy", "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: SuggestionRiskFormComponent, selector: "spine-suggestion-risk-form", inputs: ["open", "risk", "task"], outputs: ["openChange"] }, { 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 }); }
1671
+ }
1672
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionTaskDetailComponent, decorators: [{
1673
+ type: Component,
1674
+ args: [{ selector: 'spine-suggestion-task-detail', imports: [
1675
+ AccountAvatarComponent, AlertComponent, ButtonComponent, CheckboxComponent, ComboboxMultiComponent, DatePickerComponent, IconButtonComponent,
1676
+ IconComponent, InputComponent, LevelBadgeComponent, ModalComponent, PopoverComponent, PopoverTriggerDirective, RichTextComponent,
1677
+ SelectComponent, SuggestionLabelPickerComponent, SuggestionPeoplePickerComponent, SuggestionRiskFormComponent,
1678
+ SpineSuggestionChatComponent, SpineTagRowComponent, SwitchComponent, TabComponent, TabsComponent, TagComponent, TextareaComponent,
1679
+ ], 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 <!-- Planejamento: pontos, sprint e as tarefas de que esta depende (Cronograma e Planejamento leem daqui). -->\n <div class=\"as-field__dates\">\n <hm-select [label]=\"plan.detail.estimate\" [value]=\"t.estimate === null || t.estimate === undefined ? '' : '' + t.estimate\" [disabled]=\"busy()\"\n (valueChange)=\"changeEstimate($any($event))\">\n <option value=\"\">{{ plan.noEstimate }}</option>\n @for (n of pointScale; track n) { <option [value]=\"'' + n\">{{ n }}</option> }\n @if (t.estimate !== null && t.estimate !== undefined && !pointScale.includes($any(t.estimate))) {\n <option [value]=\"'' + t.estimate\">{{ t.estimate }}</option>\n }\n </hm-select>\n <hm-select [label]=\"plan.detail.sprint\" [value]=\"t.sprint_id ?? ''\" [disabled]=\"busy()\" (valueChange)=\"changeSprint($any($event))\">\n <option value=\"\">{{ plan.detail.backlogOption }}</option>\n @for (sp of sprintOptions(); track sp.id) { <option [value]=\"sp.id\">{{ sp.name }}</option> }\n </hm-select>\n </div>\n <hm-combobox-multi [label]=\"plan.detail.dependsOn\" [placeholder]=\"plan.detail.dependsOnPlaceholder\" [options]=\"dependencyOptions()\"\n [selected]=\"t.depends_on ?? []\" [visibleChipsLimit]=\"null\" (selectedChange)=\"changeDependencies($event)\" />\n @if (blockedBy()) {\n <span class=\"as-due\" data-due=\"today\"><hm-icon shape=\"link\" />{{ blockedBy() }}</span>\n }\n\n <!-- Riscos da tarefa: todo risco \u00E9 de uma tarefa; a nota de risco dela \u00E9 a maior entre os abertos. -->\n @if (planning && riskTask(); as rt) {\n <div class=\"as-field\">\n <div class=\"as-field__head\">\n <span class=\"as-field__label\">{{ plan.risks.ofTask }}</span>\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"plus\" [label]=\"plan.risks.add\" (clicked)=\"openRisk(null)\" />\n </div>\n @if (taskRisks().length) {\n <ul class=\"as-risks\">\n @for (r of taskRisks(); track r.id) {\n <li>\n <button type=\"button\" class=\"as-risks__item\" (click)=\"openRisk(r)\">\n <hm-level-badge scale=\"risk\" [value]=\"riskLevelOf(r)\" />\n <span class=\"as-risks__title\" [class.as-risks__title--closed]=\"r.status === 'closed'\">{{ r.title }}</span>\n <span class=\"as-risks__status\">{{ plan.risks.statuses[r.status] }}</span>\n </button>\n </li>\n }\n </ul>\n } @else {\n <p class=\"as-field__hint\">{{ plan.risks.empty }}</p>\n }\n </div>\n <spine-suggestion-risk-form [(open)]=\"riskOpen\" [risk]=\"riskEditing()\" [task]=\"rt\" />\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: ["@charset \"UTF-8\";.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);font-size:var(--hm-font-size-sm)}.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}.as-risks{display:grid;gap:var(--hm-space-micro);margin:0;padding:0;list-style:none}.as-risks__item{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);background:transparent;color:var(--hm-text);font:inherit;font-size:var(--hm-font-size-sm);text-align:start;cursor:pointer}.as-risks__item:hover{background:var(--hm-hover)}.as-risks__item:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:calc(var(--hm-border-thin) * -1)}.as-risks__title{flex:1;min-inline-size:0;overflow-wrap:anywhere}.as-risks__title--closed{color:var(--hm-muted);text-decoration:line-through}.as-risks__status{flex:none;color:var(--hm-muted);font-size:var(--hm-font-size-xs)}\n"] }]
1680
+ }], 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 }] }], candidates: [{ type: i0.Input, args: [{ isSignal: true, alias: "candidates", 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 }] }] } });
1681
+
1682
+ /** Sem acento e sem caixa: "relatório" acha "RELATORIO". */
1683
+ const fold$1 = (s) => s.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
1684
+ /** Teto da lista do seletor: a busca afunila o resto. */
1685
+ const MAX_OPTIONS = 50;
1686
+ /**
1687
+ * "Agrupar em…": escolhe a sugestão principal para uma repetida (`source`), entre as abertas do quadro (`candidates`,
1688
+ * 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
1689
+ * relator passam para SUG-12"); "Agrupar" confirma (`HermesDialogService.confirm`) e grava. Uma sugestão que já recebeu
1690
+ * outras não pode ser agrupada (sem cadeia): o seletor avisa em vez de listar.
1691
+ */
1692
+ class SuggestionMergePickerComponent {
1693
+ constructor() {
1694
+ this.messages = inject(SPINE_MESSAGES);
1695
+ this.text = this.messages.suggestions;
1696
+ this.service = inject(AutoServiceSuggestionsService);
1697
+ this.dialogs = inject(HermesDialogService);
1698
+ this.toast = inject(HermesToastService);
1699
+ this.clientId = input.required(...(ngDevMode ? [{ debugName: "clientId" }] : /* istanbul ignore next */ []));
1700
+ /** A que vai ser agrupada; null = fechado. */
1701
+ this.source = input(null, ...(ngDevMode ? [{ debugName: "source" }] : /* istanbul ignore next */ []));
1702
+ /** As do quadro (o seletor tira a própria e as agrupadas). */
1703
+ this.candidates = input([], ...(ngDevMode ? [{ debugName: "candidates" }] : /* istanbul ignore next */ []));
1704
+ /** Agrupou: o destino inteiro, como o servidor devolveu. */
1705
+ this.merged = output();
1706
+ this.closed = output();
1707
+ this.statusColors = SPINE_SUGGESTION_STATUS_COLORS;
1708
+ this.term = signal('', ...(ngDevMode ? [{ debugName: "term" }] : /* istanbul ignore next */ []));
1709
+ this.picked = signal(null, ...(ngDevMode ? [{ debugName: "picked" }] : /* istanbul ignore next */ []));
1710
+ this.preview = signal(null, ...(ngDevMode ? [{ debugName: "preview" }] : /* istanbul ignore next */ []));
1711
+ this.previewError = signal(false, ...(ngDevMode ? [{ debugName: "previewError" }] : /* istanbul ignore next */ []));
1712
+ this.busy = signal(false, ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
1713
+ this.heading = computed(() => formatMessage(this.text.mergeHeading, { key: this.source()?.key ?? '' }), ...(ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []));
1714
+ this.options = computed(() => {
1715
+ const source = this.source();
1716
+ const term = fold$1(this.term().trim());
1717
+ return this.candidates()
1718
+ .filter((c) => c.id !== source?.id && !c.merged_into)
1719
+ .filter((c) => !term || fold$1(`${c.key} ${c.title}`).includes(term))
1720
+ .slice(0, MAX_OPTIONS);
1721
+ }, ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
1722
+ // Cada abertura começa do zero.
1723
+ effect(() => {
1724
+ this.source();
1725
+ untracked(() => {
1726
+ this.term.set('');
1727
+ this.picked.set(null);
1728
+ this.preview.set(null);
1729
+ this.previewError.set(false);
1730
+ });
1731
+ });
1732
+ }
1733
+ onOpen(open) {
1734
+ if (!open)
1735
+ this.closed.emit();
1736
+ }
1737
+ async pick(target) {
1738
+ const source = this.source();
1739
+ if (!source || this.picked()?.id === target.id)
1740
+ return;
1741
+ this.picked.set(target);
1742
+ this.preview.set(null);
1743
+ this.previewError.set(false);
1744
+ try {
1745
+ const preview = await this.service.mergePreview(this.clientId(), source.id, target.id);
1746
+ if (this.picked()?.id === target.id)
1747
+ this.preview.set(preview);
1748
+ }
1749
+ catch {
1750
+ if (this.picked()?.id === target.id)
1751
+ this.previewError.set(true);
1752
+ }
1753
+ }
1754
+ previewText(p) {
1755
+ const locale = this.messages.meta.locale;
1756
+ return formatMessage(this.text.mergePreview, {
1757
+ votes: pluralMessage(this.text.votes, p.votes, locale),
1758
+ reporters: pluralMessage(this.text.reportersCount, p.reporters, locale),
1759
+ key: p.target.key,
1760
+ });
1761
+ }
1762
+ votesText(count) {
1763
+ return pluralMessage(this.text.votes, count, this.messages.meta.locale);
1764
+ }
1765
+ async confirm() {
1766
+ const source = this.source();
1767
+ const preview = this.preview();
1768
+ const target = this.picked();
1769
+ if (!source || !preview || !target || this.busy())
1770
+ return;
1771
+ const ok = await this.dialogs.confirm({
1772
+ heading: formatMessage(this.text.mergeConfirmHeading, { key: source.key, target: target.key }),
1773
+ message: `${this.previewText(preview)} ${this.text.mergeConfirmMessage}`,
1774
+ confirmLabel: this.text.mergeConfirm,
1775
+ });
1776
+ if (!ok)
1777
+ return;
1778
+ this.busy.set(true);
1779
+ try {
1780
+ const saved = await this.service.merge(this.clientId(), source.id, target.id);
1781
+ this.toast.success(formatMessage(this.text.mergedToast, { key: source.key, target: target.key }));
1782
+ this.merged.emit({ source, target: saved });
1783
+ }
1784
+ catch {
1785
+ this.toast.error(this.text.mergeError);
1786
+ }
1787
+ finally {
1788
+ this.busy.set(false);
1789
+ }
1790
+ }
1791
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionMergePickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1792
+ 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: `
1793
+ <hm-modal [heading]="heading()" [open]="!!source()" (openChange)="onOpen($event)">
1794
+ @if (source(); as s) {
1795
+ <div class="as-merge">
1796
+ <p class="as-merge__intro">{{ text.mergeIntro }}</p>
1797
+ @if (s.merged_count) {
1798
+ <hm-alert variant="warning">{{ text.mergeHasMerged }}</hm-alert>
1799
+ } @else {
1800
+ <hm-search-field [label]="text.mergeSearch" labelHidden [placeholder]="text.mergeSearch" [landmark]="false"
1801
+ [value]="term()" (search)="term.set($event)" />
1802
+ <ul class="as-merge__list" [attr.aria-label]="text.mergeSearch">
1803
+ @for (c of options(); track c.id) {
1804
+ <li>
1805
+ <button type="button" class="as-merge__option" [class.as-merge__option--on]="picked()?.id === c.id"
1806
+ [attr.aria-pressed]="picked()?.id === c.id" (click)="pick(c)">
1807
+ <span class="as-merge__key">{{ c.key }}</span>
1808
+ <span class="as-merge__title">{{ c.title }}</span>
1809
+ <span class="as-merge__meta">
1810
+ <hm-tag [name]="text.statuses[c.status]" [color]="statusColors[c.status]" />
1811
+ <span>{{ votesText(c.votes) }}</span>
1812
+ </span>
1813
+ </button>
1814
+ </li>
1815
+ } @empty {
1816
+ <li class="as-merge__empty">{{ text.mergeNone }}</li>
1817
+ }
1818
+ </ul>
1819
+ @if (picked()) {
1820
+ <p class="as-merge__preview" aria-live="polite">
1821
+ @if (previewError()) { {{ text.mergePreviewError }} }
1822
+ @else if (preview(); as p) { {{ previewText(p) }} }
1823
+ @else { {{ text.mergeCalculating }} }
1824
+ </p>
1825
+ }
1826
+ }
1827
+ </div>
1828
+ }
1829
+ <div footer class="as-merge__footer">
1830
+ <hm-button emphasis="medium" (clicked)="closed.emit()">{{ text.cancel }}</hm-button>
1831
+ <hm-button icon="layers" [disabled]="!preview() || !!source()?.merged_count" [loading]="busy()" (clicked)="confirm()">
1832
+ {{ text.mergeConfirm }}
1833
+ </hm-button>
1834
+ </div>
1835
+ </hm-modal>
1836
+ `, 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 }); }
1837
+ }
1838
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionMergePickerComponent, decorators: [{
1839
+ type: Component,
1840
+ args: [{ selector: 'spine-suggestion-merge-picker', imports: [AlertComponent, ButtonComponent, ModalComponent, SearchFieldComponent, TagComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
1841
+ <hm-modal [heading]="heading()" [open]="!!source()" (openChange)="onOpen($event)">
1842
+ @if (source(); as s) {
1843
+ <div class="as-merge">
1844
+ <p class="as-merge__intro">{{ text.mergeIntro }}</p>
1845
+ @if (s.merged_count) {
1846
+ <hm-alert variant="warning">{{ text.mergeHasMerged }}</hm-alert>
1847
+ } @else {
1848
+ <hm-search-field [label]="text.mergeSearch" labelHidden [placeholder]="text.mergeSearch" [landmark]="false"
1849
+ [value]="term()" (search)="term.set($event)" />
1850
+ <ul class="as-merge__list" [attr.aria-label]="text.mergeSearch">
1851
+ @for (c of options(); track c.id) {
1852
+ <li>
1853
+ <button type="button" class="as-merge__option" [class.as-merge__option--on]="picked()?.id === c.id"
1854
+ [attr.aria-pressed]="picked()?.id === c.id" (click)="pick(c)">
1855
+ <span class="as-merge__key">{{ c.key }}</span>
1856
+ <span class="as-merge__title">{{ c.title }}</span>
1857
+ <span class="as-merge__meta">
1858
+ <hm-tag [name]="text.statuses[c.status]" [color]="statusColors[c.status]" />
1859
+ <span>{{ votesText(c.votes) }}</span>
1860
+ </span>
1861
+ </button>
1862
+ </li>
1863
+ } @empty {
1864
+ <li class="as-merge__empty">{{ text.mergeNone }}</li>
1865
+ }
1866
+ </ul>
1867
+ @if (picked()) {
1868
+ <p class="as-merge__preview" aria-live="polite">
1869
+ @if (previewError()) { {{ text.mergePreviewError }} }
1870
+ @else if (preview(); as p) { {{ previewText(p) }} }
1871
+ @else { {{ text.mergeCalculating }} }
1872
+ </p>
1873
+ }
1874
+ }
1875
+ </div>
1876
+ }
1877
+ <div footer class="as-merge__footer">
1878
+ <hm-button emphasis="medium" (clicked)="closed.emit()">{{ text.cancel }}</hm-button>
1879
+ <hm-button icon="layers" [disabled]="!preview() || !!source()?.merged_count" [loading]="busy()" (clicked)="confirm()">
1880
+ {{ text.mergeConfirm }}
1881
+ </hm-button>
1882
+ </div>
1883
+ </hm-modal>
1884
+ `, 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"] }]
1885
+ }], 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"] }] } });
1886
+
1887
+ const BACKLOG = 'backlog';
1888
+ const STATE_COLORS = { planned: 'neutral', active: 'info', closed: 'success' };
1889
+ /**
1890
+ * Planejamento: as sprints do app (criar, editar, iniciar, encerrar — as não concluídas voltam ao backlog — e excluir),
1891
+ * o `hm-backlog` com as sprints abertas e o backlog (arrastar uma tarefa entre eles grava a sprint dela) e o andamento
1892
+ * da sprint em curso: pontos concluídos e comprometidos contra a capacidade (`hm-progress`) e o burndown dia a dia
1893
+ * (`taskBurndownInput` → `burndownSeries`) numa tabela — o spine não depende do pacote de gráficos.
1894
+ */
1895
+ class SuggestionPlanningViewComponent {
1896
+ constructor() {
1897
+ this.s = inject(SuggestionPlanningStore);
1898
+ this.toast = inject(HermesToastService);
1899
+ this.p = this.s.plan;
1900
+ this.stateColors = STATE_COLORS;
1901
+ /** Hoje (AAAA-MM-DD no fuso de quem consulta): o burndown real para aqui. */
1902
+ this.today = input.required(...(ngDevMode ? [{ debugName: "today" }] : /* istanbul ignore next */ []));
1903
+ this.byName = (t) => `${t.key ?? ''} ${t.title}`.trim();
1904
+ /** Sobe quando uma gravação falha: as seções voltam ao que o store tem (desfaz o arrasto). */
1905
+ this.revision = signal(0, ...(ngDevMode ? [{ debugName: "revision" }] : /* istanbul ignore next */ []));
1906
+ this.openSprints = computed(() => this.s.sprints().filter((sp) => sp.state !== 'closed'), ...(ngDevMode ? [{ debugName: "openSprints" }] : /* istanbul ignore next */ []));
1907
+ this.closedSprints = computed(() => this.s.sprints().filter((sp) => sp.state === 'closed').reverse(), ...(ngDevMode ? [{ debugName: "closedSprints" }] : /* istanbul ignore next */ []));
1908
+ /** As sprints abertas e o backlog; a concluída só fica na sprint em curso (é o que ela entregou). */
1909
+ this.sections = linkedSignal(() => {
1910
+ this.revision();
1911
+ const open = this.openSprints();
1912
+ const ids = new Set(open.map((sp) => sp.id));
1913
+ const active = this.s.activeSprint()?.id;
1914
+ const tasks = this.s.items().filter((t) => t.task.status !== 'recusada' && (!t.done || (t.sprintId && t.sprintId === active)));
1915
+ return [
1916
+ ...open.map((sp) => ({
1917
+ id: sp.id,
1918
+ title: `${sp.name} · ${this.p.sprints.state[sp.state]}`,
1919
+ kind: 'sprint',
1920
+ start: sp.start_date,
1921
+ end: sp.end_date,
1922
+ goal: sp.goal,
1923
+ capacity: sp.capacity,
1924
+ items: tasks.filter((t) => t.sprintId === sp.id),
1925
+ })),
1926
+ { id: BACKLOG, title: this.p.backlog, kind: 'backlog', items: tasks.filter((t) => !t.done && (!t.sprintId || !ids.has(t.sprintId))) },
1927
+ ];
1928
+ }, ...(ngDevMode ? [{ debugName: "sections" }] : /* istanbul ignore next */ []));
1929
+ /** Burndown da sprint em curso, um dia por linha (o restante para em hoje). */
1930
+ this.burndown = computed(() => {
1931
+ const sprint = this.s.activeSprint();
1932
+ if (!sprint)
1933
+ return [];
1934
+ const tasks = this.s.items().filter((t) => t.sprintId === sprint.id);
1935
+ const series = burndownSeries(taskBurndownInput(tasks, {
1936
+ start: sprint.start_date, end: sprint.end_date, completedAt: (t) => t.completedAt, addedAt: (t) => t.createdAt,
1937
+ today: `${this.today()}T23:59:59`,
1938
+ }));
1939
+ const format = new Intl.DateTimeFormat(this.s.locale, { day: 'numeric', month: 'numeric' });
1940
+ const round = (v) => (v === null ? null : Math.round(v * 10) / 10);
1941
+ return series.days.map((d, i) => ({ day: format.format(d), ideal: round(series.ideal[i]), remaining: round(series.remaining[i]) }));
1942
+ }, ...(ngDevMode ? [{ debugName: "burndown" }] : /* istanbul ignore next */ []));
1943
+ // --- cadastro ---
1944
+ this.modalOpen = signal(false, ...(ngDevMode ? [{ debugName: "modalOpen" }] : /* istanbul ignore next */ []));
1945
+ this.editing = signal(null, ...(ngDevMode ? [{ debugName: "editing" }] : /* istanbul ignore next */ []));
1946
+ this.submitted = signal(false, ...(ngDevMode ? [{ debugName: "submitted" }] : /* istanbul ignore next */ []));
1947
+ this.saving = signal(false, ...(ngDevMode ? [{ debugName: "saving" }] : /* istanbul ignore next */ []));
1948
+ this.draft = signal(this.empty(), ...(ngDevMode ? [{ debugName: "draft" }] : /* istanbul ignore next */ []));
1949
+ this.dateError = computed(() => {
1950
+ if (!this.submitted())
1951
+ return '';
1952
+ const d = this.draft();
1953
+ if (!d.start || !d.end)
1954
+ return this.p.sprints.datesRequired;
1955
+ return d.end < d.start ? this.p.sprints.datesOrder : '';
1956
+ }, ...(ngDevMode ? [{ debugName: "dateError" }] : /* istanbul ignore next */ []));
1957
+ }
1958
+ named(message, sprint) {
1959
+ return formatMessage(message, { name: sprint.name });
1960
+ }
1961
+ patch(change) {
1962
+ this.draft.update((d) => ({ ...d, ...change }));
1963
+ }
1964
+ toNumber(value) {
1965
+ if (value === '' || value === null || value === undefined)
1966
+ return null;
1967
+ const n = Math.trunc(Number(value));
1968
+ return Number.isFinite(n) && n >= 0 ? n : null;
1969
+ }
1970
+ create() {
1971
+ this.editing.set(null);
1972
+ this.submitted.set(false);
1973
+ this.draft.set(this.empty());
1974
+ this.modalOpen.set(true);
1975
+ }
1976
+ edit(sprint) {
1977
+ this.editing.set(sprint);
1978
+ this.submitted.set(false);
1979
+ this.draft.set({ name: sprint.name, goal: sprint.goal, start: sprint.start_date, end: sprint.end_date, capacity: sprint.capacity });
1980
+ this.modalOpen.set(true);
1981
+ }
1982
+ async save() {
1983
+ this.submitted.set(true);
1984
+ const d = this.draft();
1985
+ if (!d.name.trim() || this.dateError())
1986
+ return;
1987
+ this.saving.set(true);
1988
+ const ok = await this.s.saveSprint(this.editing()?.id ?? null, {
1989
+ name: d.name.trim(), goal: d.goal.trim(), start_date: d.start, end_date: d.end, capacity: d.capacity,
1990
+ });
1991
+ this.saving.set(false);
1992
+ if (ok)
1993
+ this.modalOpen.set(false);
1994
+ }
1995
+ /** Arrastar para outra seção grava a sprint da tarefa; na mesma seção, a ordem é só da tela. */
1996
+ async onMoved(event) {
1997
+ if (event.from === event.to)
1998
+ return;
1999
+ const sprintId = event.to === BACKLOG ? null : event.to;
2000
+ const ok = await this.s.updateTask(event.item.id, { sprint_id: sprintId });
2001
+ if (!ok) {
2002
+ this.revision.update((n) => n + 1);
2003
+ return;
2004
+ }
2005
+ void this.s.refreshSprints();
2006
+ const target = sprintId ? this.s.sprintName().get(sprintId) ?? '' : this.p.backlog;
2007
+ this.toast.success(formatMessage(this.p.sprints.moved, { key: event.item.key ?? event.item.title, target }));
2008
+ }
2009
+ empty() {
2010
+ return { name: '', goal: '', start: null, end: null, capacity: null };
2011
+ }
2012
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionPlanningViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2013
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SuggestionPlanningViewComponent, isStandalone: true, selector: "spine-suggestion-planning", inputs: { today: { classPropertyName: "today", publicName: "today", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
2014
+ <section class="plan-card" aria-labelledby="sug-plan-sprints">
2015
+ <div class="plan-toolbar">
2016
+ <h3 id="sug-plan-sprints">{{ p.sprints.heading }}</h3>
2017
+ <hm-button class="plan-toolbar__end" icon="plus" size="small" (clicked)="create()">{{ p.sprints.new }}</hm-button>
2018
+ </div>
2019
+ @if (openSprints().length) {
2020
+ <ul class="plan-rows">
2021
+ @for (sp of openSprints(); track sp.id) {
2022
+ <li>
2023
+ <span>
2024
+ <strong>{{ sp.name }}</strong>
2025
+ <hm-tag [name]="p.sprints.state[sp.state]" [color]="stateColors[sp.state]" />
2026
+ <span class="plan-muted">{{ s.rangeText(sp.start_date, sp.end_date) }} · {{ s.pointsText(sp.points) }}@if (sp.capacity !== null) { / {{ s.pointsText(sp.capacity) }} }</span>
2027
+ </span>
2028
+ <span class="plan-toolbar">
2029
+ @if (sp.state === 'planned' && !s.activeSprint()) {
2030
+ <hm-button emphasis="medium" size="small" icon="play" (clicked)="s.startSprint(sp)">{{ p.sprints.start }}</hm-button>
2031
+ }
2032
+ @if (sp.state === 'active') {
2033
+ <hm-button emphasis="medium" size="small" icon="check" [hmPopconfirmTrigger]="closeConfirm">{{ p.sprints.close }}</hm-button>
2034
+ <hm-popconfirm #closeConfirm [heading]="named(p.sprints.closeHeading, sp)" [message]="p.sprints.closeMessage" [confirmLabel]="p.sprints.close" (confirmed)="s.closeSprint(sp)" />
2035
+ }
2036
+ <hm-button emphasis="minimal" size="small" icon="pencil" (clicked)="edit(sp)">{{ p.edit }}</hm-button>
2037
+ <hm-button emphasis="minimal" intent="danger" size="small" icon="trash" [hmPopconfirmTrigger]="removeConfirm">{{ p.delete }}</hm-button>
2038
+ <hm-popconfirm #removeConfirm [heading]="named(p.sprints.deleteHeading, sp)" [message]="p.sprints.deleteMessage" [confirmLabel]="p.delete" intent="danger" (confirmed)="s.removeSprint(sp)" />
2039
+ </span>
2040
+ </li>
2041
+ }
2042
+ </ul>
2043
+ }
2044
+ <p class="plan-muted">{{ p.sprints.hint }}</p>
2045
+ <hm-backlog [(sections)]="sections" [itemLabel]="byName" [headingLevel]="4" (moved)="onMoved($event)" />
2046
+ </section>
2047
+
2048
+ @if (s.activeSprint(); as sprint) {
2049
+ <section class="plan-card" aria-labelledby="sug-plan-progress">
2050
+ <h3 id="sug-plan-progress">{{ named(p.sprints.progressHeading, sprint) }}</h3>
2051
+ <hm-progress [label]="p.sprints.done" [value]="sprint.done_points" [max]="sprint.points || 1" intent="success" showValue />
2052
+ @if (sprint.capacity) {
2053
+ <hm-progress [label]="p.sprints.capacityUse" [value]="sprint.points" [max]="sprint.capacity" [intent]="sprint.points > sprint.capacity ? 'danger' : 'accent'" showValue />
2054
+ }
2055
+ @if (burndown().length) {
2056
+ <div class="plan-table-wrap" tabindex="0" role="region" [attr.aria-label]="named(p.sprints.burndownCaption, sprint)">
2057
+ <table class="plan-table">
2058
+ <caption>{{ named(p.sprints.burndownCaption, sprint) }}</caption>
2059
+ <thead><tr><th scope="col">{{ p.sprints.day }}</th><th scope="col">{{ p.sprints.ideal }}</th><th scope="col">{{ p.sprints.remaining }}</th></tr></thead>
2060
+ <tbody>
2061
+ @for (row of burndown(); track row.day) {
2062
+ <tr><th scope="row">{{ row.day }}</th><td>{{ row.ideal }}</td><td>{{ row.remaining ?? '—' }}</td></tr>
2063
+ }
2064
+ </tbody>
2065
+ </table>
2066
+ </div>
2067
+ }
2068
+ </section>
2069
+ }
2070
+
2071
+ @if (closedSprints().length) {
2072
+ <section class="plan-card" aria-labelledby="sug-plan-closed">
2073
+ <h3 id="sug-plan-closed">{{ p.sprints.closed }}</h3>
2074
+ <p class="plan-muted">{{ p.sprints.readOnlyClosed }}</p>
2075
+ <ul class="plan-rows">
2076
+ @for (sp of closedSprints(); track sp.id) {
2077
+ <li>
2078
+ <span><strong>{{ sp.name }}</strong> <span class="plan-muted">{{ s.rangeText(sp.start_date, sp.end_date) }}</span></span>
2079
+ <span>{{ s.pointsText(sp.done_points) }} / {{ s.pointsText(sp.points) }}</span>
2080
+ </li>
2081
+ }
2082
+ </ul>
2083
+ </section>
2084
+ }
2085
+
2086
+ <hm-modal [heading]="editing() ? p.sprints.editHeading : p.sprints.newHeading" size="medium" [(open)]="modalOpen">
2087
+ <form class="plan-form" (submit)="$event.preventDefault(); save()" novalidate>
2088
+ <hm-input [label]="p.sprints.name" required [maxLength]="80" [value]="draft().name" (valueChange)="patch({ name: $any($event) })"
2089
+ [error]="submitted() && !draft().name.trim() ? p.sprints.nameRequired : ''" />
2090
+ <hm-textarea [label]="p.sprints.goal" [rows]="2" autoGrow [maxLength]="500" [value]="draft().goal" (valueChange)="patch({ goal: $event })" />
2091
+ <div class="plan-row">
2092
+ <hm-date-picker [label]="p.sprints.start_date" required [value]="draft().start" [max]="$any(draft().end)" (valueChange)="patch({ start: $event })" />
2093
+ <hm-date-picker [label]="p.sprints.end_date" required [value]="draft().end" [min]="$any(draft().start)" (valueChange)="patch({ end: $event })" />
2094
+ </div>
2095
+ @if (dateError()) {
2096
+ <p class="plan-muted" role="alert">{{ dateError() }}</p>
2097
+ }
2098
+ <hm-input type="number" [label]="p.sprints.capacity" [min]="0" [max]="9999" [step]="1" [value]="$any(draft().capacity ?? '')"
2099
+ (valueChange)="patch({ capacity: toNumber($event) })" />
2100
+ </form>
2101
+ <div footer class="plan-footer">
2102
+ <hm-button emphasis="minimal" (clicked)="modalOpen.set(false)">{{ p.cancel }}</hm-button>
2103
+ <hm-button [loading]="saving()" (clicked)="save()">{{ p.save }}</hm-button>
2104
+ </div>
2105
+ </hm-modal>
2106
+ `, isInline: true, styles: [":host{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--hm-space-md);min-inline-size:0}.plan-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,20rem),1fr));gap:var(--hm-space-md)}.plan-card{display:grid;grid-template-columns:minmax(0,1fr);align-content:start;gap:var(--hm-space-sm);min-inline-size:0;padding:var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.plan-card h3,.plan-card h4{margin:0;font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold);line-height:var(--hm-line-height-snug)}.plan-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.plan-toolbar__end{margin-inline-start:auto}.plan-muted{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-normal)}.plan-stats{display:flex;flex-wrap:wrap;gap:var(--hm-space-md);margin:0}.plan-stats div{display:grid;gap:var(--hm-space-nano);min-inline-size:0}.plan-stats dt{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.plan-stats dd{margin:0;font-size:var(--hm-font-size-lg);font-weight:var(--hm-font-weight-semibold)}.plan-text{margin:0;font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-snug)}.plan-title{font-weight:var(--hm-font-weight-medium)}.plan-rows{display:grid;gap:var(--hm-space-xs);margin:0;padding:0;font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-snug);list-style:none}.plan-rows li{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs) var(--hm-space-sm);min-inline-size:0}.plan-link{min-inline-size:0;padding:0;border:0;background:none;color:var(--hm-accent-strong);font:inherit;font-weight:var(--hm-font-weight-medium);text-align:start;cursor:pointer}.plan-link:hover{text-decoration:underline}.plan-link:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:var(--hm-border-thin)}.plan-key{color:var(--hm-muted);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-medium)}.plan-form{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--hm-space-sm)}.plan-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,12rem),1fr));gap:var(--hm-space-sm)}.plan-footer{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.plan-footer__start{margin-inline-end:auto}.plan-table-wrap{overflow-x:auto}.plan-table{inline-size:100%;border-collapse:collapse;font-size:var(--hm-font-size-sm);font-variant-numeric:tabular-nums}.plan-table caption{padding-block-end:var(--hm-space-xs);color:var(--hm-muted);text-align:start}.plan-table th,.plan-table td{padding:var(--hm-space-nano) var(--hm-space-xs);border-block-end:var(--hm-border-hairline) solid var(--hm-border);text-align:end;white-space:nowrap}.plan-table th:first-child,.plan-table td:first-child{text-align:start}\n"], dependencies: [{ kind: "component", type: BacklogComponent, selector: "hm-backlog", inputs: ["sections", "itemKey", "itemLabel", "priorityScale", "headingLevel", "disabled", "texts"], outputs: ["sectionsChange", "moved"] }, { 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: DatePickerComponent, selector: "hm-date-picker", inputs: ["value", "label", "labelHidden", "hint", "placeholder", "min", "max", "valueFormat"], outputs: ["valueChange"] }, { 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: PopconfirmComponent, selector: "hm-popconfirm", inputs: ["open", "heading", "headingLevel", "message", "confirmLabel", "cancelLabel", "intent", "side", "align", "texts"], outputs: ["openChange", "confirmed", "cancelled"] }, { kind: "directive", type: PopconfirmTriggerDirective, selector: "[hmPopconfirmTrigger]", inputs: ["hmPopconfirmTrigger"] }, { kind: "component", type: ProgressComponent, selector: "hm-progress", inputs: ["value", "max", "label", "labelHidden", "showValue", "indeterminate", "intent", "size", "hint", "shape"] }, { kind: "component", type: TagComponent, selector: "hm-tag", inputs: ["name", "color", "compact"] }, { kind: "component", type: TextareaComponent, selector: "hm-textarea", inputs: ["value", "label", "hint", "placeholder", "maxLength", "rows", "autoGrow", "labelHidden", "loading", "maxRows", "submitKey", "mentionCandidates", "mentionTrigger", "texts"], outputs: ["valueChange", "submitted", "mentioned"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2107
+ }
2108
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionPlanningViewComponent, decorators: [{
2109
+ type: Component,
2110
+ args: [{ selector: 'spine-suggestion-planning', imports: [
2111
+ BacklogComponent, ButtonComponent, DatePickerComponent, InputComponent, ModalComponent, PopconfirmComponent,
2112
+ PopconfirmTriggerDirective, ProgressComponent, TagComponent, TextareaComponent,
2113
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: `
2114
+ <section class="plan-card" aria-labelledby="sug-plan-sprints">
2115
+ <div class="plan-toolbar">
2116
+ <h3 id="sug-plan-sprints">{{ p.sprints.heading }}</h3>
2117
+ <hm-button class="plan-toolbar__end" icon="plus" size="small" (clicked)="create()">{{ p.sprints.new }}</hm-button>
2118
+ </div>
2119
+ @if (openSprints().length) {
2120
+ <ul class="plan-rows">
2121
+ @for (sp of openSprints(); track sp.id) {
2122
+ <li>
2123
+ <span>
2124
+ <strong>{{ sp.name }}</strong>
2125
+ <hm-tag [name]="p.sprints.state[sp.state]" [color]="stateColors[sp.state]" />
2126
+ <span class="plan-muted">{{ s.rangeText(sp.start_date, sp.end_date) }} · {{ s.pointsText(sp.points) }}@if (sp.capacity !== null) { / {{ s.pointsText(sp.capacity) }} }</span>
2127
+ </span>
2128
+ <span class="plan-toolbar">
2129
+ @if (sp.state === 'planned' && !s.activeSprint()) {
2130
+ <hm-button emphasis="medium" size="small" icon="play" (clicked)="s.startSprint(sp)">{{ p.sprints.start }}</hm-button>
2131
+ }
2132
+ @if (sp.state === 'active') {
2133
+ <hm-button emphasis="medium" size="small" icon="check" [hmPopconfirmTrigger]="closeConfirm">{{ p.sprints.close }}</hm-button>
2134
+ <hm-popconfirm #closeConfirm [heading]="named(p.sprints.closeHeading, sp)" [message]="p.sprints.closeMessage" [confirmLabel]="p.sprints.close" (confirmed)="s.closeSprint(sp)" />
2135
+ }
2136
+ <hm-button emphasis="minimal" size="small" icon="pencil" (clicked)="edit(sp)">{{ p.edit }}</hm-button>
2137
+ <hm-button emphasis="minimal" intent="danger" size="small" icon="trash" [hmPopconfirmTrigger]="removeConfirm">{{ p.delete }}</hm-button>
2138
+ <hm-popconfirm #removeConfirm [heading]="named(p.sprints.deleteHeading, sp)" [message]="p.sprints.deleteMessage" [confirmLabel]="p.delete" intent="danger" (confirmed)="s.removeSprint(sp)" />
2139
+ </span>
2140
+ </li>
2141
+ }
2142
+ </ul>
2143
+ }
2144
+ <p class="plan-muted">{{ p.sprints.hint }}</p>
2145
+ <hm-backlog [(sections)]="sections" [itemLabel]="byName" [headingLevel]="4" (moved)="onMoved($event)" />
2146
+ </section>
2147
+
2148
+ @if (s.activeSprint(); as sprint) {
2149
+ <section class="plan-card" aria-labelledby="sug-plan-progress">
2150
+ <h3 id="sug-plan-progress">{{ named(p.sprints.progressHeading, sprint) }}</h3>
2151
+ <hm-progress [label]="p.sprints.done" [value]="sprint.done_points" [max]="sprint.points || 1" intent="success" showValue />
2152
+ @if (sprint.capacity) {
2153
+ <hm-progress [label]="p.sprints.capacityUse" [value]="sprint.points" [max]="sprint.capacity" [intent]="sprint.points > sprint.capacity ? 'danger' : 'accent'" showValue />
2154
+ }
2155
+ @if (burndown().length) {
2156
+ <div class="plan-table-wrap" tabindex="0" role="region" [attr.aria-label]="named(p.sprints.burndownCaption, sprint)">
2157
+ <table class="plan-table">
2158
+ <caption>{{ named(p.sprints.burndownCaption, sprint) }}</caption>
2159
+ <thead><tr><th scope="col">{{ p.sprints.day }}</th><th scope="col">{{ p.sprints.ideal }}</th><th scope="col">{{ p.sprints.remaining }}</th></tr></thead>
2160
+ <tbody>
2161
+ @for (row of burndown(); track row.day) {
2162
+ <tr><th scope="row">{{ row.day }}</th><td>{{ row.ideal }}</td><td>{{ row.remaining ?? '—' }}</td></tr>
2163
+ }
2164
+ </tbody>
2165
+ </table>
2166
+ </div>
2167
+ }
2168
+ </section>
2169
+ }
2170
+
2171
+ @if (closedSprints().length) {
2172
+ <section class="plan-card" aria-labelledby="sug-plan-closed">
2173
+ <h3 id="sug-plan-closed">{{ p.sprints.closed }}</h3>
2174
+ <p class="plan-muted">{{ p.sprints.readOnlyClosed }}</p>
2175
+ <ul class="plan-rows">
2176
+ @for (sp of closedSprints(); track sp.id) {
2177
+ <li>
2178
+ <span><strong>{{ sp.name }}</strong> <span class="plan-muted">{{ s.rangeText(sp.start_date, sp.end_date) }}</span></span>
2179
+ <span>{{ s.pointsText(sp.done_points) }} / {{ s.pointsText(sp.points) }}</span>
2180
+ </li>
2181
+ }
2182
+ </ul>
2183
+ </section>
2184
+ }
2185
+
2186
+ <hm-modal [heading]="editing() ? p.sprints.editHeading : p.sprints.newHeading" size="medium" [(open)]="modalOpen">
2187
+ <form class="plan-form" (submit)="$event.preventDefault(); save()" novalidate>
2188
+ <hm-input [label]="p.sprints.name" required [maxLength]="80" [value]="draft().name" (valueChange)="patch({ name: $any($event) })"
2189
+ [error]="submitted() && !draft().name.trim() ? p.sprints.nameRequired : ''" />
2190
+ <hm-textarea [label]="p.sprints.goal" [rows]="2" autoGrow [maxLength]="500" [value]="draft().goal" (valueChange)="patch({ goal: $event })" />
2191
+ <div class="plan-row">
2192
+ <hm-date-picker [label]="p.sprints.start_date" required [value]="draft().start" [max]="$any(draft().end)" (valueChange)="patch({ start: $event })" />
2193
+ <hm-date-picker [label]="p.sprints.end_date" required [value]="draft().end" [min]="$any(draft().start)" (valueChange)="patch({ end: $event })" />
2194
+ </div>
2195
+ @if (dateError()) {
2196
+ <p class="plan-muted" role="alert">{{ dateError() }}</p>
2197
+ }
2198
+ <hm-input type="number" [label]="p.sprints.capacity" [min]="0" [max]="9999" [step]="1" [value]="$any(draft().capacity ?? '')"
2199
+ (valueChange)="patch({ capacity: toNumber($event) })" />
2200
+ </form>
2201
+ <div footer class="plan-footer">
2202
+ <hm-button emphasis="minimal" (clicked)="modalOpen.set(false)">{{ p.cancel }}</hm-button>
2203
+ <hm-button [loading]="saving()" (clicked)="save()">{{ p.save }}</hm-button>
2204
+ </div>
2205
+ </hm-modal>
2206
+ `, styles: [":host{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--hm-space-md);min-inline-size:0}.plan-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,20rem),1fr));gap:var(--hm-space-md)}.plan-card{display:grid;grid-template-columns:minmax(0,1fr);align-content:start;gap:var(--hm-space-sm);min-inline-size:0;padding:var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.plan-card h3,.plan-card h4{margin:0;font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold);line-height:var(--hm-line-height-snug)}.plan-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.plan-toolbar__end{margin-inline-start:auto}.plan-muted{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-normal)}.plan-stats{display:flex;flex-wrap:wrap;gap:var(--hm-space-md);margin:0}.plan-stats div{display:grid;gap:var(--hm-space-nano);min-inline-size:0}.plan-stats dt{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.plan-stats dd{margin:0;font-size:var(--hm-font-size-lg);font-weight:var(--hm-font-weight-semibold)}.plan-text{margin:0;font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-snug)}.plan-title{font-weight:var(--hm-font-weight-medium)}.plan-rows{display:grid;gap:var(--hm-space-xs);margin:0;padding:0;font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-snug);list-style:none}.plan-rows li{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs) var(--hm-space-sm);min-inline-size:0}.plan-link{min-inline-size:0;padding:0;border:0;background:none;color:var(--hm-accent-strong);font:inherit;font-weight:var(--hm-font-weight-medium);text-align:start;cursor:pointer}.plan-link:hover{text-decoration:underline}.plan-link:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:var(--hm-border-thin)}.plan-key{color:var(--hm-muted);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-medium)}.plan-form{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--hm-space-sm)}.plan-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,12rem),1fr));gap:var(--hm-space-sm)}.plan-footer{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.plan-footer__start{margin-inline-end:auto}.plan-table-wrap{overflow-x:auto}.plan-table{inline-size:100%;border-collapse:collapse;font-size:var(--hm-font-size-sm);font-variant-numeric:tabular-nums}.plan-table caption{padding-block-end:var(--hm-space-xs);color:var(--hm-muted);text-align:start}.plan-table th,.plan-table td{padding:var(--hm-space-nano) var(--hm-space-xs);border-block-end:var(--hm-border-hairline) solid var(--hm-border);text-align:end;white-space:nowrap}.plan-table th:first-child,.plan-table td:first-child{text-align:start}\n"] }]
2207
+ }], propDecorators: { today: [{ type: i0.Input, args: [{ isSignal: true, alias: "today", required: true }] }] } });
2208
+
2209
+ /**
2210
+ * Riscos das tarefas do app: o `hm-risk-register` com a matriz probabilidade x impacto (clicar numa célula filtra a
2211
+ * lista) e, embaixo do título de cada risco, a tarefa ameaçada. Cadastro e edição no `spine-suggestion-risk-form`,
2212
+ * onde a tarefa é obrigatória (todo risco é de uma tarefa).
2213
+ */
2214
+ class SuggestionRisksComponent {
2215
+ constructor() {
2216
+ this.s = inject(SuggestionPlanningStore);
2217
+ this.p = this.s.plan;
2218
+ this.selected = signal(null, ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
2219
+ this.open = signal(false, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
2220
+ this.editing = signal(null, ...(ngDevMode ? [{ debugName: "editing" }] : /* istanbul ignore next */ []));
2221
+ }
2222
+ create() {
2223
+ this.editing.set(null);
2224
+ this.open.set(true);
2225
+ }
2226
+ edit(risk) {
2227
+ this.editing.set(risk);
2228
+ this.open.set(true);
2229
+ }
2230
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionRisksComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2231
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.24", type: SuggestionRisksComponent, isStandalone: true, selector: "spine-suggestion-risks", ngImport: i0, template: `
2232
+ <div class="plan-toolbar">
2233
+ <hm-button icon="plus" (clicked)="create()">{{ p.risks.new }}</hm-button>
2234
+ <p class="plan-muted">{{ p.risks.hint }}</p>
2235
+ </div>
2236
+ <hm-risk-register [label]="p.risks.label" [risks]="s.riskItems()" [statuses]="s.riskStatuses()" interactive
2237
+ [(selected)]="selected" (riskClick)="edit($event.risk)" />
2238
+
2239
+ <spine-suggestion-risk-form [(open)]="open" [risk]="editing()" />
2240
+ `, isInline: true, styles: [":host{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--hm-space-md);min-inline-size:0}.plan-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,20rem),1fr));gap:var(--hm-space-md)}.plan-card{display:grid;grid-template-columns:minmax(0,1fr);align-content:start;gap:var(--hm-space-sm);min-inline-size:0;padding:var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.plan-card h3,.plan-card h4{margin:0;font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold);line-height:var(--hm-line-height-snug)}.plan-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.plan-toolbar__end{margin-inline-start:auto}.plan-muted{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-normal)}.plan-stats{display:flex;flex-wrap:wrap;gap:var(--hm-space-md);margin:0}.plan-stats div{display:grid;gap:var(--hm-space-nano);min-inline-size:0}.plan-stats dt{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.plan-stats dd{margin:0;font-size:var(--hm-font-size-lg);font-weight:var(--hm-font-weight-semibold)}.plan-text{margin:0;font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-snug)}.plan-title{font-weight:var(--hm-font-weight-medium)}.plan-rows{display:grid;gap:var(--hm-space-xs);margin:0;padding:0;font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-snug);list-style:none}.plan-rows li{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs) var(--hm-space-sm);min-inline-size:0}.plan-link{min-inline-size:0;padding:0;border:0;background:none;color:var(--hm-accent-strong);font:inherit;font-weight:var(--hm-font-weight-medium);text-align:start;cursor:pointer}.plan-link:hover{text-decoration:underline}.plan-link:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:var(--hm-border-thin)}.plan-key{color:var(--hm-muted);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-medium)}.plan-form{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--hm-space-sm)}.plan-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,12rem),1fr));gap:var(--hm-space-sm)}.plan-footer{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.plan-footer__start{margin-inline-end:auto}.plan-table-wrap{overflow-x:auto}.plan-table{inline-size:100%;border-collapse:collapse;font-size:var(--hm-font-size-sm);font-variant-numeric:tabular-nums}.plan-table caption{padding-block-end:var(--hm-space-xs);color:var(--hm-muted);text-align:start}.plan-table th,.plan-table td{padding:var(--hm-space-nano) var(--hm-space-xs);border-block-end:var(--hm-border-hairline) solid var(--hm-border);text-align:end;white-space:nowrap}.plan-table th:first-child,.plan-table td:first-child{text-align:start}\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: RiskRegisterComponent, selector: "hm-risk-register", inputs: ["risks", "statuses", "showMatrix", "interactive", "thresholds", "selected", "label", "texts"], outputs: ["selectedChange", "riskClick"] }, { kind: "component", type: SuggestionRiskFormComponent, selector: "spine-suggestion-risk-form", inputs: ["open", "risk", "task"], outputs: ["openChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2241
+ }
2242
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionRisksComponent, decorators: [{
2243
+ type: Component,
2244
+ args: [{ selector: 'spine-suggestion-risks', imports: [ButtonComponent, RiskRegisterComponent, SuggestionRiskFormComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
2245
+ <div class="plan-toolbar">
2246
+ <hm-button icon="plus" (clicked)="create()">{{ p.risks.new }}</hm-button>
2247
+ <p class="plan-muted">{{ p.risks.hint }}</p>
2248
+ </div>
2249
+ <hm-risk-register [label]="p.risks.label" [risks]="s.riskItems()" [statuses]="s.riskStatuses()" interactive
2250
+ [(selected)]="selected" (riskClick)="edit($event.risk)" />
2251
+
2252
+ <spine-suggestion-risk-form [(open)]="open" [risk]="editing()" />
2253
+ `, styles: [":host{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--hm-space-md);min-inline-size:0}.plan-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,20rem),1fr));gap:var(--hm-space-md)}.plan-card{display:grid;grid-template-columns:minmax(0,1fr);align-content:start;gap:var(--hm-space-sm);min-inline-size:0;padding:var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.plan-card h3,.plan-card h4{margin:0;font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold);line-height:var(--hm-line-height-snug)}.plan-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.plan-toolbar__end{margin-inline-start:auto}.plan-muted{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-normal)}.plan-stats{display:flex;flex-wrap:wrap;gap:var(--hm-space-md);margin:0}.plan-stats div{display:grid;gap:var(--hm-space-nano);min-inline-size:0}.plan-stats dt{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.plan-stats dd{margin:0;font-size:var(--hm-font-size-lg);font-weight:var(--hm-font-weight-semibold)}.plan-text{margin:0;font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-snug)}.plan-title{font-weight:var(--hm-font-weight-medium)}.plan-rows{display:grid;gap:var(--hm-space-xs);margin:0;padding:0;font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-snug);list-style:none}.plan-rows li{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs) var(--hm-space-sm);min-inline-size:0}.plan-link{min-inline-size:0;padding:0;border:0;background:none;color:var(--hm-accent-strong);font:inherit;font-weight:var(--hm-font-weight-medium);text-align:start;cursor:pointer}.plan-link:hover{text-decoration:underline}.plan-link:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:var(--hm-border-thin)}.plan-key{color:var(--hm-muted);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-medium)}.plan-form{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--hm-space-sm)}.plan-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,12rem),1fr));gap:var(--hm-space-sm)}.plan-footer{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.plan-footer__start{margin-inline-end:auto}.plan-table-wrap{overflow-x:auto}.plan-table{inline-size:100%;border-collapse:collapse;font-size:var(--hm-font-size-sm);font-variant-numeric:tabular-nums}.plan-table caption{padding-block-end:var(--hm-space-xs);color:var(--hm-muted);text-align:start}.plan-table th,.plan-table td{padding:var(--hm-space-nano) var(--hm-space-xs);border-block-end:var(--hm-border-hairline) solid var(--hm-border);text-align:end;white-space:nowrap}.plan-table th:first-child,.plan-table td:first-child{text-align:start}\n"] }]
2254
+ }] });
2255
+
2256
+ /** Capacidade de referência por pessoa por semana quando a sprint em curso não diz (pontos). */
2257
+ const DEFAULT_WEEKLY_CAPACITY = 8;
2258
+ const WEEKS = 4;
2259
+ const DAY_MS = 86_400_000;
2260
+ const MILESTONE_PREFIX = 'milestone:';
2261
+ /** AAAA-MM-DD de uma data local. */
2262
+ function isoDay(d) {
2263
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
2264
+ }
2265
+ /** A segunda-feira da semana de `d` (meia-noite local). */
2266
+ function mondayOf(d) {
2267
+ const day = (d.getDay() + 6) % 7;
2268
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate() - day);
2269
+ }
2270
+ /**
2271
+ * Cronograma: o `hm-gantt` com as tarefas abertas que têm data (barras de `taskGanttItems`, agrupadas pela sprint, com
2272
+ * as setas de dependência) e os marcos do app; arrastar uma barra grava início e prazo (dependência quebrada vira
2273
+ * aviso) e arrastar um marco grava a data. Os marcos se cadastram e se excluem aqui. Abaixo, a carga da equipe nas
2274
+ * próximas 4 semanas (`taskWorkloadAllocations` → `hm-workload`), contra uma capacidade de referência por pessoa.
2275
+ */
2276
+ class SuggestionScheduleComponent {
2277
+ constructor() {
2278
+ this.s = inject(SuggestionPlanningStore);
2279
+ this.toast = inject(HermesToastService);
2280
+ this.p = this.s.plan;
2281
+ this.openTask = output();
2282
+ /** Clique numa célula da carga: a pessoa (o quadro pode filtrar por ela). */
2283
+ this.person = output();
2284
+ this.items = computed(() => {
2285
+ const names = this.s.sprintName();
2286
+ const tasks = taskGanttItems(this.s.open(), {
2287
+ group: (t) => (t.sprintId ? names.get(t.sprintId) ?? this.p.backlog : this.p.backlog),
2288
+ tone: (t) => (t.task.priority === 'urgente' ? 'danger' : t.task.status === 'em_desenvolvimento' ? 'info' : undefined),
2289
+ });
2290
+ const milestones = this.s.milestones().map((m) => ({
2291
+ id: `${MILESTONE_PREFIX}${m.id}`, label: m.title, start: m.date, milestone: true, group: this.p.schedule.milestones,
2292
+ }));
2293
+ return [...tasks, ...milestones];
2294
+ }, ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
2295
+ /** Capacidade por pessoa por semana: a da sprint em curso dividida pela equipe e pelas semanas dela; senão, a padrão. */
2296
+ this.weekly = computed(() => {
2297
+ const sprint = this.s.activeSprint();
2298
+ const people = this.team0().length;
2299
+ if (!sprint?.capacity || !people)
2300
+ return DEFAULT_WEEKLY_CAPACITY;
2301
+ const days = (Date.parse(`${sprint.end_date}T00:00:00Z`) - Date.parse(`${sprint.start_date}T00:00:00Z`)) / DAY_MS + 1;
2302
+ return Math.max(1, Math.round(sprint.capacity / Math.max(1, days / 7) / people));
2303
+ }, ...(ngDevMode ? [{ debugName: "weekly" }] : /* istanbul ignore next */ []));
2304
+ /** Quem tem tarefa aberta atribuída. */
2305
+ this.team0 = computed(() => {
2306
+ const ids = new Set(this.s.open().map((t) => t.assignee?.id).filter(Boolean));
2307
+ return this.s.people().filter((p) => ids.has(p.id));
2308
+ }, ...(ngDevMode ? [{ debugName: "team0" }] : /* istanbul ignore next */ []));
2309
+ this.team = computed(() => this.team0().map((p) => ({ id: p.id, name: p.name, imageUrl: p.imageUrl, capacity: this.weekly() })), ...(ngDevMode ? [{ debugName: "team" }] : /* istanbul ignore next */ []));
2310
+ this.capacityHint = computed(() => formatMessage(this.p.schedule.workloadHint, { capacity: this.weekly() }), ...(ngDevMode ? [{ debugName: "capacityHint" }] : /* istanbul ignore next */ []));
2311
+ this.ranges = computed(() => {
2312
+ const monday = mondayOf(new Date());
2313
+ const format = new Intl.DateTimeFormat(this.s.locale, { day: 'numeric', month: 'short' });
2314
+ return Array.from({ length: WEEKS }, (_, i) => {
2315
+ const start = new Date(monday.getFullYear(), monday.getMonth(), monday.getDate() + i * 7);
2316
+ const end = new Date(start.getFullYear(), start.getMonth(), start.getDate() + 6);
2317
+ return { id: `w${i}`, start, end, label: formatMessage(this.p.schedule.week, { date: format.format(start) }) };
2318
+ });
2319
+ }, ...(ngDevMode ? [{ debugName: "ranges" }] : /* istanbul ignore next */ []));
2320
+ this.weeks = computed(() => this.ranges().map((r) => ({ id: r.id, label: r.label })), ...(ngDevMode ? [{ debugName: "weeks" }] : /* istanbul ignore next */ []));
2321
+ this.allocations = computed(() => taskWorkloadAllocations(this.s.open(), this.ranges()), ...(ngDevMode ? [{ debugName: "allocations" }] : /* istanbul ignore next */ []));
2322
+ // --- marcos ---
2323
+ this.title = signal('', ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
2324
+ this.date = signal(null, ...(ngDevMode ? [{ debugName: "date" }] : /* istanbul ignore next */ []));
2325
+ this.missing = signal(false, ...(ngDevMode ? [{ debugName: "missing" }] : /* istanbul ignore next */ []));
2326
+ this.saving = signal(false, ...(ngDevMode ? [{ debugName: "saving" }] : /* istanbul ignore next */ []));
2327
+ }
2328
+ async addMilestone() {
2329
+ const title = this.title().trim();
2330
+ const date = this.date();
2331
+ this.missing.set(!title || !date);
2332
+ if (!title || !date)
2333
+ return;
2334
+ this.saving.set(true);
2335
+ const ok = await this.s.createMilestone(title, date);
2336
+ this.saving.set(false);
2337
+ if (ok) {
2338
+ this.title.set('');
2339
+ this.date.set(null);
2340
+ }
2341
+ }
2342
+ removeLabel(title) {
2343
+ return formatMessage(this.p.schedule.removeMilestone, { title });
2344
+ }
2345
+ dayText(iso) {
2346
+ return new Intl.DateTimeFormat(this.s.locale, { day: 'numeric', month: 'short', year: 'numeric', timeZone: 'UTC' })
2347
+ .format(new Date(`${iso}T00:00:00Z`));
2348
+ }
2349
+ // --- gestos ---
2350
+ onClick(item) {
2351
+ if (!item.milestone)
2352
+ this.openTask.emit(item.id);
2353
+ }
2354
+ async onChange(change) {
2355
+ const start = isoDay(new Date(change.start));
2356
+ if (change.item.milestone) {
2357
+ await this.s.moveMilestone(change.id.slice(MILESTONE_PREFIX.length), start);
2358
+ return;
2359
+ }
2360
+ const task = this.s.items().find((t) => t.id === change.id);
2361
+ if (!task)
2362
+ return;
2363
+ const ok = await this.s.updateTask(change.id, { start_date: start, due_date: isoDay(new Date(change.end)) });
2364
+ if (!ok)
2365
+ return;
2366
+ const key = task.key ?? task.title;
2367
+ if (change.conflicts.length) {
2368
+ this.toast.warning(formatMessage(this.p.schedule.conflicts, { key, list: change.conflicts.map((c) => c.label).join(', ') }));
2369
+ }
2370
+ else {
2371
+ this.toast.success(formatMessage(this.p.schedule.rescheduled, { key }));
2372
+ }
2373
+ }
2374
+ onCell(event) {
2375
+ this.person.emit(event.person.id);
2376
+ }
2377
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionScheduleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2378
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SuggestionScheduleComponent, isStandalone: true, selector: "spine-suggestion-schedule", outputs: { openTask: "openTask", person: "person" }, ngImport: i0, template: `
2379
+ <section class="plan-card" aria-labelledby="sug-sched-gantt">
2380
+ <h3 id="sug-sched-gantt">{{ p.schedule.heading }}</h3>
2381
+ <p class="plan-muted">{{ p.schedule.hint }}</p>
2382
+ <hm-gantt [label]="p.schedule.label" [items]="items()" scale="week" editable interactive
2383
+ (itemClick)="onClick($event)" (taskChange)="onChange($event)" />
2384
+ </section>
2385
+
2386
+ <section class="plan-card" aria-labelledby="sug-sched-milestones">
2387
+ <h3 id="sug-sched-milestones">{{ p.schedule.milestones }}</h3>
2388
+ <form class="plan-row" (submit)="$event.preventDefault(); addMilestone()" novalidate>
2389
+ <hm-input [label]="p.schedule.milestoneTitle" [maxLength]="120" [value]="title()" (valueChange)="title.set($any($event))" />
2390
+ <hm-date-picker [label]="p.schedule.milestoneDate" [value]="date()" (valueChange)="date.set($event)" />
2391
+ <hm-button type="submit" emphasis="medium" icon="plus" [loading]="saving()">{{ p.schedule.newMilestone }}</hm-button>
2392
+ </form>
2393
+ @if (missing()) {
2394
+ <p class="plan-muted" role="alert">{{ p.schedule.milestoneRequired }}</p>
2395
+ }
2396
+ @if (s.milestones().length) {
2397
+ <ul class="plan-rows">
2398
+ @for (m of s.milestones(); track m.id) {
2399
+ <li>
2400
+ <span>{{ m.title }} <span class="plan-muted">{{ dayText(m.date) }}</span></span>
2401
+ <hm-icon-button icon="trash" size="small" [label]="removeLabel(m.title)" (clicked)="s.removeMilestone(m.id)" />
2402
+ </li>
2403
+ }
2404
+ </ul>
2405
+ }
2406
+ </section>
2407
+
2408
+ <section class="plan-card" aria-labelledby="sug-sched-load">
2409
+ <h3 id="sug-sched-load">{{ p.schedule.workloadHeading }}</h3>
2410
+ <p class="plan-muted">{{ capacityHint() }}</p>
2411
+ @if (team().length) {
2412
+ <hm-workload [label]="p.schedule.workloadLabel" [people]="team()" [periods]="weeks()" [allocations]="allocations()" unit="pts"
2413
+ interactive (cellClick)="onCell($event)" />
2414
+ } @else {
2415
+ <p class="plan-muted">{{ p.schedule.noPeople }}</p>
2416
+ }
2417
+ </section>
2418
+ `, isInline: true, styles: [":host{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--hm-space-md);min-inline-size:0}.plan-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,20rem),1fr));gap:var(--hm-space-md)}.plan-card{display:grid;grid-template-columns:minmax(0,1fr);align-content:start;gap:var(--hm-space-sm);min-inline-size:0;padding:var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.plan-card h3,.plan-card h4{margin:0;font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold);line-height:var(--hm-line-height-snug)}.plan-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.plan-toolbar__end{margin-inline-start:auto}.plan-muted{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-normal)}.plan-stats{display:flex;flex-wrap:wrap;gap:var(--hm-space-md);margin:0}.plan-stats div{display:grid;gap:var(--hm-space-nano);min-inline-size:0}.plan-stats dt{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.plan-stats dd{margin:0;font-size:var(--hm-font-size-lg);font-weight:var(--hm-font-weight-semibold)}.plan-text{margin:0;font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-snug)}.plan-title{font-weight:var(--hm-font-weight-medium)}.plan-rows{display:grid;gap:var(--hm-space-xs);margin:0;padding:0;font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-snug);list-style:none}.plan-rows li{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs) var(--hm-space-sm);min-inline-size:0}.plan-link{min-inline-size:0;padding:0;border:0;background:none;color:var(--hm-accent-strong);font:inherit;font-weight:var(--hm-font-weight-medium);text-align:start;cursor:pointer}.plan-link:hover{text-decoration:underline}.plan-link:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:var(--hm-border-thin)}.plan-key{color:var(--hm-muted);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-medium)}.plan-form{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--hm-space-sm)}.plan-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,12rem),1fr));gap:var(--hm-space-sm)}.plan-footer{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.plan-footer__start{margin-inline-end:auto}.plan-table-wrap{overflow-x:auto}.plan-table{inline-size:100%;border-collapse:collapse;font-size:var(--hm-font-size-sm);font-variant-numeric:tabular-nums}.plan-table caption{padding-block-end:var(--hm-space-xs);color:var(--hm-muted);text-align:start}.plan-table th,.plan-table td{padding:var(--hm-space-nano) var(--hm-space-xs);border-block-end:var(--hm-border-hairline) solid var(--hm-border);text-align:end;white-space:nowrap}.plan-table th:first-child,.plan-table td:first-child{text-align:start}\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: DatePickerComponent, selector: "hm-date-picker", inputs: ["value", "label", "labelHidden", "hint", "placeholder", "min", "max", "valueFormat"], outputs: ["valueChange"] }, { kind: "component", type: GanttComponent, selector: "hm-gantt", inputs: ["items", "scale", "scaleControl", "interactive", "label", "now", "texts", "editable", "editStep", "blockConflicts"], outputs: ["scaleChange", "itemClick", "taskChange"] }, { 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: 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: WorkloadComponent, selector: "hm-workload", inputs: ["people", "periods", "allocations", "unit", "interactive", "label", "texts"], outputs: ["cellClick"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2419
+ }
2420
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionScheduleComponent, decorators: [{
2421
+ type: Component,
2422
+ args: [{ selector: 'spine-suggestion-schedule', imports: [ButtonComponent, DatePickerComponent, GanttComponent, IconButtonComponent, InputComponent, WorkloadComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
2423
+ <section class="plan-card" aria-labelledby="sug-sched-gantt">
2424
+ <h3 id="sug-sched-gantt">{{ p.schedule.heading }}</h3>
2425
+ <p class="plan-muted">{{ p.schedule.hint }}</p>
2426
+ <hm-gantt [label]="p.schedule.label" [items]="items()" scale="week" editable interactive
2427
+ (itemClick)="onClick($event)" (taskChange)="onChange($event)" />
2428
+ </section>
2429
+
2430
+ <section class="plan-card" aria-labelledby="sug-sched-milestones">
2431
+ <h3 id="sug-sched-milestones">{{ p.schedule.milestones }}</h3>
2432
+ <form class="plan-row" (submit)="$event.preventDefault(); addMilestone()" novalidate>
2433
+ <hm-input [label]="p.schedule.milestoneTitle" [maxLength]="120" [value]="title()" (valueChange)="title.set($any($event))" />
2434
+ <hm-date-picker [label]="p.schedule.milestoneDate" [value]="date()" (valueChange)="date.set($event)" />
2435
+ <hm-button type="submit" emphasis="medium" icon="plus" [loading]="saving()">{{ p.schedule.newMilestone }}</hm-button>
2436
+ </form>
2437
+ @if (missing()) {
2438
+ <p class="plan-muted" role="alert">{{ p.schedule.milestoneRequired }}</p>
2439
+ }
2440
+ @if (s.milestones().length) {
2441
+ <ul class="plan-rows">
2442
+ @for (m of s.milestones(); track m.id) {
2443
+ <li>
2444
+ <span>{{ m.title }} <span class="plan-muted">{{ dayText(m.date) }}</span></span>
2445
+ <hm-icon-button icon="trash" size="small" [label]="removeLabel(m.title)" (clicked)="s.removeMilestone(m.id)" />
2446
+ </li>
2447
+ }
2448
+ </ul>
2449
+ }
2450
+ </section>
2451
+
2452
+ <section class="plan-card" aria-labelledby="sug-sched-load">
2453
+ <h3 id="sug-sched-load">{{ p.schedule.workloadHeading }}</h3>
2454
+ <p class="plan-muted">{{ capacityHint() }}</p>
2455
+ @if (team().length) {
2456
+ <hm-workload [label]="p.schedule.workloadLabel" [people]="team()" [periods]="weeks()" [allocations]="allocations()" unit="pts"
2457
+ interactive (cellClick)="onCell($event)" />
2458
+ } @else {
2459
+ <p class="plan-muted">{{ p.schedule.noPeople }}</p>
2460
+ }
2461
+ </section>
2462
+ `, styles: [":host{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--hm-space-md);min-inline-size:0}.plan-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,20rem),1fr));gap:var(--hm-space-md)}.plan-card{display:grid;grid-template-columns:minmax(0,1fr);align-content:start;gap:var(--hm-space-sm);min-inline-size:0;padding:var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.plan-card h3,.plan-card h4{margin:0;font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold);line-height:var(--hm-line-height-snug)}.plan-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.plan-toolbar__end{margin-inline-start:auto}.plan-muted{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-normal)}.plan-stats{display:flex;flex-wrap:wrap;gap:var(--hm-space-md);margin:0}.plan-stats div{display:grid;gap:var(--hm-space-nano);min-inline-size:0}.plan-stats dt{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.plan-stats dd{margin:0;font-size:var(--hm-font-size-lg);font-weight:var(--hm-font-weight-semibold)}.plan-text{margin:0;font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-snug)}.plan-title{font-weight:var(--hm-font-weight-medium)}.plan-rows{display:grid;gap:var(--hm-space-xs);margin:0;padding:0;font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-snug);list-style:none}.plan-rows li{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs) var(--hm-space-sm);min-inline-size:0}.plan-link{min-inline-size:0;padding:0;border:0;background:none;color:var(--hm-accent-strong);font:inherit;font-weight:var(--hm-font-weight-medium);text-align:start;cursor:pointer}.plan-link:hover{text-decoration:underline}.plan-link:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:var(--hm-border-thin)}.plan-key{color:var(--hm-muted);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-medium)}.plan-form{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--hm-space-sm)}.plan-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,12rem),1fr));gap:var(--hm-space-sm)}.plan-footer{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.plan-footer__start{margin-inline-end:auto}.plan-table-wrap{overflow-x:auto}.plan-table{inline-size:100%;border-collapse:collapse;font-size:var(--hm-font-size-sm);font-variant-numeric:tabular-nums}.plan-table caption{padding-block-end:var(--hm-space-xs);color:var(--hm-muted);text-align:start}.plan-table th,.plan-table td{padding:var(--hm-space-nano) var(--hm-space-xs);border-block-end:var(--hm-border-hairline) solid var(--hm-border);text-align:end;white-space:nowrap}.plan-table th:first-child,.plan-table td:first-child{text-align:start}\n"] }]
2463
+ }], propDecorators: { openTask: [{ type: i0.Output, args: ["openTask"] }], person: [{ type: i0.Output, args: ["person"] }] } });
2464
+
2465
+ /** Risco alto: probabilidade x impacto a partir do terceiro degrau da matriz do `hm-risk-register` (9 de 25). */
2466
+ const HIGH_RISK = 9;
2467
+ /** Quantas linhas cada bloco mostra. */
2468
+ const ROWS = 6;
2469
+ /**
2470
+ * Resumo do quadro: a sprint em andamento (meta, datas e pontos concluídos/comprometidos contra a capacidade), as
2471
+ * tarefas abertas de quem consulta (`hm-task-list`), os prazos vencidos e dos próximos 7 dias (`hm-deadline`), os
2472
+ * riscos altos em aberto e o próximo marco. Clicar numa tarefa abre o detalhe (o quadro cuida dele).
2473
+ */
2474
+ class SuggestionSummaryComponent {
2475
+ constructor() {
2476
+ this.s = inject(SuggestionPlanningStore);
2477
+ this.p = this.s.plan;
2478
+ /** Hoje (AAAA-MM-DD no fuso de quem consulta), do quadro. */
2479
+ this.today = input.required(...(ngDevMode ? [{ debugName: "today" }] : /* istanbul ignore next */ []));
2480
+ this.openTask = output();
2481
+ this.sprintPoints = computed(() => {
2482
+ const sprint = this.s.activeSprint();
2483
+ return sprint ? formatMessage(this.p.summary.sprintPoints, { done: sprint.done_points, total: sprint.points }) : '';
2484
+ }, ...(ngDevMode ? [{ debugName: "sprintPoints" }] : /* istanbul ignore next */ []));
2485
+ this.capacityHint = computed(() => {
2486
+ const capacity = this.s.activeSprint()?.capacity;
2487
+ return capacity === null || capacity === undefined ? '' : formatMessage(this.p.summary.sprintCapacity, { capacity });
2488
+ }, ...(ngDevMode ? [{ debugName: "capacityHint" }] : /* istanbul ignore next */ []));
2489
+ this.sprintOpen = computed(() => {
2490
+ const id = this.s.activeSprint()?.id;
2491
+ return this.s.open().filter((t) => t.sprintId === id).length;
2492
+ }, ...(ngDevMode ? [{ debugName: "sprintOpen" }] : /* istanbul ignore next */ []));
2493
+ this.openPoints = computed(() => sumEstimates(this.s.open()), ...(ngDevMode ? [{ debugName: "openPoints" }] : /* istanbul ignore next */ []));
2494
+ this.overdue = computed(() => this.s.open().filter((t) => t.task.due_date && t.task.due_date < this.today()).length, ...(ngDevMode ? [{ debugName: "overdue" }] : /* istanbul ignore next */ []));
2495
+ this.mine = computed(() => this.s.open().filter((t) => t.task.assignees.some((p) => p.user_id === this.s.me())).slice(0, ROWS * 2), ...(ngDevMode ? [{ debugName: "mine" }] : /* istanbul ignore next */ []));
2496
+ /** Vencidas ou com prazo até daqui a 7 dias, da mais urgente para a menos. */
2497
+ this.deadlines = computed(() => {
2498
+ const limit = addDays(this.today(), 7);
2499
+ return this.s.open()
2500
+ .filter((t) => t.task.due_date && t.task.due_date <= limit)
2501
+ .sort((a, b) => a.task.due_date.localeCompare(b.task.due_date))
2502
+ .slice(0, ROWS);
2503
+ }, ...(ngDevMode ? [{ debugName: "deadlines" }] : /* istanbul ignore next */ []));
2504
+ this.topRisks = computed(() => this.s.risks()
2505
+ .filter((r) => r.status !== 'closed' && r.likelihood * r.impact >= HIGH_RISK)
2506
+ .slice(0, ROWS), ...(ngDevMode ? [{ debugName: "topRisks" }] : /* istanbul ignore next */ []));
2507
+ this.nextMilestone = computed(() => this.s.milestones().find((m) => m.date >= this.today()) ?? null, ...(ngDevMode ? [{ debugName: "nextMilestone" }] : /* istanbul ignore next */ []));
2508
+ }
2509
+ riskScore(likelihood, impact) {
2510
+ return formatMessage(this.p.summary.riskScore, { likelihood, impact });
2511
+ }
2512
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionSummaryComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2513
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SuggestionSummaryComponent, isStandalone: true, selector: "spine-suggestion-summary", inputs: { today: { classPropertyName: "today", publicName: "today", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { openTask: "openTask" }, ngImport: i0, template: `
2514
+ <div class="plan-grid">
2515
+ <section class="plan-card" aria-labelledby="sug-sum-sprint">
2516
+ <h3 id="sug-sum-sprint">{{ s.activeSprint()?.name ?? p.summary.activeSprint }}</h3>
2517
+ @if (s.activeSprint(); as sprint) {
2518
+ <p class="plan-muted">{{ s.rangeText(sprint.start_date, sprint.end_date) }}@if (sprint.goal) { · {{ sprint.goal }} }</p>
2519
+ <hm-progress [label]="sprintPoints()" [hint]="capacityHint()" [value]="sprint.done_points" [max]="sprint.points || 1" intent="success" showValue />
2520
+ <dl class="plan-stats">
2521
+ <div><dt>{{ p.sprints.committed }}</dt><dd>{{ s.pointsText(sprint.points) }}</dd></div>
2522
+ <div><dt>{{ p.summary.openTasks }}</dt><dd>{{ sprintOpen() }}</dd></div>
2523
+ </dl>
2524
+ } @else {
2525
+ <p class="plan-muted">{{ p.noSprint }}</p>
2526
+ }
2527
+ <dl class="plan-stats">
2528
+ <div><dt>{{ p.summary.openTasks }}</dt><dd>{{ s.open().length }}</dd></div>
2529
+ <div><dt>{{ p.summary.overdue }}</dt><dd>{{ overdue() }}</dd></div>
2530
+ <div><dt>{{ p.points }}</dt><dd>{{ s.pointsText(openPoints()) }}</dd></div>
2531
+ </dl>
2532
+ </section>
2533
+
2534
+ <section class="plan-card" aria-labelledby="sug-sum-mine">
2535
+ <h3 id="sug-sum-mine">{{ p.summary.mine }}</h3>
2536
+ @if (mine().length) {
2537
+ <hm-task-list [label]="p.summary.mine" [tasks]="mine()" [statuses]="s.statuses()" openable (open)="openTask.emit($event.id)" />
2538
+ } @else {
2539
+ <p class="plan-muted">{{ p.summary.mineEmpty }}</p>
2540
+ }
2541
+ </section>
2542
+
2543
+ <section class="plan-card" aria-labelledby="sug-sum-due">
2544
+ <h3 id="sug-sum-due">{{ p.summary.deadlines }}</h3>
2545
+ @if (deadlines().length) {
2546
+ <ul class="plan-rows">
2547
+ @for (t of deadlines(); track t.id) {
2548
+ <li>
2549
+ <button type="button" class="plan-link" (click)="openTask.emit(t.id)"><span class="plan-key">{{ t.key }}</span> {{ t.title }}</button>
2550
+ <hm-deadline [due]="t.due" />
2551
+ </li>
2552
+ }
2553
+ </ul>
2554
+ } @else {
2555
+ <p class="plan-muted">{{ p.summary.deadlinesEmpty }}</p>
2556
+ }
2557
+ </section>
2558
+
2559
+ <section class="plan-card" aria-labelledby="sug-sum-risks">
2560
+ <h3 id="sug-sum-risks">{{ p.summary.topRisks }}</h3>
2561
+ @if (topRisks().length) {
2562
+ <ul class="plan-rows">
2563
+ @for (r of topRisks(); track r.id) {
2564
+ <li>
2565
+ <span class="plan-title">{{ r.title }}</span>
2566
+ <hm-tag [name]="riskScore(r.likelihood, r.impact)" [color]="r.likelihood * r.impact >= 16 ? 'danger' : 'warning'" />
2567
+ </li>
2568
+ }
2569
+ </ul>
2570
+ } @else {
2571
+ <p class="plan-muted">{{ p.summary.topRisksEmpty }}</p>
2572
+ }
2573
+ <h4>{{ p.summary.nextMilestone }}</h4>
2574
+ @if (nextMilestone(); as m) {
2575
+ <p class="plan-text"><span class="plan-title">{{ m.title }}</span> · <hm-deadline [due]="m.date" /></p>
2576
+ } @else {
2577
+ <p class="plan-muted">{{ p.summary.nextMilestoneEmpty }}</p>
2578
+ }
2579
+ </section>
2580
+ </div>
2581
+ `, isInline: true, styles: [":host{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--hm-space-md);min-inline-size:0}.plan-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,20rem),1fr));gap:var(--hm-space-md)}.plan-card{display:grid;grid-template-columns:minmax(0,1fr);align-content:start;gap:var(--hm-space-sm);min-inline-size:0;padding:var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.plan-card h3,.plan-card h4{margin:0;font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold);line-height:var(--hm-line-height-snug)}.plan-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.plan-toolbar__end{margin-inline-start:auto}.plan-muted{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-normal)}.plan-stats{display:flex;flex-wrap:wrap;gap:var(--hm-space-md);margin:0}.plan-stats div{display:grid;gap:var(--hm-space-nano);min-inline-size:0}.plan-stats dt{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.plan-stats dd{margin:0;font-size:var(--hm-font-size-lg);font-weight:var(--hm-font-weight-semibold)}.plan-text{margin:0;font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-snug)}.plan-title{font-weight:var(--hm-font-weight-medium)}.plan-rows{display:grid;gap:var(--hm-space-xs);margin:0;padding:0;font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-snug);list-style:none}.plan-rows li{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs) var(--hm-space-sm);min-inline-size:0}.plan-link{min-inline-size:0;padding:0;border:0;background:none;color:var(--hm-accent-strong);font:inherit;font-weight:var(--hm-font-weight-medium);text-align:start;cursor:pointer}.plan-link:hover{text-decoration:underline}.plan-link:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:var(--hm-border-thin)}.plan-key{color:var(--hm-muted);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-medium)}.plan-form{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--hm-space-sm)}.plan-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,12rem),1fr));gap:var(--hm-space-sm)}.plan-footer{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.plan-footer__start{margin-inline-end:auto}.plan-table-wrap{overflow-x:auto}.plan-table{inline-size:100%;border-collapse:collapse;font-size:var(--hm-font-size-sm);font-variant-numeric:tabular-nums}.plan-table caption{padding-block-end:var(--hm-space-xs);color:var(--hm-muted);text-align:start}.plan-table th,.plan-table td{padding:var(--hm-space-nano) var(--hm-space-xs);border-block-end:var(--hm-border-hairline) solid var(--hm-border);text-align:end;white-space:nowrap}.plan-table th:first-child,.plan-table td:first-child{text-align:start}\n"], dependencies: [{ kind: "component", type: DeadlineComponent, selector: "hm-deadline", inputs: ["due", "completedAt", "variant", "soonHours", "now", "texts"], outputs: ["expired"] }, { kind: "component", type: ProgressComponent, selector: "hm-progress", inputs: ["value", "max", "label", "labelHidden", "showValue", "indeterminate", "intent", "size", "hint", "shape"] }, { kind: "component", type: TagComponent, selector: "hm-tag", inputs: ["name", "color", "compact"] }, { kind: "component", type: TaskListComponent, selector: "hm-task-list", inputs: ["tasks", "statuses", "priorityScale", "checkable", "openable", "disabled", "label", "now", "texts"], outputs: ["doneChange", "open"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2582
+ }
2583
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionSummaryComponent, decorators: [{
2584
+ type: Component,
2585
+ args: [{ selector: 'spine-suggestion-summary', imports: [DeadlineComponent, ProgressComponent, TagComponent, TaskListComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
2586
+ <div class="plan-grid">
2587
+ <section class="plan-card" aria-labelledby="sug-sum-sprint">
2588
+ <h3 id="sug-sum-sprint">{{ s.activeSprint()?.name ?? p.summary.activeSprint }}</h3>
2589
+ @if (s.activeSprint(); as sprint) {
2590
+ <p class="plan-muted">{{ s.rangeText(sprint.start_date, sprint.end_date) }}@if (sprint.goal) { · {{ sprint.goal }} }</p>
2591
+ <hm-progress [label]="sprintPoints()" [hint]="capacityHint()" [value]="sprint.done_points" [max]="sprint.points || 1" intent="success" showValue />
2592
+ <dl class="plan-stats">
2593
+ <div><dt>{{ p.sprints.committed }}</dt><dd>{{ s.pointsText(sprint.points) }}</dd></div>
2594
+ <div><dt>{{ p.summary.openTasks }}</dt><dd>{{ sprintOpen() }}</dd></div>
2595
+ </dl>
2596
+ } @else {
2597
+ <p class="plan-muted">{{ p.noSprint }}</p>
2598
+ }
2599
+ <dl class="plan-stats">
2600
+ <div><dt>{{ p.summary.openTasks }}</dt><dd>{{ s.open().length }}</dd></div>
2601
+ <div><dt>{{ p.summary.overdue }}</dt><dd>{{ overdue() }}</dd></div>
2602
+ <div><dt>{{ p.points }}</dt><dd>{{ s.pointsText(openPoints()) }}</dd></div>
2603
+ </dl>
2604
+ </section>
2605
+
2606
+ <section class="plan-card" aria-labelledby="sug-sum-mine">
2607
+ <h3 id="sug-sum-mine">{{ p.summary.mine }}</h3>
2608
+ @if (mine().length) {
2609
+ <hm-task-list [label]="p.summary.mine" [tasks]="mine()" [statuses]="s.statuses()" openable (open)="openTask.emit($event.id)" />
2610
+ } @else {
2611
+ <p class="plan-muted">{{ p.summary.mineEmpty }}</p>
2612
+ }
2613
+ </section>
2614
+
2615
+ <section class="plan-card" aria-labelledby="sug-sum-due">
2616
+ <h3 id="sug-sum-due">{{ p.summary.deadlines }}</h3>
2617
+ @if (deadlines().length) {
2618
+ <ul class="plan-rows">
2619
+ @for (t of deadlines(); track t.id) {
2620
+ <li>
2621
+ <button type="button" class="plan-link" (click)="openTask.emit(t.id)"><span class="plan-key">{{ t.key }}</span> {{ t.title }}</button>
2622
+ <hm-deadline [due]="t.due" />
2623
+ </li>
2624
+ }
2625
+ </ul>
2626
+ } @else {
2627
+ <p class="plan-muted">{{ p.summary.deadlinesEmpty }}</p>
2628
+ }
2629
+ </section>
2630
+
2631
+ <section class="plan-card" aria-labelledby="sug-sum-risks">
2632
+ <h3 id="sug-sum-risks">{{ p.summary.topRisks }}</h3>
2633
+ @if (topRisks().length) {
2634
+ <ul class="plan-rows">
2635
+ @for (r of topRisks(); track r.id) {
2636
+ <li>
2637
+ <span class="plan-title">{{ r.title }}</span>
2638
+ <hm-tag [name]="riskScore(r.likelihood, r.impact)" [color]="r.likelihood * r.impact >= 16 ? 'danger' : 'warning'" />
2639
+ </li>
2640
+ }
2641
+ </ul>
2642
+ } @else {
2643
+ <p class="plan-muted">{{ p.summary.topRisksEmpty }}</p>
2644
+ }
2645
+ <h4>{{ p.summary.nextMilestone }}</h4>
2646
+ @if (nextMilestone(); as m) {
2647
+ <p class="plan-text"><span class="plan-title">{{ m.title }}</span> · <hm-deadline [due]="m.date" /></p>
2648
+ } @else {
2649
+ <p class="plan-muted">{{ p.summary.nextMilestoneEmpty }}</p>
2650
+ }
2651
+ </section>
2652
+ </div>
2653
+ `, styles: [":host{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--hm-space-md);min-inline-size:0}.plan-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,20rem),1fr));gap:var(--hm-space-md)}.plan-card{display:grid;grid-template-columns:minmax(0,1fr);align-content:start;gap:var(--hm-space-sm);min-inline-size:0;padding:var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.plan-card h3,.plan-card h4{margin:0;font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold);line-height:var(--hm-line-height-snug)}.plan-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.plan-toolbar__end{margin-inline-start:auto}.plan-muted{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-normal)}.plan-stats{display:flex;flex-wrap:wrap;gap:var(--hm-space-md);margin:0}.plan-stats div{display:grid;gap:var(--hm-space-nano);min-inline-size:0}.plan-stats dt{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.plan-stats dd{margin:0;font-size:var(--hm-font-size-lg);font-weight:var(--hm-font-weight-semibold)}.plan-text{margin:0;font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-snug)}.plan-title{font-weight:var(--hm-font-weight-medium)}.plan-rows{display:grid;gap:var(--hm-space-xs);margin:0;padding:0;font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-snug);list-style:none}.plan-rows li{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs) var(--hm-space-sm);min-inline-size:0}.plan-link{min-inline-size:0;padding:0;border:0;background:none;color:var(--hm-accent-strong);font:inherit;font-weight:var(--hm-font-weight-medium);text-align:start;cursor:pointer}.plan-link:hover{text-decoration:underline}.plan-link:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:var(--hm-border-thin)}.plan-key{color:var(--hm-muted);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-medium)}.plan-form{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--hm-space-sm)}.plan-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,12rem),1fr));gap:var(--hm-space-sm)}.plan-footer{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.plan-footer__start{margin-inline-end:auto}.plan-table-wrap{overflow-x:auto}.plan-table{inline-size:100%;border-collapse:collapse;font-size:var(--hm-font-size-sm);font-variant-numeric:tabular-nums}.plan-table caption{padding-block-end:var(--hm-space-xs);color:var(--hm-muted);text-align:start}.plan-table th,.plan-table td{padding:var(--hm-space-nano) var(--hm-space-xs);border-block-end:var(--hm-border-hairline) solid var(--hm-border);text-align:end;white-space:nowrap}.plan-table th:first-child,.plan-table td:first-child{text-align:start}\n"] }]
2654
+ }], propDecorators: { today: [{ type: i0.Input, args: [{ isSignal: true, alias: "today", required: true }] }], openTask: [{ type: i0.Output, args: ["openTask"] }] } });
2655
+
2656
+ const PRIORITY_RANK$1 = { urgente: 0, alta: 1, media: 2, baixa: 3 };
2657
+ /**
2658
+ * As tarefas abertas do app (em análise, planejadas e em desenvolvimento) em lista (`hm-task-list`, agrupada pelo
2659
+ * status) ou em tabela (`hm-data-grid`, ordenável, com sprint e pontos). Abrir leva ao detalhe da tarefa, que o quadro
2660
+ * mostra; é lá que se edita.
2661
+ */
2662
+ class SuggestionTasksComponent {
2663
+ constructor() {
2664
+ this.s = inject(SuggestionPlanningStore);
2665
+ this.p = this.s.plan;
2666
+ this.locale = inject(SPINE_MESSAGES).meta.locale;
2667
+ this.openTask = output();
2668
+ this.view = signal('list', ...(ngDevMode ? [{ debugName: "view" }] : /* istanbul ignore next */ []));
2669
+ this.byId = (t) => t.id;
2670
+ this.views = [
2671
+ { value: 'list', label: this.p.tasks.viewList, icon: 'list' },
2672
+ { value: 'table', label: this.p.tasks.viewTable, icon: 'table' },
2673
+ ];
2674
+ this.actions = [{ id: 'open', label: this.p.tasks.open, icon: 'eye' }];
2675
+ /** Pela ordem do fluxo (as que estão mais perto de sair primeiro) e, no mesmo status, pela prioridade. */
2676
+ this.rows = computed(() => {
2677
+ const order = ['em_desenvolvimento', 'planejada', 'em_analise'];
2678
+ return [...this.s.open()].sort((a, b) => order.indexOf(a.task.status) - order.indexOf(b.task.status)
2679
+ || PRIORITY_RANK$1[a.task.priority] - PRIORITY_RANK$1[b.task.priority] || a.task.position - b.task.position);
2680
+ }, ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
2681
+ this.countText = computed(() => pluralMessage(this.p.tasks.count, this.rows().length, this.locale), ...(ngDevMode ? [{ debugName: "countText" }] : /* istanbul ignore next */ []));
2682
+ this.columns = computed(() => {
2683
+ const t = this.p.tasks;
2684
+ const text = this.s.text;
2685
+ const date = (iso) => (iso
2686
+ ? new Intl.DateTimeFormat(this.locale, { day: 'numeric', month: 'short', timeZone: 'UTC' }).format(new Date(`${iso}T00:00:00Z`))
2687
+ : '—');
2688
+ return [
2689
+ { key: 'key', title: t.key, value: (r) => r.key ?? '', initialWidth: '6rem' },
2690
+ { key: 'title', title: t.title, value: (r) => r.title, initialWidth: '18rem' },
2691
+ { key: 'status', title: t.status, value: (r) => text.statuses[r.task.status], initialWidth: '10rem' },
2692
+ {
2693
+ key: 'priority', title: t.priority, value: (r) => text.priorities[r.task.priority],
2694
+ raw: (r) => PRIORITY_RANK$1[r.task.priority],
2695
+ },
2696
+ { key: 'assignee', title: t.assignee, value: (r) => r.assignee?.name ?? this.p.unassigned, initialWidth: '11rem' },
2697
+ { key: 'sprint', title: t.sprint, value: (r) => (r.sprintId ? this.s.sprintName().get(r.sprintId) ?? '—' : this.p.backlog) },
2698
+ {
2699
+ key: 'estimate', title: this.p.points, type: 'number', align: 'end',
2700
+ value: (r) => (r.estimate === null || r.estimate === undefined ? '—' : String(r.estimate)), raw: (r) => r.estimate ?? null,
2701
+ },
2702
+ { key: 'due', title: t.due, type: 'date', value: (r) => date(r.task.due_date), raw: (r) => r.task.due_date ?? '' },
2703
+ {
2704
+ key: 'risk', title: this.p.risks.level,
2705
+ value: (r) => {
2706
+ const score = this.s.riskScoreOf(r.task);
2707
+ return score === null ? '—' : `${this.p.risks.levels[riskLevel(score)]} (${score})`;
2708
+ },
2709
+ raw: (r) => this.s.riskScoreOf(r.task) ?? 0,
2710
+ },
2711
+ ];
2712
+ }, ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
2713
+ }
2714
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionTasksComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2715
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SuggestionTasksComponent, isStandalone: true, selector: "spine-suggestion-tasks", outputs: { openTask: "openTask" }, ngImport: i0, template: `
2716
+ <div class="plan-toolbar">
2717
+ <hm-segmented-control [label]="p.tasks.viewLabel" [options]="views" [value]="view()" (valueChange)="view.set($any($event))" size="small" />
2718
+ <p class="plan-muted" aria-live="polite">{{ countText() }}</p>
2719
+ </div>
2720
+ @if (!rows().length) {
2721
+ <hm-empty [heading]="p.tasks.emptyHeading" [description]="p.tasks.emptyBody" icon="tasks" />
2722
+ } @else if (view() === 'list') {
2723
+ <hm-task-list [label]="p.tasks.listLabel" [tasks]="rows()" [statuses]="s.statuses()" openable (open)="openTask.emit($event.id)" />
2724
+ } @else {
2725
+ <hm-data-grid
2726
+ [caption]="p.tasks.gridCaption"
2727
+ [columns]="columns()"
2728
+ [rows]="rows()"
2729
+ [rowId]="byId"
2730
+ [rowActions]="actions"
2731
+ [filterable]="false"
2732
+ [paginated]="false"
2733
+ [pagination]="false"
2734
+ (actionTriggered)="openTask.emit($event.row.id)"
2735
+ />
2736
+ }
2737
+ `, isInline: true, styles: [":host{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--hm-space-md);min-inline-size:0}.plan-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,20rem),1fr));gap:var(--hm-space-md)}.plan-card{display:grid;grid-template-columns:minmax(0,1fr);align-content:start;gap:var(--hm-space-sm);min-inline-size:0;padding:var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.plan-card h3,.plan-card h4{margin:0;font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold);line-height:var(--hm-line-height-snug)}.plan-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.plan-toolbar__end{margin-inline-start:auto}.plan-muted{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-normal)}.plan-stats{display:flex;flex-wrap:wrap;gap:var(--hm-space-md);margin:0}.plan-stats div{display:grid;gap:var(--hm-space-nano);min-inline-size:0}.plan-stats dt{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.plan-stats dd{margin:0;font-size:var(--hm-font-size-lg);font-weight:var(--hm-font-weight-semibold)}.plan-text{margin:0;font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-snug)}.plan-title{font-weight:var(--hm-font-weight-medium)}.plan-rows{display:grid;gap:var(--hm-space-xs);margin:0;padding:0;font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-snug);list-style:none}.plan-rows li{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs) var(--hm-space-sm);min-inline-size:0}.plan-link{min-inline-size:0;padding:0;border:0;background:none;color:var(--hm-accent-strong);font:inherit;font-weight:var(--hm-font-weight-medium);text-align:start;cursor:pointer}.plan-link:hover{text-decoration:underline}.plan-link:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:var(--hm-border-thin)}.plan-key{color:var(--hm-muted);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-medium)}.plan-form{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--hm-space-sm)}.plan-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,12rem),1fr));gap:var(--hm-space-sm)}.plan-footer{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.plan-footer__start{margin-inline-end:auto}.plan-table-wrap{overflow-x:auto}.plan-table{inline-size:100%;border-collapse:collapse;font-size:var(--hm-font-size-sm);font-variant-numeric:tabular-nums}.plan-table caption{padding-block-end:var(--hm-space-xs);color:var(--hm-muted);text-align:start}.plan-table th,.plan-table td{padding:var(--hm-space-nano) var(--hm-space-xs);border-block-end:var(--hm-border-hairline) solid var(--hm-border);text-align:end;white-space:nowrap}.plan-table th:first-child,.plan-table td:first-child{text-align:start}\n"], dependencies: [{ kind: "component", type: DataGridComponent, selector: "hm-data-grid", inputs: ["columns", "rows", "rowId", "caption", "rowActions", "loading", "error", "emptyMessage", "noResultsMessage", "filterable", "paginated", "pageState", "pagination", "totalItems", "sort", "selectable", "selectionMode", "selection", "rowSelectable", "rowSelectableReason", "rowDetailDisabled", "rowDetailHidden", "virtual", "rowHeight", "pinnedColumns", "columnState", "pageSizes", "fillHeight", "maxHeight", "bordered", "rowState", "rowClickable", "remoteSearch", "remoteSort", "itemsLabel", "pageSizeLabel", "exportable", "exportFileName", "groupBy", "aggregates", "aggregateFormatter", "persistKey", "persistStorage", "advancedFilter", "filterQuery", "columnManager", "columnOrder", "striped", "columnGroups", "editMode", "editBatch", "texts", "treeChildren", "treeHasChildren", "treeExpanded", "searchTerm"], outputs: ["pageStateChange", "sortChange", "selectionChange", "pinnedColumnsChange", "columnStateChange", "actionTriggered", "detailToggled", "rowClicked", "clearFiltersRequested", "filteredChange", "searchTermChanged", "groupByChange", "filterQueryChange", "columnOrderChange", "editSaved", "editStarted", "editCanceled", "treeExpandedChange", "treeToggled", "searchTermChange", "detailPaneToggled"] }, { kind: "component", type: EmptyComponent, selector: "hm-empty", inputs: ["heading", "description", "icon", "appearance", "size", "headingLevel"] }, { kind: "component", type: SegmentedControlComponent, selector: "hm-segmented-control", inputs: ["options", "value", "label", "size", "emphasis"], outputs: ["valueChange"] }, { kind: "component", type: TaskListComponent, selector: "hm-task-list", inputs: ["tasks", "statuses", "priorityScale", "checkable", "openable", "disabled", "label", "now", "texts"], outputs: ["doneChange", "open"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2738
+ }
2739
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionTasksComponent, decorators: [{
2740
+ type: Component,
2741
+ args: [{ selector: 'spine-suggestion-tasks', imports: [DataGridComponent, EmptyComponent, SegmentedControlComponent, TaskListComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
2742
+ <div class="plan-toolbar">
2743
+ <hm-segmented-control [label]="p.tasks.viewLabel" [options]="views" [value]="view()" (valueChange)="view.set($any($event))" size="small" />
2744
+ <p class="plan-muted" aria-live="polite">{{ countText() }}</p>
2745
+ </div>
2746
+ @if (!rows().length) {
2747
+ <hm-empty [heading]="p.tasks.emptyHeading" [description]="p.tasks.emptyBody" icon="tasks" />
2748
+ } @else if (view() === 'list') {
2749
+ <hm-task-list [label]="p.tasks.listLabel" [tasks]="rows()" [statuses]="s.statuses()" openable (open)="openTask.emit($event.id)" />
2750
+ } @else {
2751
+ <hm-data-grid
2752
+ [caption]="p.tasks.gridCaption"
2753
+ [columns]="columns()"
2754
+ [rows]="rows()"
2755
+ [rowId]="byId"
2756
+ [rowActions]="actions"
2757
+ [filterable]="false"
2758
+ [paginated]="false"
2759
+ [pagination]="false"
2760
+ (actionTriggered)="openTask.emit($event.row.id)"
2761
+ />
2762
+ }
2763
+ `, styles: [":host{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--hm-space-md);min-inline-size:0}.plan-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,20rem),1fr));gap:var(--hm-space-md)}.plan-card{display:grid;grid-template-columns:minmax(0,1fr);align-content:start;gap:var(--hm-space-sm);min-inline-size:0;padding:var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.plan-card h3,.plan-card h4{margin:0;font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold);line-height:var(--hm-line-height-snug)}.plan-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.plan-toolbar__end{margin-inline-start:auto}.plan-muted{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-normal)}.plan-stats{display:flex;flex-wrap:wrap;gap:var(--hm-space-md);margin:0}.plan-stats div{display:grid;gap:var(--hm-space-nano);min-inline-size:0}.plan-stats dt{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.plan-stats dd{margin:0;font-size:var(--hm-font-size-lg);font-weight:var(--hm-font-weight-semibold)}.plan-text{margin:0;font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-snug)}.plan-title{font-weight:var(--hm-font-weight-medium)}.plan-rows{display:grid;gap:var(--hm-space-xs);margin:0;padding:0;font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-snug);list-style:none}.plan-rows li{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs) var(--hm-space-sm);min-inline-size:0}.plan-link{min-inline-size:0;padding:0;border:0;background:none;color:var(--hm-accent-strong);font:inherit;font-weight:var(--hm-font-weight-medium);text-align:start;cursor:pointer}.plan-link:hover{text-decoration:underline}.plan-link:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:var(--hm-border-thin)}.plan-key{color:var(--hm-muted);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-medium)}.plan-form{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--hm-space-sm)}.plan-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,12rem),1fr));gap:var(--hm-space-sm)}.plan-footer{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.plan-footer__start{margin-inline-end:auto}.plan-table-wrap{overflow-x:auto}.plan-table{inline-size:100%;border-collapse:collapse;font-size:var(--hm-font-size-sm);font-variant-numeric:tabular-nums}.plan-table caption{padding-block-end:var(--hm-space-xs);color:var(--hm-muted);text-align:start}.plan-table th,.plan-table td{padding:var(--hm-space-nano) var(--hm-space-xs);border-block-end:var(--hm-border-hairline) solid var(--hm-border);text-align:end;white-space:nowrap}.plan-table th:first-child,.plan-table td:first-child{text-align:start}\n"] }]
2764
+ }], propDecorators: { openTask: [{ type: i0.Output, args: ["openTask"] }] } });
2765
+
2766
+ /**
2767
+ * O fluxo dos status das sugestões, só leitura (`hm-workflow-diagram`): é o contrato com quem pede, que acompanha a
2768
+ * sugestão por esses mesmos 5 status em "Minhas sugestões", então não muda por app. Cada status diz quantas há no quadro.
2769
+ */
2770
+ class SuggestionWorkflowComponent {
2771
+ constructor() {
2772
+ this.s = inject(SuggestionPlanningStore);
2773
+ this.p = this.s.plan;
2774
+ }
2775
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionWorkflowComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2776
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.24", type: SuggestionWorkflowComponent, isStandalone: true, selector: "spine-suggestion-workflow", ngImport: i0, template: `
2777
+ <section class="plan-card" aria-labelledby="sug-flow">
2778
+ <h3 id="sug-flow">{{ p.workflow.heading }}</h3>
2779
+ <p class="plan-muted">{{ p.workflow.hint }}</p>
2780
+ <hm-workflow-diagram [label]="p.workflow.label" [definition]="s.workflow()" />
2781
+ </section>
2782
+ `, isInline: true, styles: [":host{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--hm-space-md);min-inline-size:0}.plan-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,20rem),1fr));gap:var(--hm-space-md)}.plan-card{display:grid;grid-template-columns:minmax(0,1fr);align-content:start;gap:var(--hm-space-sm);min-inline-size:0;padding:var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.plan-card h3,.plan-card h4{margin:0;font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold);line-height:var(--hm-line-height-snug)}.plan-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.plan-toolbar__end{margin-inline-start:auto}.plan-muted{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-normal)}.plan-stats{display:flex;flex-wrap:wrap;gap:var(--hm-space-md);margin:0}.plan-stats div{display:grid;gap:var(--hm-space-nano);min-inline-size:0}.plan-stats dt{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.plan-stats dd{margin:0;font-size:var(--hm-font-size-lg);font-weight:var(--hm-font-weight-semibold)}.plan-text{margin:0;font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-snug)}.plan-title{font-weight:var(--hm-font-weight-medium)}.plan-rows{display:grid;gap:var(--hm-space-xs);margin:0;padding:0;font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-snug);list-style:none}.plan-rows li{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs) var(--hm-space-sm);min-inline-size:0}.plan-link{min-inline-size:0;padding:0;border:0;background:none;color:var(--hm-accent-strong);font:inherit;font-weight:var(--hm-font-weight-medium);text-align:start;cursor:pointer}.plan-link:hover{text-decoration:underline}.plan-link:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:var(--hm-border-thin)}.plan-key{color:var(--hm-muted);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-medium)}.plan-form{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--hm-space-sm)}.plan-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,12rem),1fr));gap:var(--hm-space-sm)}.plan-footer{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.plan-footer__start{margin-inline-end:auto}.plan-table-wrap{overflow-x:auto}.plan-table{inline-size:100%;border-collapse:collapse;font-size:var(--hm-font-size-sm);font-variant-numeric:tabular-nums}.plan-table caption{padding-block-end:var(--hm-space-xs);color:var(--hm-muted);text-align:start}.plan-table th,.plan-table td{padding:var(--hm-space-nano) var(--hm-space-xs);border-block-end:var(--hm-border-hairline) solid var(--hm-border);text-align:end;white-space:nowrap}.plan-table th:first-child,.plan-table td:first-child{text-align:start}\n"], dependencies: [{ kind: "component", type: WorkflowDiagramComponent, selector: "hm-workflow-diagram", inputs: ["definition", "current", "label", "texts"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2783
+ }
2784
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionWorkflowComponent, decorators: [{
2785
+ type: Component,
2786
+ args: [{ selector: 'spine-suggestion-workflow', imports: [WorkflowDiagramComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
2787
+ <section class="plan-card" aria-labelledby="sug-flow">
2788
+ <h3 id="sug-flow">{{ p.workflow.heading }}</h3>
2789
+ <p class="plan-muted">{{ p.workflow.hint }}</p>
2790
+ <hm-workflow-diagram [label]="p.workflow.label" [definition]="s.workflow()" />
2791
+ </section>
2792
+ `, styles: [":host{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--hm-space-md);min-inline-size:0}.plan-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,20rem),1fr));gap:var(--hm-space-md)}.plan-card{display:grid;grid-template-columns:minmax(0,1fr);align-content:start;gap:var(--hm-space-sm);min-inline-size:0;padding:var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.plan-card h3,.plan-card h4{margin:0;font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold);line-height:var(--hm-line-height-snug)}.plan-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.plan-toolbar__end{margin-inline-start:auto}.plan-muted{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-normal)}.plan-stats{display:flex;flex-wrap:wrap;gap:var(--hm-space-md);margin:0}.plan-stats div{display:grid;gap:var(--hm-space-nano);min-inline-size:0}.plan-stats dt{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.plan-stats dd{margin:0;font-size:var(--hm-font-size-lg);font-weight:var(--hm-font-weight-semibold)}.plan-text{margin:0;font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-snug)}.plan-title{font-weight:var(--hm-font-weight-medium)}.plan-rows{display:grid;gap:var(--hm-space-xs);margin:0;padding:0;font-size:var(--hm-font-size-sm);line-height:var(--hm-line-height-snug);list-style:none}.plan-rows li{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs) var(--hm-space-sm);min-inline-size:0}.plan-link{min-inline-size:0;padding:0;border:0;background:none;color:var(--hm-accent-strong);font:inherit;font-weight:var(--hm-font-weight-medium);text-align:start;cursor:pointer}.plan-link:hover{text-decoration:underline}.plan-link:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:var(--hm-border-thin)}.plan-key{color:var(--hm-muted);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-medium)}.plan-form{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--hm-space-sm)}.plan-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,12rem),1fr));gap:var(--hm-space-sm)}.plan-footer{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.plan-footer__start{margin-inline-end:auto}.plan-table-wrap{overflow-x:auto}.plan-table{inline-size:100%;border-collapse:collapse;font-size:var(--hm-font-size-sm);font-variant-numeric:tabular-nums}.plan-table caption{padding-block-end:var(--hm-space-xs);color:var(--hm-muted);text-align:start}.plan-table th,.plan-table td{padding:var(--hm-space-nano) var(--hm-space-xs);border-block-end:var(--hm-border-hairline) solid var(--hm-border);text-align:end;white-space:nowrap}.plan-table th:first-child,.plan-table td:first-child{text-align:start}\n"] }]
2793
+ }] });
2794
+
2795
+ const PLANNING_MODES = ['summary', 'tasks', 'planning', 'schedule', 'risks', 'workflow'];
2796
+ const MODES = ['summary', 'tasks', 'board', 'planning', 'schedule', 'risks', 'workflow', 'archive'];
2797
+ /** As colunas do quadro: recusada não tem coluna (sai na hora; fica no arquivo). */
2798
+ const BOARD_STATUSES = ['em_analise', 'planejada', 'em_desenvolvimento', 'concluida'];
2799
+ const OPEN_STATUSES = ['em_analise', 'planejada', 'em_desenvolvimento'];
2800
+ /** Quantas levam o selo "Mais votada" (das com algum apoio, no quadro inteiro, não só no filtrado). */
2801
+ const TOP_VOTED = 3;
2802
+ /** Responsáveis que aparecem no cartão; o resto vira "+N". */
2803
+ const FACES = 3;
2804
+ const ARCHIVE_PAGE = 20;
2805
+ const EXCERPT = 140;
2806
+ /** Parâmetro do link copiado ("?sugestao=SUG-12"): abrir a tela com ele abre a tarefa. */
2807
+ const SUGGESTION_LINK_PARAM = 'sugestao';
2808
+ const PRIORITY_RANK = { urgente: 0, alta: 1, media: 2, baixa: 3 };
2809
+ const byPosition = (a, b) => a.position - b.position || a.created_at.localeCompare(b.created_at);
2810
+ const SORTS = {
2811
+ position: byPosition,
2812
+ votes: (a, b) => b.votes - a.votes || byPosition(a, b),
2813
+ recent: (a, b) => b.created_at.localeCompare(a.created_at),
2814
+ priority: (a, b) => PRIORITY_RANK[a.priority] - PRIORITY_RANK[b.priority] || byPosition(a, b),
2815
+ due: (a, b) => (a.due_date ?? '9999').localeCompare(b.due_date ?? '9999') || byPosition(a, b),
2816
+ };
2817
+ /** Sem acento e minúsculo: "analise" acha "Análise". */
2818
+ const fold = (s) => s.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
2819
+ /** Intervalos do "há 3 dias" (segundos por unidade), do maior para o menor. */
2820
+ const AGES = [
2821
+ ['year', 31_536_000], ['month', 2_592_000], ['week', 604_800], ['day', 86_400], ['hour', 3_600], ['minute', 60],
2822
+ ];
2823
+ /**
2824
+ * O quadro de trabalho das sugestões do app (`hm-kanban`), montado na tela Sugestões do auto-serviço.
2825
+ *
2826
+ * - **Quadro**: o servidor manda o recorte (`board=true`): abertas e as concluídas dos últimos `done_days` dias;
2827
+ * recusadas e arquivadas saem. Indicadores do quadro inteiro, tudo calculado aqui com o que já vem (vencidas e as
2828
+ * que venceram ontem, vencendo em breve, em andamento por status, concluídas na janela e hoje, atribuídas a mim e as
2829
+ * abertas por prioridade; vencidas, vencendo, minhas e cada prioridade também filtram) e os filtros da tela — texto
2830
+ * (inclusive a chave "SUG-12"), responsável ("minhas", "sem responsável"), tipo, status (só aquela coluna),
2831
+ * prioridade, rótulo, prazo, e em "Mais filtros" tela e quem pediu, além da ordenação — sobre a lista inteira.
2832
+ * - **Coluna**: "Mais antiga há 4h 12min" (pelo `status_at`) e as vencidas abaixo do título, vazio explicado e
2833
+ * "+ Criar tarefa" no rodapé (os templates `hmKanbanColumnSummary`/`Empty`/`Footer` do `hm-kanban`).
2834
+ * - **Cartão**: capa (token da paleta), chave, selo de prioridade, título, contexto (tipo · tela), rótulos, trecho,
2835
+ * checklist (e os itens, com "Mostrar no cartão"), prazo por extenso (vencida em vermelho; hoje/amanhã em âmbar) e o
2836
+ * rodapé com comentários, checklist, apoios, a data e os responsáveis com "+N"; o "⋮": status e prioridade
2837
+ * (submenus), atribuir a mim, copiar link e chave, rótulos, capa (popover), arquivar e excluir (com confirmação).
2838
+ * - **Criar**: sempre o cadastro completo (a tarefa em branco no modal), já com o status da coluna: "Nova tarefa" no
2839
+ * topo (o "▾" escolhe a coluna), o "+" do cabeçalho e o "+ Criar tarefa" do rodapé de cada coluna.
2840
+ * - **Arrastar** (ponteiro, toque ou teclado; o `hm-kanban` cuida do gesto e dos anúncios) muda status e/ou posição: a
2841
+ * tela aplica na hora e grava; se o servidor recusar, avisa e relê. A posição gravada é sempre a da coluna INTEIRA
2842
+ * (pelo vizinho visível). Fora da "ordem do quadro", arrastar só troca de coluna (vai para o fim).
2843
+ * - **Arquivo**: arquivadas, recusadas, agrupadas (selo "Agrupada", com "Desagrupar") e concluídas antigas, paginado e
2844
+ * buscado no servidor, com "Reabrir"/"Restaurar".
2845
+ * - **Agrupar**: "Agrupar em…" no "⋯" do cartão e no detalhe abre o seletor (`spine-suggestion-merge-picker`) com as
2846
+ * abertas do quadro e a prévia do servidor; confirmar agrupa, tira a origem do quadro e abre o destino.
2847
+ * - **Tarefa**: `spine-suggestion-task-detail` (modal de duas colunas); o link "?sugestao=SUG-12" abre direto nela.
2848
+ * - **Planejamento** (pasta `planning/`): Resumo, Tarefas (lista e tabela), Planejamento (sprints, backlog e burndown),
2849
+ * Cronograma (Gantt com marcos e a carga da equipe), Riscos (matriz e registro) e Fluxo (os status, só leitura), no
2850
+ * molde do app de projetos do site de docs. Leem do `SuggestionPlanningStore` (tarefas sem o recorte do quadro,
2851
+ * sprints, marcos e riscos), relido ao entrar nelas; abrir uma tarefa abre o mesmo detalhe.
2852
+ */
2853
+ class AutoServiceSuggestionsComponent {
2854
+ constructor() {
2855
+ this.messages = inject(SPINE_MESSAGES);
2856
+ this.text = this.messages.suggestions;
2857
+ this.service = inject(AutoServiceSuggestionsService);
2858
+ this.planning = inject(SuggestionPlanningStore);
2859
+ this.toast = inject(HermesToastService);
2860
+ this.dialogs = inject(HermesDialogService);
2861
+ this.preferences = inject(SpinePreferencesService);
2862
+ this.userStorage = inject(SpineUserStorage, { optional: true });
2863
+ this.router = inject(Router, { optional: true });
2864
+ this.location = inject(Location, { optional: true });
2865
+ /** O client da sessão: a tela da sugestão só abre daqui se o app administrado for este. */
2866
+ this.sessionClientId = inject(AUTH_CONFIG, { optional: true })?.clientId ?? null;
2867
+ this.clientId = input.required(...(ngDevMode ? [{ debugName: "clientId" }] : /* istanbul ignore next */ []));
2868
+ this.kinds = SPINE_SUGGESTION_KINDS;
2869
+ this.statuses = SPINE_SUGGESTION_STATUSES;
2870
+ this.statusColors = SPINE_SUGGESTION_STATUS_COLORS;
2871
+ this.statusIcons = SPINE_SUGGESTION_STATUS_ICONS;
2872
+ this.kindColors = SPINE_SUGGESTION_KIND_COLORS;
2873
+ this.waitingValues = SPINE_SUGGESTION_WAITING;
2874
+ this.waitingColors = SPINE_SUGGESTION_WAITING_COLORS;
2875
+ this.newScreenValue = NEW_SCREEN_VALUE;
2876
+ this.priorities = TASK_PRIORITIES;
2877
+ this.sortKeys = ['position', 'votes', 'recent', 'priority', 'due'];
2878
+ this.today = todayIso(this.preferences.effectiveTimeZone());
2879
+ this.mode = signal('board', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
2880
+ this.modeOptions = [
2881
+ { value: 'summary', label: this.text.planning.viewSummary, icon: 'dashboard' },
2882
+ { value: 'tasks', label: this.text.planning.viewTasks, icon: 'list' },
2883
+ { value: 'board', label: this.text.viewBoard, icon: 'view-columns' },
2884
+ { value: 'planning', label: this.text.planning.viewPlanning, icon: 'tasks' },
2885
+ { value: 'schedule', label: this.text.planning.viewSchedule, icon: 'calendar-days' },
2886
+ { value: 'risks', label: this.text.planning.viewRisks, icon: 'exclamation-triangle' },
2887
+ { value: 'workflow', label: this.text.planning.viewWorkflow, icon: 'nodes' },
2888
+ { value: 'archive', label: this.text.viewArchive, icon: 'archive' },
2889
+ ];
2890
+ this.isPlanningMode = computed(() => PLANNING_MODES.includes(this.mode()), ...(ngDevMode ? [{ debugName: "isPlanningMode" }] : /* istanbul ignore next */ []));
2891
+ // --- quadro ---
2892
+ this.all = signal([], ...(ngDevMode ? [{ debugName: "all" }] : /* istanbul ignore next */ []));
2893
+ this.catalog = signal([], ...(ngDevMode ? [{ debugName: "catalog" }] : /* istanbul ignore next */ []));
2894
+ this.doneDays = signal(AUTO_SERVICE_SUGGESTION_DONE_DAYS, ...(ngDevMode ? [{ debugName: "doneDays" }] : /* istanbul ignore next */ []));
2895
+ this.loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
2896
+ this.loadError = signal('', ...(ngDevMode ? [{ debugName: "loadError" }] : /* istanbul ignore next */ []));
2897
+ this.search = signal('', ...(ngDevMode ? [{ debugName: "search" }] : /* istanbul ignore next */ []));
2898
+ this.kind = signal('', ...(ngDevMode ? [{ debugName: "kind" }] : /* istanbul ignore next */ []));
2899
+ this.screen = signal('', ...(ngDevMode ? [{ debugName: "screen" }] : /* istanbul ignore next */ []));
2900
+ this.author = signal('', ...(ngDevMode ? [{ debugName: "author" }] : /* istanbul ignore next */ []));
2901
+ /** '' | 'me' | 'none' | user_id */
2902
+ this.assignee = signal('', ...(ngDevMode ? [{ debugName: "assignee" }] : /* istanbul ignore next */ []));
2903
+ this.label = signal('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
2904
+ this.priority = signal('', ...(ngDevMode ? [{ debugName: "priority" }] : /* istanbul ignore next */ []));
2905
+ /** '' | 'overdue' | 'soon' */
2906
+ this.due = signal('', ...(ngDevMode ? [{ debugName: "due" }] : /* istanbul ignore next */ []));
2907
+ /** '' | um status do quadro: só essa coluna aparece. */
2908
+ this.status = signal('', ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
2909
+ /** '' | 'solicitante' | 'responsaveis': com quem está a vez na conversa. */
2910
+ this.waiting = signal('', ...(ngDevMode ? [{ debugName: "waiting" }] : /* istanbul ignore next */ []));
2911
+ /** '' | 'none' (criada pela equipe, sem grupo) | group_id: o grupo de quem pediu. */
2912
+ this.group = signal('', ...(ngDevMode ? [{ debugName: "group" }] : /* istanbul ignore next */ []));
2913
+ this.sort = signal('position', ...(ngDevMode ? [{ debugName: "sort" }] : /* istanbul ignore next */ []));
2914
+ /** "Mais filtros" aberto (quem pediu e o grupo). */
2915
+ this.moreFilters = signal(false, ...(ngDevMode ? [{ debugName: "moreFilters" }] : /* istanbul ignore next */ []));
2916
+ this.boardStatuses = BOARD_STATUSES;
2917
+ /** Os filtros escondidos em "Mais filtros" que estão ligados (o botão mostra a conta). */
2918
+ this.extraFilters = computed(() => [this.author(), this.group()].filter(Boolean).length, ...(ngDevMode ? [{ debugName: "extraFilters" }] : /* istanbul ignore next */ []));
2919
+ this.moreFiltersText = computed(() => (this.extraFilters()
2920
+ ? formatMessage(this.text.moreFiltersActive, { count: this.extraFilters() }) : this.text.moreFilters), ...(ngDevMode ? [{ debugName: "moreFiltersText" }] : /* istanbul ignore next */ []));
2921
+ this.me = computed(() => this.userStorage?.userId() ?? '', ...(ngDevMode ? [{ debugName: "me" }] : /* istanbul ignore next */ []));
2922
+ this.filtered = computed(() => {
2923
+ const term = fold(this.search().trim());
2924
+ const keyMatch = this.search().trim().match(/^(?:sug-?)?(\d{1,9})$/i);
2925
+ const [kind, screen, author, assignee, label, priority, due, waiting, group] = [this.kind(), this.screen(),
2926
+ this.author(), this.assignee(), this.label(), this.priority(), this.due(), this.waiting(), this.group()];
2927
+ const me = this.me();
2928
+ return this.all().filter((s) => (!kind || s.kind === kind)
2929
+ && (!screen || (screen === NEW_SCREEN_VALUE ? !!s.screen.new : !s.screen.new && s.screen.path === screen))
2930
+ && (!waiting || (s.waiting_on === waiting && OPEN_STATUSES.includes(s.status)))
2931
+ && (!group || (group === 'none' ? !s.groups?.length : !!s.groups?.some((g) => g.id === group)))
2932
+ && (!author || s.author?.user_id === author)
2933
+ && (!assignee || (assignee === 'none' ? s.assignees.length === 0 : assignedTo(s, assignee === 'me' ? me : assignee)))
2934
+ && (!label || s.labels.some((l) => l.id === label))
2935
+ && (!priority || s.priority === priority)
2936
+ && (!due || this.matchesDue(s, due))
2937
+ && (!term || (keyMatch && s.key_number === Number(keyMatch[1]))
2938
+ || fold(`${s.title} ${s.description} ${s.screen.label ?? ''} ${this.personName(s.author)} ${s.key}`).includes(term)));
2939
+ }, ...(ngDevMode ? [{ debugName: "filtered" }] : /* istanbul ignore next */ []));
2940
+ this.isFiltered = computed(() => !!(this.search().trim() || this.kind() || this.screen() || this.author()
2941
+ || this.assignee() || this.label() || this.priority() || this.due() || this.status() || this.waiting() || this.group()), ...(ngDevMode ? [{ debugName: "isFiltered" }] : /* istanbul ignore next */ []));
2942
+ this.filteredText = computed(() => formatMessage(this.text.boardFiltered, {
2943
+ shown: this.columns().reduce((n, c) => n + c.items.length, 0), total: this.all().length,
2944
+ }), ...(ngDevMode ? [{ debugName: "filteredText" }] : /* istanbul ignore next */ []));
2945
+ this.columns = computed(() => {
2946
+ const list = this.filtered();
2947
+ const order = SORTS[this.sort()];
2948
+ const only = this.status();
2949
+ return BOARD_STATUSES.filter((status) => !only || status === only).map((status) => ({
2950
+ id: status, title: this.text.statuses[status], items: list.filter((s) => s.status === status).sort(order),
2951
+ }));
2952
+ }, ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
2953
+ /**
2954
+ * Resumo de cada coluna (o que está à vista nela): há quanto tempo a mais antiga está no status ("Mais antiga há
2955
+ * 4h 12min", pelo `status_at`) e quantas vencidas.
2956
+ */
2957
+ this.columnSummaries = computed(() => {
2958
+ const now = Date.now();
2959
+ return new Map(this.columns().map((c) => {
2960
+ const since = c.items.map((s) => Date.parse(s.status_at ?? s.created_at)).filter((t) => !Number.isNaN(t));
2961
+ const oldest = since.length ? this.duration(now - Math.min(...since)) : '';
2962
+ const overdue = c.items.filter((s) => dueState(s, this.today) === 'overdue').length;
2963
+ return [c.id, {
2964
+ oldest: oldest ? formatMessage(this.text.columnOldest, { age: oldest }) : '',
2965
+ overdue: overdue ? pluralMessage(this.text.columnOverdue, overdue, this.messages.meta.locale) : '',
2966
+ }];
2967
+ }));
2968
+ }, ...(ngDevMode ? [{ debugName: "columnSummaries" }] : /* istanbul ignore next */ []));
2969
+ /**
2970
+ * Telas, pessoas, responsáveis e grupos que aparecem no quadro, para os filtros (só o que existe). As telas na ordem
2971
+ * 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ó.
2972
+ */
2973
+ this.screens = computed(() => {
2974
+ const byPath = new Map();
2975
+ for (const s of this.all())
2976
+ if (s.screen.path && !s.screen.new)
2977
+ byPath.set(s.screen.path, s.screen.label || s.screen.path);
2978
+ return [...byPath].map(([path, label]) => ({ path, label })).sort((a, b) => a.label.localeCompare(b.label));
2979
+ }, ...(ngDevMode ? [{ debugName: "screens" }] : /* istanbul ignore next */ []));
2980
+ this.hasNewScreens = computed(() => this.all().some((s) => s.screen.new), ...(ngDevMode ? [{ debugName: "hasNewScreens" }] : /* istanbul ignore next */ []));
2981
+ this.groupOptions = computed(() => {
2982
+ const byId = new Map();
2983
+ for (const s of this.all())
2984
+ for (const g of s.groups ?? [])
2985
+ byId.set(g.id, g.name);
2986
+ return [...byId].map(([id, name]) => ({ id, name })).sort((a, b) => a.name.localeCompare(b.name));
2987
+ }, ...(ngDevMode ? [{ debugName: "groupOptions" }] : /* istanbul ignore next */ []));
2988
+ this.authors = computed(() => this.people((s) => (s.author ? [s.author] : [])), ...(ngDevMode ? [{ debugName: "authors" }] : /* istanbul ignore next */ []));
2989
+ this.assigneeOptions = computed(() => this.people((s) => s.assignees), ...(ngDevMode ? [{ debugName: "assigneeOptions" }] : /* istanbul ignore next */ []));
2990
+ /** Indicadores do quadro inteiro (o filtro não muda: é o retrato do app). */
2991
+ this.stats = computed(() => {
2992
+ const list = this.all();
2993
+ const count = (status) => list.filter((s) => s.status === status).length;
2994
+ const open = list.filter((s) => OPEN_STATUSES.includes(s.status));
2995
+ const yesterday = addDays(this.today, -1);
2996
+ const zone = this.preferences.effectiveTimeZone();
2997
+ const overdue = open.filter((s) => dueState(s, this.today) === 'overdue');
2998
+ const done = list.filter((s) => s.status === 'concluida');
2999
+ return {
3000
+ open: open.length, em_analise: count('em_analise'), planejada: count('planejada'),
3001
+ em_desenvolvimento: count('em_desenvolvimento'), concluida: done.length,
3002
+ mine: open.filter((s) => assignedTo(s, this.me())).length,
3003
+ overdue: overdue.length,
3004
+ /** Venceram ontem: passaram a vencidas hoje (o "+N desde ontem"). */
3005
+ overdueNew: overdue.filter((s) => s.due_date === yesterday).length,
3006
+ soon: open.filter((s) => { const d = dueState(s, this.today); return d === 'today' || d === 'tomorrow'; }).length,
3007
+ /** A vez é da equipe (quem pediu escreveu) e as mensagens de quem pede ainda não lidas por quem consulta. */
3008
+ waitingTeam: open.filter((s) => s.waiting_on === 'responsaveis').length,
3009
+ unread: list.reduce((n, s) => n + (s.unread ?? 0), 0),
3010
+ doneToday: done.filter((s) => todayIso(zone, new Date(s.status_at ?? s.updated_at)) === this.today).length,
3011
+ byPriority: TASK_PRIORITIES.map((p) => ({ priority: p, count: open.filter((s) => s.priority === p).length })),
3012
+ };
3013
+ }, ...(ngDevMode ? [{ debugName: "stats" }] : /* istanbul ignore next */ []));
3014
+ this.doneLabel = computed(() => formatMessage(this.text.kpiDone, { days: this.doneDays() }), ...(ngDevMode ? [{ debugName: "doneLabel" }] : /* istanbul ignore next */ []));
3015
+ /** "Em análise 3 · Planejada 4 · Em desenvolvimento 5" no indicador das abertas. */
3016
+ this.openParts = computed(() => OPEN_STATUSES.map((st) => ({
3017
+ status: st,
3018
+ text: formatMessage(this.text.kpiStatusCount, { status: this.text.statuses[st], count: this.all().filter((s) => s.status === st).length }),
3019
+ })), ...(ngDevMode ? [{ debugName: "openParts" }] : /* istanbul ignore next */ []));
3020
+ this.overdueNote = computed(() => (this.stats().overdueNew
3021
+ ? formatMessage(this.text.kpiOverdueTrend, { count: this.stats().overdueNew }) : this.text.kpiOverdueSteady), ...(ngDevMode ? [{ debugName: "overdueNote" }] : /* istanbul ignore next */ []));
3022
+ this.doneNote = computed(() => (this.stats().doneToday
3023
+ ? formatMessage(this.text.kpiDoneToday, { count: this.stats().doneToday }) : this.text.kpiDoneNoneToday), ...(ngDevMode ? [{ debugName: "doneNote" }] : /* istanbul ignore next */ []));
3024
+ this.kpiLoading = computed(() => this.loading() && !this.all().length, ...(ngDevMode ? [{ debugName: "kpiLoading" }] : /* istanbul ignore next */ []));
3025
+ /** Itens do "▾" ao lado de "Nova tarefa": criar direto numa coluna. */
3026
+ this.newTaskItems = BOARD_STATUSES.map((st) => ({
3027
+ id: st, label: formatMessage(this.text.newTaskIn, { status: this.text.statuses[st] }), icon: SPINE_SUGGESTION_STATUS_ICONS[st],
3028
+ }));
3029
+ 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 */ []));
3030
+ /** O menu "⋯" de cada cartão, montado uma vez por versão da tarefa (o menu aberto não perde o foco). */
3031
+ this.menus = computed(() => new Map(this.all().map((t) => [t.id, this.buildMenu(t)])), ...(ngDevMode ? [{ debugName: "menus" }] : /* istanbul ignore next */ []));
3032
+ this.itemKey = (s) => s.id;
3033
+ this.itemLabel = (s) => `${s.key} ${s.title}`;
3034
+ // --- arquivo ---
3035
+ this.archived = signal([], ...(ngDevMode ? [{ debugName: "archived" }] : /* istanbul ignore next */ []));
3036
+ this.archiveTotal = signal(0, ...(ngDevMode ? [{ debugName: "archiveTotal" }] : /* istanbul ignore next */ []));
3037
+ this.archivePage = signal({ page: 1, size: ARCHIVE_PAGE }, ...(ngDevMode ? [{ debugName: "archivePage" }] : /* istanbul ignore next */ []));
3038
+ this.archiveSearch = signal('', ...(ngDevMode ? [{ debugName: "archiveSearch" }] : /* istanbul ignore next */ []));
3039
+ this.archiveKind = signal('', ...(ngDevMode ? [{ debugName: "archiveKind" }] : /* istanbul ignore next */ []));
3040
+ this.archiveStatus = signal('', ...(ngDevMode ? [{ debugName: "archiveStatus" }] : /* istanbul ignore next */ []));
3041
+ this.archiveLoading = signal(false, ...(ngDevMode ? [{ debugName: "archiveLoading" }] : /* istanbul ignore next */ []));
3042
+ this.archiveError = signal('', ...(ngDevMode ? [{ debugName: "archiveError" }] : /* istanbul ignore next */ []));
3043
+ this.reopening = signal(null, ...(ngDevMode ? [{ debugName: "reopening" }] : /* istanbul ignore next */ []));
3044
+ this.archiveCountText = computed(() => pluralMessage(this.text.archiveCount, this.archiveTotal(), this.messages.meta.locale), ...(ngDevMode ? [{ debugName: "archiveCountText" }] : /* istanbul ignore next */ []));
3045
+ this.archiveEmptyHint = computed(() => formatMessage(this.text.archiveEmptyHint, { days: this.doneDays() }), ...(ngDevMode ? [{ debugName: "archiveEmptyHint" }] : /* istanbul ignore next */ []));
3046
+ // --- tarefa aberta ---
3047
+ this.selected = signal(null, ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
3048
+ this.drafting = signal(false, ...(ngDevMode ? [{ debugName: "drafting" }] : /* istanbul ignore next */ []));
3049
+ this.draftStatus = signal('em_analise', ...(ngDevMode ? [{ debugName: "draftStatus" }] : /* istanbul ignore next */ []));
3050
+ this.canOpenScreen = computed(() => !!this.sessionClientId && this.sessionClientId.toLowerCase() === this.clientId().toLowerCase(), ...(ngDevMode ? [{ debugName: "canOpenScreen" }] : /* istanbul ignore next */ []));
3051
+ this.linkHandled = false;
3052
+ // --- agrupar ---
3053
+ /** A que está sendo agrupada (o seletor aberto); o detalhe fecha antes (um modal por vez). */
3054
+ this.mergeSource = signal(null, ...(ngDevMode ? [{ debugName: "mergeSource" }] : /* istanbul ignore next */ []));
3055
+ /** Checklist no cartão: os itens abertos (por cartão, só nesta tela; começa fechado, o rodapé diz quantos faltam). */
3056
+ this.openChecklists = signal(new Set(), ...(ngDevMode ? [{ debugName: "openChecklists" }] : /* istanbul ignore next */ []));
3057
+ effect(() => {
3058
+ const clientId = this.clientId();
3059
+ untracked(() => {
3060
+ this.selected.set(null);
3061
+ this.drafting.set(false);
3062
+ void this.loadLabels(clientId);
3063
+ this.planning.clientId.set(clientId);
3064
+ if (this.mode() === 'archive')
3065
+ void this.loadArchive(1, clientId);
3066
+ else if (this.isPlanningMode())
3067
+ void this.planning.load(clientId);
3068
+ else
3069
+ void this.load(clientId);
3070
+ });
3071
+ });
3072
+ effect(() => this.planning.me.set(this.me()));
3073
+ }
3074
+ people(of) {
3075
+ const byId = new Map();
3076
+ for (const s of this.all())
3077
+ for (const p of of(s))
3078
+ byId.set(p.user_id, p);
3079
+ return [...byId.values()].sort((a, b) => this.personName(a).localeCompare(this.personName(b)));
3080
+ }
3081
+ matchesDue(s, due) {
3082
+ const state = dueState(s, this.today);
3083
+ return due === 'overdue' ? state === 'overdue' : state === 'today' || state === 'tomorrow';
3084
+ }
3085
+ setMode(value) {
3086
+ const mode = MODES.includes(value) ? value : 'board';
3087
+ if (mode === this.mode())
3088
+ return;
3089
+ const wasPlanning = this.isPlanningMode();
3090
+ this.mode.set(mode);
3091
+ // Cada troca relê: o que foi reaberto no arquivo aparece no quadro e vice-versa. Entre duas visões de
3092
+ // planejamento o store já está em dia (cada gravação aplica a resposta do servidor).
3093
+ if (mode === 'archive')
3094
+ void this.loadArchive(1);
3095
+ else if (this.isPlanningMode()) {
3096
+ if (!wasPlanning)
3097
+ void this.planning.load();
3098
+ }
3099
+ else
3100
+ void this.load();
3101
+ }
3102
+ refresh() {
3103
+ if (this.mode() === 'archive')
3104
+ void this.loadArchive(this.archivePage().page);
3105
+ else if (this.isPlanningMode())
3106
+ void this.planning.load();
3107
+ else
3108
+ void this.load();
3109
+ }
3110
+ /** Clique numa pessoa da carga (Cronograma): o quadro filtrado pelas tarefas dela. */
3111
+ filterByPerson(userId) {
3112
+ this.assignee.set(userId);
3113
+ this.setMode('board');
3114
+ }
3115
+ async load(clientId = this.clientId()) {
3116
+ this.loading.set(true);
3117
+ this.loadError.set('');
3118
+ try {
3119
+ const board = await this.service.board(clientId);
3120
+ this.all.set(board.suggestions);
3121
+ this.doneDays.set(board.doneDays);
3122
+ void this.openFromLink(clientId);
3123
+ }
3124
+ catch {
3125
+ this.loadError.set(this.text.loadError);
3126
+ }
3127
+ finally {
3128
+ this.loading.set(false);
3129
+ }
3130
+ }
3131
+ async loadLabels(clientId = this.clientId()) {
3132
+ try {
3133
+ this.catalog.set(await this.service.labels(clientId));
3134
+ }
3135
+ catch {
3136
+ this.catalog.set([]);
3137
+ }
3138
+ }
3139
+ /** "?sugestao=SUG-12" na URL: abre a tarefa (uma vez por tela). */
3140
+ async openFromLink(clientId) {
3141
+ if (this.linkHandled || !this.router)
3142
+ return;
3143
+ this.linkHandled = true;
3144
+ const key = this.router.parseUrl(this.router.url).queryParams[SUGGESTION_LINK_PARAM];
3145
+ if (typeof key !== 'string' || !key)
3146
+ return;
3147
+ const known = this.all().find((s) => s.key.toLowerCase() === key.toLowerCase());
3148
+ try {
3149
+ this.openDetail(known ?? (await this.service.lookup(clientId, key)));
3150
+ }
3151
+ catch {
3152
+ this.toast.error(this.text.loadError);
3153
+ }
3154
+ }
3155
+ clearFilters() {
3156
+ for (const s of [this.search, this.kind, this.screen, this.author, this.assignee, this.label, this.priority, this.due, this.status,
3157
+ this.waiting, this.group]) {
3158
+ s.set('');
3159
+ }
3160
+ }
3161
+ /**
3162
+ * Indicador que filtra ("atribuídas a mim", "vencidas", "vencendo em breve", "aguardando responsáveis"): liga/desliga
3163
+ * o filtro equivalente.
3164
+ */
3165
+ toggleKpi(which, on) {
3166
+ if (which === 'mine')
3167
+ this.assignee.set(on ? 'me' : '');
3168
+ else if (which === 'waiting')
3169
+ this.waiting.set(on ? 'responsaveis' : '');
3170
+ else
3171
+ this.due.set(on ? which : '');
3172
+ }
3173
+ /** Uma prioridade da legenda do indicador: filtra por ela (de novo, desliga). */
3174
+ togglePriority(p) {
3175
+ this.priority.update((current) => (current === p ? '' : p));
3176
+ }
3177
+ /** Largura de cada prioridade na barra empilhada (% das abertas). */
3178
+ priorityShare(count) {
3179
+ const total = this.stats().open;
3180
+ return total ? (count / total) * 100 : 0;
3181
+ }
3182
+ priorityPart(p, count) {
3183
+ return formatMessage(this.text.kpiPriorityPart, { priority: this.text.priorities[p], count });
3184
+ }
3185
+ /**
3186
+ * O quadro já moveu o cartão (na lista filtrada); aqui vira a posição na coluna INTEIRA — logo depois do vizinho de
3187
+ * cima que está visível (ou antes do de baixo; sozinho na coluna filtrada = fim) — aplica na lista e grava. Com
3188
+ * outra ordenação, só a coluna conta: vai para o fim dela, e reordenar na mesma coluna não grava.
3189
+ */
3190
+ async onMoved(event) {
3191
+ const item = event.item;
3192
+ const to = event.to;
3193
+ const changed = event.from !== to;
3194
+ let change;
3195
+ if (this.sort() !== 'position') {
3196
+ if (!changed) {
3197
+ this.all.update((list) => [...list]); // devolve o cartão ao lugar da ordenação
3198
+ return;
3199
+ }
3200
+ const end = this.all().filter((s) => s.status === to && s.id !== item.id).length;
3201
+ this.all.update((list) => list.map((s) => (s.id === item.id ? { ...s, status: to, position: end } : s)));
3202
+ change = { status: to };
3203
+ }
3204
+ else {
3205
+ const shown = event.columns.find((c) => c.id === to)?.items ?? [];
3206
+ const above = shown[event.toIndex - 1];
3207
+ const below = shown[event.toIndex + 1];
3208
+ const column = this.all().filter((s) => s.status === to && s.id !== item.id).sort(byPosition);
3209
+ let index = column.length;
3210
+ if (above)
3211
+ index = column.findIndex((s) => s.id === above.id) + 1;
3212
+ else if (below)
3213
+ index = Math.max(0, column.findIndex((s) => s.id === below.id));
3214
+ const order = [...column.slice(0, index).map((s) => s.id), item.id, ...column.slice(index).map((s) => s.id)];
3215
+ const position = new Map(order.map((id, i) => [id, i]));
3216
+ this.all.update((list) => list.map((s) => (position.has(s.id)
3217
+ ? { ...s, position: position.get(s.id), ...(s.id === item.id ? { status: to } : {}) } : s)));
3218
+ change = { ...(changed ? { status: to } : {}), position: index };
3219
+ }
3220
+ try {
3221
+ this.replace(await this.service.move(this.clientId(), item.id, change));
3222
+ }
3223
+ catch {
3224
+ this.toast.error(this.text.moveError);
3225
+ await this.load();
3226
+ }
3227
+ }
3228
+ // --- criação ---
3229
+ /**
3230
+ * Abre o cadastro completo (a tarefa em branco no modal) já na coluna escolhida: "Nova tarefa" do topo (Em análise),
3231
+ * o "▾" ao lado dele, o "+" do cabeçalho e o "+ Criar tarefa" do rodapé de cada coluna.
3232
+ */
3233
+ newTask(status = 'em_analise') {
3234
+ this.selected.set(null);
3235
+ this.draftStatus.set(BOARD_STATUSES.includes(status) ? status : 'em_analise');
3236
+ this.drafting.set(true);
3237
+ }
3238
+ /** Nome do "+" da coluna: "Nova tarefa em Planejada". */
3239
+ newTaskInLabel(status) {
3240
+ return formatMessage(this.text.newTaskIn, { status: this.text.statuses[status] ?? status });
3241
+ }
3242
+ // --- tarefa aberta ---
3243
+ openDetail(s) {
3244
+ this.drafting.set(false);
3245
+ this.selected.set(s);
3246
+ }
3247
+ closeDetail() {
3248
+ this.selected.set(null);
3249
+ this.drafting.set(false);
3250
+ if (this.mode() === 'archive')
3251
+ void this.loadArchive(this.archivePage().page);
3252
+ }
3253
+ onCreated(task) {
3254
+ if (this.mode() === 'board')
3255
+ this.all.update((list) => [...list, task]);
3256
+ this.planning.replace(task);
3257
+ this.drafting.set(false);
3258
+ this.selected.set(task);
3259
+ }
3260
+ onRemoved(task) {
3261
+ this.all.update((list) => list.filter((s) => s.id !== task.id));
3262
+ this.planning.remove(task.id);
3263
+ this.archived.update((list) => list.filter((s) => s.id !== task.id));
3264
+ this.archiveTotal.update((n) => Math.max(0, n - (this.mode() === 'archive' ? 1 : 0)));
3265
+ this.selected.set(null);
3266
+ }
3267
+ onLabelCreated(label) {
3268
+ this.catalog.update((list) => [...list, label].sort((a, b) => a.name.localeCompare(b.name)));
3269
+ }
3270
+ /**
3271
+ * Troca a tarefa nas listas (o que o servidor devolveu), mantendo o autor que já veio com a foto. Recusada,
3272
+ * arquivada e agrupada saem do quadro na hora (o servidor também as tira do próximo `board=true`).
3273
+ */
3274
+ replace(saved) {
3275
+ const merge = (s) => ({ ...s, ...saved, author: saved.author ?? s.author });
3276
+ const leaves = saved.status === 'recusada' || !!saved.archived_at || !!saved.merged_into;
3277
+ this.all.update((list) => (leaves
3278
+ ? list.filter((s) => s.id !== saved.id)
3279
+ : list.map((s) => (s.id === saved.id ? merge(s) : s))));
3280
+ this.archived.update((list) => list.map((s) => (s.id === saved.id ? merge(s) : s)));
3281
+ this.planning.replace(saved);
3282
+ }
3283
+ // --- menu do cartão ---
3284
+ buildMenu(t) {
3285
+ const linked = new Set(t.labels.map((l) => l.id));
3286
+ const labels = this.catalog().length
3287
+ ? this.catalog().map((l) => ({ id: `label:${l.id}`, label: l.name, icon: linked.has(l.id) ? 'check' : 'tag' }))
3288
+ : [{ id: 'label:none', label: this.text.menuNoLabels, disabled: true }];
3289
+ return [
3290
+ {
3291
+ id: 'status', label: this.text.menuStatus, icon: 'view-columns',
3292
+ children: SPINE_SUGGESTION_STATUSES.map((st) => ({
3293
+ id: `status:${st}`, label: this.text.statuses[st], icon: st === t.status ? 'check' : SPINE_SUGGESTION_STATUS_ICONS[st], disabled: st === t.status,
3294
+ })),
3295
+ },
3296
+ {
3297
+ id: 'priority', label: this.text.menuPriority, icon: TASK_PRIORITY_ICONS[t.priority],
3298
+ children: TASK_PRIORITIES.map((p) => ({ id: `priority:${p}`, label: this.text.priorities[p], icon: p === t.priority ? 'check' : TASK_PRIORITY_ICONS[p] })),
3299
+ },
3300
+ {
3301
+ id: 'waiting', label: this.text.menuWaiting, icon: 'chat-bubble',
3302
+ children: [
3303
+ ...SPINE_SUGGESTION_WAITING.map((w) => ({
3304
+ id: `waiting:${w}`, label: this.text.waiting[w], icon: t.waiting_on === w ? 'check' : 'chat-bubble', disabled: t.waiting_on === w,
3305
+ })),
3306
+ { id: 'waiting:none', label: this.text.waitingClear, icon: t.waiting_on ? 'check-circle' : 'check', disabled: !t.waiting_on },
3307
+ ],
3308
+ },
3309
+ { id: 'assign-me', label: this.text.menuAssignMe, icon: 'user-plus', disabled: assignedTo(t, this.me()) },
3310
+ { id: 'labels', label: this.text.menuLabel, icon: 'tag', children: labels },
3311
+ { id: 'copy-link', label: this.text.menuCopyLink, icon: 'link', separatorAbove: true },
3312
+ { id: 'copy-key', label: this.text.menuCopyKey, icon: 'copy' },
3313
+ // Sem cadeia: a que já recebeu outras não pode ser agrupada (o seletor explica; aqui fica desligado).
3314
+ { id: 'merge', label: this.text.menuMerge, icon: 'layers', separatorAbove: true, disabled: !!t.merged_count },
3315
+ { id: 'archive', label: this.text.menuArchive, icon: 'archive', separatorAbove: true },
3316
+ { id: 'delete', label: this.text.menuDelete, icon: 'trash', danger: true },
3317
+ ];
3318
+ }
3319
+ menuOf(t) {
3320
+ return this.menus().get(t.id) ?? [];
3321
+ }
3322
+ menuLabel(t) {
3323
+ return formatMessage(this.text.cardMenu, { key: t.key });
3324
+ }
3325
+ /** Uma escolha do "⋯": cada uma grava e devolve a tarefa nova (ou copia, ou abre a capa ancorada no botão). */
3326
+ async onMenu(t, id, anchor) {
3327
+ const clientId = this.clientId();
3328
+ const [action, value] = id.split(':');
3329
+ try {
3330
+ if (action === 'status')
3331
+ this.replace(await this.service.move(clientId, t.id, { status: value }));
3332
+ else if (action === 'priority')
3333
+ this.replace(await this.service.update(clientId, t.id, { priority: value }));
3334
+ else if (action === 'waiting') {
3335
+ this.replace(await this.service.update(clientId, t.id, { waiting_on: value === 'none' ? null : value }));
3336
+ this.toast.success(this.text.waitingSaved);
3337
+ }
3338
+ else if (action === 'assign-me') {
3339
+ this.replace(await this.service.addPerson(clientId, t.id, 'me', 'responsavel'));
3340
+ this.toast.success(this.text.assignedToMe);
3341
+ }
3342
+ else if (action === 'label') {
3343
+ this.replace(await this.service.setLabel(clientId, t.id, value, !t.labels.some((l) => l.id === value)));
3344
+ }
3345
+ else if (action === 'copy-link') {
3346
+ if (await copyText(this.linkOf(t)))
3347
+ this.toast.success(this.text.linkCopied);
3348
+ else
3349
+ this.toast.error(this.text.copyError);
3350
+ }
3351
+ else if (action === 'copy-key') {
3352
+ if (await copyText(t.key))
3353
+ this.toast.success(this.text.keyCopied);
3354
+ else
3355
+ this.toast.error(this.text.copyError);
3356
+ }
3357
+ else if (action === 'merge') {
3358
+ this.startMerge(t);
3359
+ }
3360
+ else if (action === 'archive') {
3361
+ this.replace(await this.service.setArchived(clientId, t.id, true));
3362
+ this.toast.success(this.text.archivedToast);
3363
+ }
3364
+ else if (action === 'delete')
3365
+ await this.confirmDelete(t);
3366
+ }
3367
+ catch {
3368
+ this.toast.error(this.text.saveError);
3369
+ }
3370
+ }
3371
+ startMerge(t) {
3372
+ this.selected.set(null);
3373
+ this.drafting.set(false);
3374
+ this.mergeSource.set(t);
3375
+ }
3376
+ /** Agrupou: a origem sai do quadro, o destino é trocado pela versão do servidor e abre (mostra "Agrupadas aqui"). */
3377
+ onMerged(event) {
3378
+ this.mergeSource.set(null);
3379
+ this.all.update((list) => list.filter((s) => s.id !== event.source.id));
3380
+ this.replace(event.target);
3381
+ this.selected.set(event.target);
3382
+ }
3383
+ /** O detalhe pediu outra tarefa (o destino de uma agrupada, uma das agrupadas): abre a completa. */
3384
+ async openTask(id) {
3385
+ try {
3386
+ const task = await this.service.task(this.clientId(), id);
3387
+ this.openDetail(task);
3388
+ }
3389
+ catch {
3390
+ this.toast.error(this.text.loadError);
3391
+ }
3392
+ }
3393
+ /** Desagrupou: a origem volta ao quadro (relê o que estiver à vista). */
3394
+ onUnmerged() {
3395
+ if (this.mode() === 'archive')
3396
+ void this.loadArchive(this.archivePage().page);
3397
+ else
3398
+ void this.load();
3399
+ }
3400
+ mergedIntoText(key) {
3401
+ return formatMessage(this.text.mergedInto, { key });
3402
+ }
3403
+ /** "Desagrupar" no arquivo. */
3404
+ async unmergeFromArchive(t) {
3405
+ this.reopening.set(t.id);
3406
+ try {
3407
+ await this.service.unmerge(this.clientId(), t.id);
3408
+ this.toast.success(formatMessage(this.text.unmergedToast, { key: t.key }));
3409
+ void this.loadArchive(this.archivePage().page);
3410
+ }
3411
+ catch {
3412
+ this.toast.error(this.text.saveError);
3413
+ }
3414
+ finally {
3415
+ this.reopening.set(null);
3416
+ }
3417
+ }
3418
+ async confirmDelete(t) {
3419
+ const ok = await this.dialogs.confirm({
3420
+ heading: formatMessage(this.text.deleteHeading, { key: t.key }), message: this.text.deleteMessage,
3421
+ confirmLabel: this.text.deleteConfirm, danger: true,
3422
+ });
3423
+ if (!ok)
3424
+ return;
3425
+ await this.service.remove(this.clientId(), t.id);
3426
+ this.onRemoved(t);
3427
+ this.toast.success(this.text.deleted);
3428
+ }
3429
+ /** O link da tela com a tarefa: o caminho atual (sem a query) + "?sugestao=SUG-12". */
3430
+ linkOf(t) {
3431
+ const path = (this.location?.path() ?? '').split('?')[0];
3432
+ const base = this.location?.prepareExternalUrl(path) ?? path;
3433
+ const origin = typeof window !== 'undefined' ? window.location.origin : '';
3434
+ return `${origin}${base}?${SUGGESTION_LINK_PARAM}=${encodeURIComponent(t.key)}`;
3435
+ }
3436
+ /** Item do checklist marcado no próprio cartão. */
3437
+ async toggleCardItem(t, item) {
3438
+ try {
3439
+ this.replace(await this.service.updateChecklistItem(this.clientId(), t.id, item.id, { done: !item.done }));
3440
+ }
3441
+ catch {
3442
+ this.toast.error(this.text.saveError);
3443
+ }
3444
+ }
3445
+ // --- arquivo ---
3446
+ async loadArchive(page = 1, clientId = this.clientId()) {
3447
+ const size = this.archivePage().size;
3448
+ this.archiveLoading.set(true);
3449
+ this.archiveError.set('');
3450
+ try {
3451
+ const res = await this.service.archive(clientId, {
3452
+ limit: size, offset: (page - 1) * size, search: this.archiveSearch().trim(),
3453
+ kind: this.archiveKind(), status: this.archiveStatus(),
3454
+ });
3455
+ this.archived.set(res.suggestions);
3456
+ this.archiveTotal.set(res.total);
3457
+ this.archivePage.set({ page, size });
3458
+ }
3459
+ catch {
3460
+ this.archiveError.set(this.text.loadError);
3461
+ }
3462
+ finally {
3463
+ this.archiveLoading.set(false);
3464
+ }
3465
+ }
3466
+ onArchiveSearch(term) {
3467
+ this.archiveSearch.set(term);
3468
+ void this.loadArchive(1);
3469
+ }
3470
+ onArchiveFilter(which, value) {
3471
+ (which === 'kind' ? this.archiveKind : this.archiveStatus).set(value);
3472
+ void this.loadArchive(1);
3473
+ }
3474
+ onArchivePage(state) {
3475
+ this.archivePage.set({ page: state.page, size: state.size });
3476
+ void this.loadArchive(state.page);
3477
+ }
3478
+ /** Recusada volta para Em análise (fim da coluna); arquivada volta ao quadro com o status que tinha. */
3479
+ async reopen(s) {
3480
+ if (this.reopening())
3481
+ return;
3482
+ this.reopening.set(s.id);
3483
+ try {
3484
+ if (s.archived_at)
3485
+ await this.service.setArchived(this.clientId(), s.id, false);
3486
+ if (s.status === 'recusada')
3487
+ await this.service.move(this.clientId(), s.id, { status: 'em_analise' });
3488
+ this.toast.success(s.status === 'recusada' ? this.text.reopened : this.text.restoredToast);
3489
+ const page = this.archivePage().page;
3490
+ // Última da página: volta uma, para não cair numa página vazia.
3491
+ await this.loadArchive(this.archived().length === 1 && page > 1 ? page - 1 : page);
3492
+ }
3493
+ catch {
3494
+ this.toast.error(this.text.reopenError);
3495
+ }
3496
+ finally {
3497
+ this.reopening.set(null);
3498
+ }
3499
+ }
3500
+ // --- textos do cartão ---
3501
+ sortLabel(k) {
3502
+ if (k === 'priority')
3503
+ return this.text.priority;
3504
+ if (k === 'due')
3505
+ return this.text.dueDate;
3506
+ return this.text.sorts[k];
3507
+ }
3508
+ kindLabel(s) {
3509
+ return this.text.kinds[s.kind];
3510
+ }
3511
+ kindIcon(s) {
3512
+ return SPINE_SUGGESTION_KIND_ICONS[s.kind];
3513
+ }
3514
+ priorityIcon(s) {
3515
+ return TASK_PRIORITY_ICONS[s.priority];
3516
+ }
3517
+ priorityName(s) {
3518
+ return this.text.priorities[s.priority];
3519
+ }
3520
+ isTop(s) {
3521
+ return this.topIds().has(s.id);
3522
+ }
3523
+ excerpt(s) {
3524
+ const d = s.description.replace(/\s+/g, ' ').trim();
3525
+ return d.length > EXCERPT ? `${d.slice(0, EXCERPT - 1).trimEnd()}…` : d;
3526
+ }
3527
+ faces(s) {
3528
+ return s.assignees.slice(0, FACES);
3529
+ }
3530
+ moreFaces(s) {
3531
+ const rest = s.assignees.length - FACES;
3532
+ return rest > 0 ? formatMessage(this.text.moreAssignees, { count: rest }) : '';
3533
+ }
3534
+ dueOf(s) {
3535
+ return dueState(s, this.today);
3536
+ }
3537
+ /** "Vencida há 3 dias", "Vence hoje", "Vence amanhã", "No prazo · até 12 de nov." (concluída: "Até 12 de nov."). */
3538
+ dueText(s) {
3539
+ if (!s.due_date)
3540
+ return '';
3541
+ const state = this.dueOf(s);
3542
+ if (state === 'overdue') {
3543
+ return pluralMessage(this.text.dueLateDays, daysBetween(s.due_date, this.today), this.messages.meta.locale);
3544
+ }
3545
+ if (state === 'today')
3546
+ return this.text.dueToday;
3547
+ if (state === 'tomorrow')
3548
+ return this.text.dueTomorrow;
3549
+ if (state === 'later')
3550
+ return formatMessage(this.text.dueWithin, { date: this.day(s.due_date) });
3551
+ return formatMessage(this.text.dueOn, { date: this.day(s.due_date) });
3552
+ }
3553
+ dueIcon(s) {
3554
+ const state = this.dueOf(s);
3555
+ if (state === 'overdue')
3556
+ return 'alarm-clock';
3557
+ if (state === 'today' || state === 'tomorrow')
3558
+ return 'hourglass';
3559
+ return 'calendar';
3560
+ }
3561
+ /** O nome da tela no cartão: o último nível do caminho, o nome da nova ou "Sem tela". */
3562
+ screenNameOf(s) {
3563
+ if (s.screen.new)
3564
+ return s.screen.label || this.text.newScreenTag;
3565
+ return screenName(s.screen.label) || s.screen.path || this.text.noScreen;
3566
+ }
3567
+ /** Selo "Aguardando…" do cartão (o `let-item` do kanban chega sem tipo). */
3568
+ waitingName(s) {
3569
+ return s.waiting_on ? this.text.waiting[s.waiting_on] : '';
3570
+ }
3571
+ waitingColor(s) {
3572
+ return s.waiting_on ? SPINE_SUGGESTION_WAITING_COLORS[s.waiting_on] : 'neutral';
3573
+ }
3574
+ /** Faixa da nota de risco da tarefa (baixo, médio, alto, crítico); vazio = sem risco aberto. */
3575
+ riskLevelOf(task) {
3576
+ const score = this.planning.riskScoreOf(task);
3577
+ return score === null ? '' : riskLevel(score);
3578
+ }
3579
+ unreadText(count) {
3580
+ return pluralMessage(this.text.unread, count, this.messages.meta.locale);
3581
+ }
3582
+ createdText(s) {
3583
+ return formatMessage(this.text.createdOn, { date: this.date(s.created_at, true) });
3584
+ }
3585
+ /** Explicação da coluna vazia: com filtro, "nenhuma com estes filtros"; sem, o que costuma cair nela. */
3586
+ emptyHint(status) {
3587
+ if (this.isFiltered())
3588
+ return this.text.columnEmptyFiltered;
3589
+ const hint = this.text.columnEmptyHints[status] ?? '';
3590
+ return formatMessage(hint, { days: this.doneDays() });
3591
+ }
3592
+ emptyIcon(status) {
3593
+ return this.isFiltered() ? 'filter' : (SPINE_SUGGESTION_STATUS_ICONS[status] ?? 'inbox');
3594
+ }
3595
+ /** "3d 4h", "4h 12min", "12min" (o catálogo dá o formato). */
3596
+ duration(ms) {
3597
+ const { d, h, m } = splitDuration(ms);
3598
+ if (d)
3599
+ return formatMessage(this.text.duration.days, { d, h });
3600
+ if (h)
3601
+ return formatMessage(this.text.duration.hours, { h, m });
3602
+ return formatMessage(this.text.duration.minutes, { m });
3603
+ }
3604
+ checklistText(s) {
3605
+ return formatMessage(this.text.checklistSpoken, { done: s.checklist.done, total: s.checklist.total });
3606
+ }
3607
+ checklistOpen(s) {
3608
+ return this.openChecklists().has(s.id);
3609
+ }
3610
+ toggleChecklist(s) {
3611
+ this.openChecklists.update((set) => {
3612
+ const next = new Set(set);
3613
+ if (!next.delete(s.id))
3614
+ next.add(s.id);
3615
+ return next;
3616
+ });
3617
+ }
3618
+ /** Os rótulos na linha de selos do cartão, cada um com a cor do token ('cat-3' → 3). */
3619
+ labelTags(s) {
3620
+ return s.labels.map((l) => ({ id: l.id, name: l.name, color: Number(l.color.replace('cat-', '')) || undefined }));
3621
+ }
3622
+ /** "3 atividades (1/3)". */
3623
+ cardChecklistText(s) {
3624
+ return pluralMessage(this.text.cardChecklist, s.checklist.total, this.messages.meta.locale, { done: s.checklist.done });
3625
+ }
3626
+ /** "Pedida por Ana" (+N relatores das agrupadas): a dica da foto de quem pediu. */
3627
+ requestersText(s) {
3628
+ const who = formatMessage(this.text.requestedByName, { name: this.personName(s.author) });
3629
+ return s.reporters_count ? `${who} ${this.moreText(s.reporters_count)}` : who;
3630
+ }
3631
+ /** "+N". */
3632
+ moreText(count) {
3633
+ return formatMessage(this.text.moreAssignees, { count });
3634
+ }
3635
+ cardItems(s) {
3636
+ return s.checklist_on_card ? [...(s.checklist.items ?? [])].sort((a, b) => a.position - b.position) : [];
3637
+ }
3638
+ personName(p) {
3639
+ return p?.name || p?.username || this.text.removedAuthor;
3640
+ }
3641
+ votesText(count) {
3642
+ return pluralMessage(this.text.votes, count, this.messages.meta.locale);
3643
+ }
3644
+ commentsText(count) {
3645
+ return pluralMessage(this.text.comments, count, this.messages.meta.locale);
3646
+ }
3647
+ closedOn(s) {
3648
+ if (s.archived_at)
3649
+ return `${this.text.archivedTag} · ${this.date(s.archived_at)}`;
3650
+ const template = s.status === 'recusada' ? this.text.closedOn.recusada : this.text.closedOn.concluida;
3651
+ return formatMessage(template, { date: this.date(s.status_at ?? s.updated_at) });
3652
+ }
3653
+ /** "há 3 dias", "ontem", "há 2 horas" (Intl.RelativeTimeFormat no idioma do catálogo). */
3654
+ age(iso) {
3655
+ const seconds = Math.round((Date.now() - new Date(iso).getTime()) / 1000);
3656
+ try {
3657
+ const rtf = new Intl.RelativeTimeFormat(this.messages.meta.locale, { numeric: 'auto' });
3658
+ for (const [unit, size] of AGES)
3659
+ if (Math.abs(seconds) >= size)
3660
+ return rtf.format(-Math.floor(seconds / size), unit);
3661
+ return rtf.format(0, 'minute');
3662
+ }
3663
+ catch {
3664
+ return this.date(iso);
3665
+ }
3666
+ }
3667
+ date(iso, withTime = false) {
3668
+ try {
3669
+ return new Intl.DateTimeFormat(this.messages.meta.locale, {
3670
+ dateStyle: 'short', ...(withTime ? { timeStyle: 'short' } : {}), timeZone: this.preferences.effectiveTimeZone(),
3671
+ }).format(new Date(iso));
3672
+ }
3673
+ catch {
3674
+ return iso.slice(0, 10);
3675
+ }
3676
+ }
3677
+ /** Data sem hora (AAAA-MM-DD): o dia é o dia, sem fuso. */
3678
+ day(iso) {
3679
+ try {
3680
+ return new Intl.DateTimeFormat(this.messages.meta.locale, { day: '2-digit', month: 'short', timeZone: 'UTC' }).format(new Date(`${iso}T00:00:00Z`));
3681
+ }
3682
+ catch {
3683
+ return iso;
3684
+ }
3685
+ }
3686
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceSuggestionsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3687
+ 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 } }, providers: [SuggestionPlanningStore], ngImport: i0, template: "<div class=\"as-stack as-suggestions\">\n <!-- As vis\u00F5es (Resumo, Tarefas, Quadro, Planejamento, Cronograma, Riscos, Fluxo e Arquivo), o atualizar e a nova\n tarefa (com o \"\u25BE\" para criar direto numa coluna). As vis\u00F5es rolam por dentro em tela estreita. -->\n <div class=\"as-suggestions__bar\">\n <div class=\"as-suggestions__modes\">\n <hm-segmented-control [label]=\"text.viewLabel\" [options]=\"modeOptions\" [value]=\"mode()\" (valueChange)=\"setMode($event)\" />\n </div>\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 <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 <!-- Nota de risco da tarefa (a maior entre os riscos abertos); o selo j\u00E1 se anuncia como \"Risco: Alto\". -->\n @if (riskLevelOf(item); as level) {\n <hm-level-badge class=\"as-card__risk\" scale=\"risk\" [value]=\"level\" />\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 if (isPlanningMode()) {\n <!-- Planejamento: as vis\u00F5es leem do store (tarefas sem o recorte do quadro, sprints, marcos e riscos). -->\n @if (planning.loadError()) {\n <hm-alert variant=\"error\">\n {{ planning.loadError() }}\n <hm-button emphasis=\"minimal\" size=\"small\" (clicked)=\"planning.load()\">{{ text.planning.retry }}</hm-button>\n </hm-alert>\n } @else if (planning.loading() && !planning.tasks().length) {\n <hm-skeleton shape=\"cards\" [label]=\"text.planning.viewPlanning\" />\n } @else {\n @switch (mode()) {\n @case ('summary') { <spine-suggestion-summary [today]=\"today\" (openTask)=\"openTask($event)\" /> }\n @case ('tasks') { <spine-suggestion-tasks (openTask)=\"openTask($event)\" /> }\n @case ('planning') { <spine-suggestion-planning [today]=\"today\" /> }\n @case ('schedule') { <spine-suggestion-schedule (openTask)=\"openTask($event)\" (person)=\"filterByPerson($event)\" /> }\n @case ('risks') { <spine-suggestion-risks /> }\n @case ('workflow') { <spine-suggestion-workflow /> }\n }\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()\" [candidates]=\"planning.tasks().length ? planning.tasks() : all()\" [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__modes{min-inline-size:0;max-inline-size:100%;overflow-x:auto}.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-size:var(--hm-font-size-sm);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", "presence", "texts"] }, { 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", "dragGroup", "canReceive", "columnLabel"], outputs: ["columnsChange", "collapsedChange", "moved", "received"] }, { kind: "component", type: LevelBadgeComponent, selector: "hm-level-badge", inputs: ["scale", "value", "compact", "showScale", "texts"] }, { 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", "candidates", "canOpenScreen"], outputs: ["changed", "created", "removed", "closed", "labelCreated", "mergeRequested", "openTask", "unmerged"] }, { kind: "component", type: TagComponent, selector: "hm-tag", inputs: ["name", "color", "compact"] }, { kind: "component", type: SkeletonComponent, selector: "hm-skeleton", inputs: ["shape", "count", "label"] }, { kind: "component", type: SuggestionSummaryComponent, selector: "spine-suggestion-summary", inputs: ["today"], outputs: ["openTask"] }, { kind: "component", type: SuggestionTasksComponent, selector: "spine-suggestion-tasks", outputs: ["openTask"] }, { kind: "component", type: SuggestionPlanningViewComponent, selector: "spine-suggestion-planning", inputs: ["today"] }, { kind: "component", type: SuggestionScheduleComponent, selector: "spine-suggestion-schedule", outputs: ["openTask", "person"] }, { kind: "component", type: SuggestionRisksComponent, selector: "spine-suggestion-risks" }, { kind: "component", type: SuggestionWorkflowComponent, selector: "spine-suggestion-workflow" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
3688
+ }
3689
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceSuggestionsComponent, decorators: [{
3690
+ type: Component,
3691
+ args: [{ selector: 'spine-auto-service-suggestions', imports: [
3692
+ AccountAvatarComponent, AlertComponent, ButtonComponent, EmptyComponent, IconButtonComponent, IconComponent,
3693
+ KanbanCardDirective, KanbanColumnActionsDirective, KanbanColumnEmptyDirective, KanbanColumnFooterDirective,
3694
+ KanbanColumnSummaryDirective, KanbanComponent, LevelBadgeComponent, MenuButtonComponent, PaginatorComponent,
3695
+ SearchFieldComponent, SegmentedControlComponent, SelectComponent, SpineTagRowComponent, SuggestionMergePickerComponent,
3696
+ SuggestionTaskDetailComponent, TagComponent, SkeletonComponent, SuggestionSummaryComponent, SuggestionTasksComponent,
3697
+ SuggestionPlanningViewComponent, SuggestionScheduleComponent, SuggestionRisksComponent, SuggestionWorkflowComponent,
3698
+ ], providers: [SuggestionPlanningStore], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"as-stack as-suggestions\">\n <!-- As vis\u00F5es (Resumo, Tarefas, Quadro, Planejamento, Cronograma, Riscos, Fluxo e Arquivo), o atualizar e a nova\n tarefa (com o \"\u25BE\" para criar direto numa coluna). As vis\u00F5es rolam por dentro em tela estreita. -->\n <div class=\"as-suggestions__bar\">\n <div class=\"as-suggestions__modes\">\n <hm-segmented-control [label]=\"text.viewLabel\" [options]=\"modeOptions\" [value]=\"mode()\" (valueChange)=\"setMode($event)\" />\n </div>\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 <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 <!-- Nota de risco da tarefa (a maior entre os riscos abertos); o selo j\u00E1 se anuncia como \"Risco: Alto\". -->\n @if (riskLevelOf(item); as level) {\n <hm-level-badge class=\"as-card__risk\" scale=\"risk\" [value]=\"level\" />\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 if (isPlanningMode()) {\n <!-- Planejamento: as vis\u00F5es leem do store (tarefas sem o recorte do quadro, sprints, marcos e riscos). -->\n @if (planning.loadError()) {\n <hm-alert variant=\"error\">\n {{ planning.loadError() }}\n <hm-button emphasis=\"minimal\" size=\"small\" (clicked)=\"planning.load()\">{{ text.planning.retry }}</hm-button>\n </hm-alert>\n } @else if (planning.loading() && !planning.tasks().length) {\n <hm-skeleton shape=\"cards\" [label]=\"text.planning.viewPlanning\" />\n } @else {\n @switch (mode()) {\n @case ('summary') { <spine-suggestion-summary [today]=\"today\" (openTask)=\"openTask($event)\" /> }\n @case ('tasks') { <spine-suggestion-tasks (openTask)=\"openTask($event)\" /> }\n @case ('planning') { <spine-suggestion-planning [today]=\"today\" /> }\n @case ('schedule') { <spine-suggestion-schedule (openTask)=\"openTask($event)\" (person)=\"filterByPerson($event)\" /> }\n @case ('risks') { <spine-suggestion-risks /> }\n @case ('workflow') { <spine-suggestion-workflow /> }\n }\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()\" [candidates]=\"planning.tasks().length ? planning.tasks() : all()\" [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__modes{min-inline-size:0;max-inline-size:100%;overflow-x:auto}.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-size:var(--hm-font-size-sm);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"] }]
3699
+ }], ctorParameters: () => [], propDecorators: { clientId: [{ type: i0.Input, args: [{ isSignal: true, alias: "clientId", required: true }] }] } });
3700
+
3701
+ /**
3702
+ * Tela "Sugestões" do auto-serviço (router `AUTO_SERVICE_SUGGESTIONS_TEMPLATE_ROUTER_ID`, na raiz do menu; migration
3703
+ * 0036 do argos): o seletor de client + o quadro de sugestões que era a aba Cliente › Sugestões. Carregada só por
3704
+ * `AUTO_SERVICE_SCREENS.suggestions` -- nunca exportada pelo ponto de entrada (ver `public-api.ts`).
3705
+ */
3706
+ class AutoServiceSuggestionsPageComponent {
3707
+ constructor() {
3708
+ this.client = signal(null, ...(ngDevMode ? [{ debugName: "client" }] : /* istanbul ignore next */ []));
3709
+ }
3710
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceSuggestionsPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3711
+ 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: `
3712
+ <div class="as-stack">
3713
+ <spine-auto-service-client-picker (clientChange)="client.set($event)" />
3714
+ @if (client(); as c) {
3715
+ <spine-auto-service-suggestions [clientId]="c.id" />
3716
+ }
3717
+ </div>
3718
+ `, 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 }); }
3719
+ }
3720
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceSuggestionsPageComponent, decorators: [{
3721
+ type: Component,
3722
+ args: [{ selector: 'spine-auto-service-suggestions-page', imports: [AutoServiceClientPickerComponent, AutoServiceSuggestionsComponent], template: `
3723
+ <div class="as-stack">
3724
+ <spine-auto-service-client-picker (clientChange)="client.set($event)" />
3725
+ @if (client(); as c) {
3726
+ <spine-auto-service-suggestions [clientId]="c.id" />
3727
+ }
3728
+ </div>
3729
+ `, 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"] }]
3730
+ }] });
3731
+
3732
+ export { AutoServiceSuggestionsPageComponent };
3733
+ //# sourceMappingURL=trismegisto-spine-auto-service-auto-service-suggestions-page.component-CkiCTmXD.mjs.map