The home page was two grids of panels side by side, each row as tall as its tallest panel, which left gaps. Now: four figures of the same size (my open tickets, waiting for QA, finished by me and by the team this week), the week, my views, and two columns that stack on their own: my tickets and the latest on the left; sprints, QA, following, releases, who is online and «Retomar» on the right. Project cards are as tall as their row, with the team at the bottom, and a phone only lists the days of the week that have something. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
177 lines
4.8 KiB
TypeScript
177 lines
4.8 KiB
TypeScript
import {
|
|
ChangeDetectionStrategy,
|
|
Component,
|
|
computed,
|
|
input,
|
|
} from '@angular/core';
|
|
import { RouterLink } from '@angular/router';
|
|
import { AgendaItem } from '@task-stream/web/tracker/domain';
|
|
|
|
const DAY = 86_400_000;
|
|
const KIND: Record<AgendaItem['kind'], string> = {
|
|
ISSUE: 'vence',
|
|
SPRINT_END: 'fin de sprint',
|
|
RELEASE: 'release',
|
|
};
|
|
|
|
/** My coming week: what is due, which sprints end, which releases land. */
|
|
@Component({
|
|
selector: 'ts-week-agenda',
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
imports: [RouterLink],
|
|
host: {
|
|
class: 'ts-panel',
|
|
role: 'region',
|
|
'aria-labelledby': 'agenda-title',
|
|
},
|
|
styleUrl: './home-panel.scss',
|
|
template: `
|
|
<header class="ts-panel__head">
|
|
<h2 id="agenda-title">Tu semana</h2>
|
|
<span class="ts-muted">{{ count() }} cosas en los próximos 7 días</span>
|
|
</header>
|
|
<ol class="ts-agenda">
|
|
@for (day of days(); track day.date) {
|
|
<li [class.is-today]="day.today" [class.is-empty]="!day.items.length">
|
|
<p class="ts-agenda__day">
|
|
<span>{{ day.weekday }}</span> <strong>{{ day.number }}</strong>
|
|
</p>
|
|
@for (item of day.items; track $index) {
|
|
<a
|
|
class="ts-agenda__item"
|
|
[attr.data-kind]="item.kind"
|
|
[routerLink]="link(item)"
|
|
[attr.title]="item.projectKey + ' · ' + item.title"
|
|
>
|
|
<span class="ts-agenda__kind">{{ kindOf(item) }}</span>
|
|
{{ item.issueKey ?? item.projectKey }}
|
|
<span class="ts-agenda__title">{{ item.title }}</span>
|
|
</a>
|
|
} @empty {
|
|
<span class="ts-agenda__none">—</span>
|
|
}
|
|
</li>
|
|
}
|
|
</ol>
|
|
`,
|
|
styles: `
|
|
.ts-agenda {
|
|
display: grid;
|
|
grid-template-columns: repeat(7, minmax(0, 1fr));
|
|
gap: 0.5rem;
|
|
margin: 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
li {
|
|
display: grid;
|
|
align-content: start;
|
|
gap: 0.25rem;
|
|
min-height: 6rem;
|
|
padding: 0.5rem;
|
|
border-radius: 0.625rem;
|
|
background: var(--ts-sunken);
|
|
}
|
|
li.is-today {
|
|
outline: 2px solid var(--ts-brand);
|
|
outline-offset: -2px;
|
|
}
|
|
}
|
|
.ts-agenda__day {
|
|
margin: 0 0 0.25rem;
|
|
color: var(--ts-muted);
|
|
font-size: 0.75rem;
|
|
text-transform: capitalize;
|
|
strong {
|
|
color: var(--ts-ink);
|
|
font-size: 1rem;
|
|
}
|
|
}
|
|
.ts-agenda__item {
|
|
display: grid;
|
|
gap: 0.0625rem;
|
|
padding: 0.25rem 0.375rem;
|
|
border-left: 3px solid var(--ts-brand);
|
|
border-radius: 0.375rem;
|
|
background: var(--ts-surface);
|
|
color: inherit;
|
|
font-family: var(--ts-font-mono);
|
|
font-size: 0.6875rem;
|
|
text-decoration: none;
|
|
&[data-kind='SPRINT_END'] {
|
|
border-left-color: var(--ts-c-amber, #c98a12);
|
|
}
|
|
&[data-kind='RELEASE'] {
|
|
border-left-color: var(--ts-c-green, #2e9e6a);
|
|
}
|
|
&:hover {
|
|
background: var(--pi-color-primary-25);
|
|
}
|
|
}
|
|
.ts-agenda__kind {
|
|
color: var(--ts-muted);
|
|
font-family: inherit;
|
|
}
|
|
.ts-agenda__title {
|
|
overflow: hidden;
|
|
font-family: var(--ts-font-body, inherit);
|
|
font-size: 0.75rem;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.ts-agenda__none {
|
|
color: var(--ts-muted);
|
|
font-size: 0.75rem;
|
|
}
|
|
@media (max-width: 720px) {
|
|
.ts-agenda {
|
|
grid-template-columns: minmax(0, 1fr);
|
|
li {
|
|
min-height: 0;
|
|
}
|
|
/* On a phone, only the days with something (and today). */
|
|
li.is-empty:not(.is-today) {
|
|
display: none;
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
})
|
|
export class WeekAgendaComponent {
|
|
readonly items = input.required<AgendaItem[]>();
|
|
readonly now = input<Date>(new Date());
|
|
|
|
protected readonly count = computed(() => this.items().length);
|
|
protected readonly days = computed(() => {
|
|
const now = this.now();
|
|
const start = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate());
|
|
return Array.from({ length: 7 }, (_, n) => {
|
|
const time = start + n * DAY;
|
|
const date = new Date(time).toISOString().slice(0, 10);
|
|
return {
|
|
date,
|
|
today: n === 0,
|
|
weekday: new Date(time).toLocaleDateString('es', {
|
|
weekday: 'short',
|
|
timeZone: 'UTC',
|
|
}),
|
|
number: new Date(time).getUTCDate(),
|
|
items: this.items().filter((i) => i.day === date),
|
|
};
|
|
});
|
|
});
|
|
|
|
protected kindOf(item: AgendaItem): string {
|
|
return KIND[item.kind];
|
|
}
|
|
|
|
protected link(item: AgendaItem): unknown[] {
|
|
return item.issueKey
|
|
? ['/p', item.projectKey, 'issues', item.issueKey]
|
|
: [
|
|
'/p',
|
|
item.projectKey,
|
|
item.kind === 'RELEASE' ? 'releases' : 'sprints',
|
|
];
|
|
}
|
|
}
|