feat: teams within a project

Projects keep teams of members in their settings (settings.teams; people
outside the project are dropped, teams are archived, not removed; absent
when saving means unchanged). Tickets go to a team (`team`, also filtered
in the list and in MCP search_issues), and a comment mentioning
@[Frontend](team-FRONT) tells each of its members. Settings has the teams
with their members, the ticket a «Equipo» select, the list a team filter,
and «@» offers the teams after the people.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 16:20:35 +02:00
parent 4acf629106
commit 869acd3f1f
41 changed files with 895 additions and 13 deletions

View File

@@ -32,7 +32,7 @@ import {
} from '@task-stream/web/tracker/domain';
import { CATEGORY, COLOR_LABEL, ICON_LABEL, tone } from '../ui/copy';
type Section = 'statuses' | 'issueTypes' | 'areas' | 'customFields';
type Section = 'statuses' | 'issueTypes' | 'areas' | 'customFields' | 'teams';
const FIELD_TYPES: { id: CustomFieldType; label: string }[] = [
{ id: CustomFieldType.TEXT, label: 'Texto' },
@@ -530,6 +530,63 @@ function idFrom(name: string): string {
</ol>
</section>
<!-- Teams -->
<section class="ts-panel ts-block" aria-labelledby="teams-title">
<header class="ts-block__head">
<div>
<h3 id="teams-title">Equipos</h3>
<p class="ts-muted">
Grupos de miembros: los tickets se pueden asignar a uno y
«@equipo» avisa a todos sus miembros.
</p>
</div>
<button type="button" class="ts-add" (click)="addTeam()">
+ Añadir equipo
</button>
</header>
@if (!d.teams.length) {
<p class="ts-muted ts-empty-fields">Ningún equipo todavía.</p>
}
<ol class="ts-rows">
@for (team of d.teams; track $index; let i = $index) {
<li
class="ts-row ts-field-grid"
[class.is-archived]="team.archived"
>
<span class="ts-name">
<input
class="ts-input"
maxlength="40"
[attr.aria-label]="'Nombre del equipo ' + (i + 1)"
[value]="team.name"
(input)="setName('teams', i, $any($event.target).value)"
/>
<code class="ts-id">{{ team.id }}</code>
</span>
<ng-container
[ngTemplateOutlet]="archive"
[ngTemplateOutletContext]="{
section: 'teams',
index: i,
item: team,
}"
/>
<div class="ts-team-members ts-field-type">
@for (member of facade.members(); track member.user.id) {
<pi-checkbox
[label]="member.user.name"
[checked]="team.memberIds.includes(member.user.id)"
(checkedChange)="
setTeamMember(i, member.user.id, $event)
"
/>
}
</div>
</li>
}
</ol>
</section>
<!-- Definition of done -->
<section class="ts-panel ts-block" aria-labelledby="dod-title">
<header class="ts-block__head">
@@ -1035,6 +1092,11 @@ function idFrom(name: string): string {
resize: vertical;
}
}
.ts-team-members {
display: flex;
flex-wrap: wrap;
gap: 0.25rem 1rem;
}
.ts-empty-fields {
margin: 0;
}
@@ -1277,7 +1339,7 @@ export class SettingsPage {
const d = this.draft();
if (!d) return [];
const problems: string[] = [];
const lists = [d.statuses, d.issueTypes, d.areas, d.customFields];
const lists = [d.statuses, d.issueTypes, d.areas, d.customFields, d.teams];
if (lists.some((list) => list.some((item) => !item.name.trim())))
problems.push('Hay nombres vacíos');
if (
@@ -1449,6 +1511,26 @@ export class SettingsPage {
);
}
protected addTeam(): void {
this.patch((d) =>
d.teams.push({
id: this.uniqueId(d.teams, 'NUEVO_EQUIPO'),
name: 'Nuevo equipo',
memberIds: [],
archived: false,
}),
);
}
protected setTeamMember(index: number, userId: string, on: boolean): void {
this.patch((d) => {
const team = d.teams[index];
team.memberIds = on
? [...new Set([...team.memberIds, userId])]
: team.memberIds.filter((id) => id !== userId);
});
}
protected setChecklistBeforeDone(on: boolean): void {
this.patch((d) => {
d.checklistBeforeDone = on;