Files
task-stream/libs/web/tracker/presenters/src/lib/ui/home/week-agenda.ts
Josep Piguillem 9044690348 feat(web): a home page in two columns, with four figures on top
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>
2026-10-15 21:37:45 +02:00

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',
];
}
}