diff --git a/docs/04-cliente-web.md b/docs/04-cliente-web.md index c2a459e..53a7463 100644 --- a/docs/04-cliente-web.md +++ b/docs/04-cliente-web.md @@ -286,7 +286,7 @@ visualización de la plataforma: | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `/login` | Inicio de sesión (solo sin sesión) | | `/auth/callback` | Retorno de Pireon (PKCE) | -| `/` | Inicio: saludo según la hora con el resumen del día, «Ir a…» (`WEB-12`), primeros pasos, la semana (`ts-week-agenda`), mis vistas guardadas, mi trabajo (`ts-my-work`), lo último en mis proyectos, la semana en cifras, sprints en curso, quién está ahora, «Retomar» y mis proyectos con su actividad | +| `/` | Inicio: saludo según la hora con el resumen del día, «Ir a…» (`WEB-12`), primeros pasos, cuatro cifras (`ts-home-kpis`: abiertos míos, esperando QA, terminados por mí y por el equipo esta semana), la semana (`ts-week-agenda`), mis vistas guardadas y dos columnas: mis tickets y lo último en mis proyectos; sprints en curso, esperando QA, que sigo, próximas releases, quién está ahora y «Retomar». Debajo, mis proyectos con su actividad | | `/cartera` | Cartera (con sesión): una tarjeta por proyecto activo con lo abierto y en riesgo, el sprint en curso (¿llega?), las próximas releases con su previsión y las épicas abiertas | | `/explorar` | Proyectos públicos (sin sesión) | | `/p/:key/board` | Tablero kanban (`?release=` y `?assignee=` preseleccionan los filtros) | diff --git a/docs/TaskStream-Documentacion.docx b/docs/TaskStream-Documentacion.docx index 9a4a66c..341092e 100644 Binary files a/docs/TaskStream-Documentacion.docx and b/docs/TaskStream-Documentacion.docx differ diff --git a/docs/TaskStream-Documentacion.pdf b/docs/TaskStream-Documentacion.pdf index 6392d70..7daef32 100644 Binary files a/docs/TaskStream-Documentacion.pdf and b/docs/TaskStream-Documentacion.pdf differ diff --git a/libs/web/tracker/presenters/src/lib/pages/my-work.ts b/libs/web/tracker/presenters/src/lib/pages/my-work.ts index 7688bce..adc1dcc 100644 --- a/libs/web/tracker/presenters/src/lib/pages/my-work.ts +++ b/libs/web/tracker/presenters/src/lib/pages/my-work.ts @@ -5,6 +5,7 @@ import { DestroyRef, DOCUMENT, inject, + input, OnInit, TemplateRef, viewChild, @@ -34,7 +35,6 @@ import { typeLook, } from '../ui/copy'; import { CalendarFeedComponent } from '../ui/calendar-feed'; -import { MyViewsComponent } from '../ui/my-views'; const VISIBLE = 12; @@ -52,213 +52,199 @@ const VISIBLE = 12; PiIconComponent, PiSkeletonComponent, CalendarFeedComponent, - MyViewsComponent, ], template: ` @if (!home.loaded()) { -
+ @if (part() === 'main') { -
- - -
-
- } @else { - -
-
-
-

Mis tickets

- {{ home.mineTotal() }} abiertos + + } + } @else if (part() === 'main') { +
+
+

Mis tickets

+ {{ home.mineTotal() }} abiertos + +
+ @if (home.qaFailed()) { +

+

+ } + @for (group of visibleGroups(); track group.category) { +

+ {{ categoryLabel(group.category) }} - -

- @if (home.qaFailed()) { -

-

- } - @for (group of visibleGroups(); track group.category) { -

- {{ categoryLabel(group.category) }} - {{ group.issues.length }} -

- + } @empty { +

No tienes tickets abiertos asignados.

+ @if (suggestions().length) { + } - @if (home.mineTotal() > shown()) { -

- y {{ home.mineTotal() - shown() }} más, en los tableros de cada - proyecto -

+ } + @if (home.mineTotal() > shown()) { +

+ y {{ home.mineTotal() - shown() }} más, en los tableros de cada + proyecto +

+ } +
+ } @else { +
+
+

Esperando QA

+ {{ home.awaitingQaTotal() }} +
+
+ + -
-
-
-

Esperando QA

- {{ home.awaitingQaTotal() }} -
- -
+
+
+

Que sigo

+ {{ home.followingTotal() }} +
+ +
-
-
-

Que sigo

- {{ home.followingTotal() }} -
- -
- -
-
-

Próximas releases

-
- -
-
-
+
+
+

Próximas releases

+
+ +
} @@ -266,21 +252,9 @@ const VISIBLE = 12; `, styles: ` :host { - display: block; - margin-bottom: 2.5rem; - } - .ts-work { - display: grid; - grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr); - gap: 1.25rem; - align-items: start; - @media (max-width: 960px) { - grid-template-columns: minmax(0, 1fr); - } - } - .ts-work__side { display: grid; gap: 1.25rem; + align-content: start; } .ts-work__panel { padding: 1.375rem 1.5rem 1.25rem; @@ -466,6 +440,8 @@ const VISIBLE = 12; `, }) export class MyWorkComponent implements OnInit { + /** «main»: my tickets (and my views); «side»: QA, following, releases. */ + readonly part = input<'main' | 'side'>('main'); protected readonly home = inject(HomeFacade); private readonly myHome = inject(MyHomeFacade); /** What to do when nothing is mine (from the home reading). */ @@ -545,6 +521,8 @@ export class MyWorkComponent implements OnInit { projectTone(projectKeyOf(issueKey)); ngOnInit(): void { + // Both parts read the same store: the main one keeps it fresh. + if (this.part() !== 'main') return; this.home.load(); // Back to the tab after a while: refresh my work. const onVisible = () => { diff --git a/libs/web/tracker/presenters/src/lib/pages/projects.page.ts b/libs/web/tracker/presenters/src/lib/pages/projects.page.ts index b51a13f..8553581 100644 --- a/libs/web/tracker/presenters/src/lib/pages/projects.page.ts +++ b/libs/web/tracker/presenters/src/lib/pages/projects.page.ts @@ -51,7 +51,8 @@ import { HomeSprintsComponent } from '../ui/home/home-sprints'; import { OnlineNowComponent } from '../ui/home/online-now'; import { ResumeComponent } from '../ui/home/resume'; import { WeekAgendaComponent } from '../ui/home/week-agenda'; -import { WeekStatsComponent } from '../ui/home/week-stats'; +import { HomeKpisComponent } from '../ui/home/kpis'; +import { MyViewsComponent } from '../ui/my-views'; /** How often the home page refreshes its reading while open. */ const REFRESH_MS = 60_000; @@ -72,7 +73,8 @@ const REFRESH_MS = 60_000; OnlineNowComponent, ResumeComponent, WeekAgendaComponent, - WeekStatsComponent, + HomeKpisComponent, + MyViewsComponent, PiActionButtonComponent, PiEmptyStateComponent, PiSkeletonComponent, @@ -106,29 +108,43 @@ const REFRESH_MS = 60_000; [firstKey]="facade.sorted()[0]?.key ?? null" (createProject)="openCreate()" /> - @if (facade.projects().length) { - - } } @if (facade.projects().length) { - @if (myHome.home(); as h) { -
- -
- - @if (h.sprints.length) { - - } - - -
-
+ + } + + +
+
+ + @if (myHome.home(); as h) { + + } +
+
+ @if (myHome.home()?.sprints?.length) { + + } + + @if (myHome.home(); as h) { + + } + +
+

Proyectos @@ -245,22 +261,25 @@ const REFRESH_MS = 60_000; font-size: 0.8125rem; } ts-first-steps, - ts-week-agenda { + ts-home-kpis, + ts-week-agenda, + ts-my-views { margin-bottom: 1.25rem; } .ts-home { display: grid; - grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr); + grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); gap: 1.25rem; align-items: start; - margin: 1.25rem 0 2rem; + margin-bottom: 2.5rem; @media (max-width: 960px) { grid-template-columns: minmax(0, 1fr); } } - .ts-home__side { + .ts-home__column { display: grid; gap: 1.25rem; + min-width: 0; } .ts-public-check { p { diff --git a/libs/web/tracker/presenters/src/lib/ui/home/kpis.ts b/libs/web/tracker/presenters/src/lib/ui/home/kpis.ts new file mode 100644 index 0000000..1e69015 --- /dev/null +++ b/libs/web/tracker/presenters/src/lib/ui/home/kpis.ts @@ -0,0 +1,180 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + input, +} from '@angular/core'; +import { Home } from '@task-stream/web/tracker/domain'; + +/** «+30 %» or «−10 %» against the week before (nothing when there was none). */ +function change(now: number, before: number): string { + if (!before) return now ? 'nuevo' : ''; + const pct = Math.round(((now - before) / before) * 100); + return `${pct >= 0 ? '+' : '−'}${Math.abs(pct)} %`; +} + +/** The home page's figures, four tiles of the same size. */ +@Component({ + selector: 'ts-home-kpis', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { role: 'region', 'aria-label': 'Cifras' }, + template: ` +
+

Abiertos tuyos

+

{{ mine() }}

+

+ @if (overdue()) { + {{ overdue() }} {{ overdue() === 1 ? 'vencido' : 'vencidos' }} + @if (dueToday()) { + · {{ dueToday() }} hoy + } + } @else if (dueToday()) { + {{ dueToday() }} {{ dueToday() === 1 ? 'vence' : 'vencen' }} hoy + } @else { + Nada vence hoy + } +

+
+
+

Esperando QA

+

{{ awaitingQa() }}

+

En tus proyectos

+
+
+

Terminados por ti

+

+ {{ me().thisWeek }} + @if (myChange(); as c) { + {{ + c + }} + } +

+

+ @if (me().streak > 1) { + {{ me().streak }} días seguidos + } @else if (me().streak === 1) { + Esta semana · racha de 1 día + } @else { + Esta semana · sin racha + } +

+
+
+

Terminados por el equipo

+

+ {{ team().thisWeek }} + @if (teamChange(); as c) { + {{ + c + }} + } +

+ +
+ `, + styles: ` + :host { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 1.25rem; + @media (max-width: 900px) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.75rem; + } + } + .ts-kpi { + display: flex; + flex-direction: column; + gap: 0.125rem; + min-width: 0; + padding: 1.125rem 1.25rem; + } + .ts-kpi__label { + margin: 0; + color: var(--ts-muted); + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + } + .ts-kpi__value { + display: flex; + align-items: baseline; + gap: 0.5rem; + margin: 0.25rem 0 0; + font-size: 2rem; + font-weight: 700; + line-height: 1.1; + letter-spacing: -0.02em; + } + .ts-kpi__change { + color: var(--ts-c-green); + font-size: 0.8125rem; + font-weight: 600; + letter-spacing: 0; + &.is-down { + color: var(--ts-muted); + } + } + .ts-kpi__sub { + margin: auto 0 0; + padding-top: 0.375rem; + color: var(--ts-ink-soft); + font-size: 0.8125rem; + &.is-late { + color: var(--ts-c-red); + font-weight: 600; + } + } + .ts-kpi__bars { + display: flex; + align-items: flex-end; + gap: 2px; + height: 1.5rem; + margin-top: auto; + padding-top: 0.375rem; + span { + flex: 1; + min-height: 2px; + border-radius: 2px 2px 0 0; + background: color-mix(in srgb, var(--ts-brand) 30%, transparent); + &.is-this-week { + background: var(--ts-brand); + } + } + } + `, +}) +export class HomeKpisComponent { + readonly mine = input.required(); + readonly dueToday = input.required(); + readonly overdue = input.required(); + readonly awaitingQa = input.required(); + readonly me = input.required(); + readonly team = input.required(); + + protected readonly myChange = computed(() => + change(this.me().thisWeek, this.me().lastWeek), + ); + protected readonly teamChange = computed(() => + change(this.team().thisWeek, this.team().lastWeek), + ); + private readonly max = computed(() => Math.max(1, ...this.team().perDay)); + + protected height(n: number): number { + return (n / this.max()) * 100; + } +} diff --git a/libs/web/tracker/presenters/src/lib/ui/home/week-agenda.ts b/libs/web/tracker/presenters/src/lib/ui/home/week-agenda.ts index 54cfaeb..acc2b8e 100644 --- a/libs/web/tracker/presenters/src/lib/ui/home/week-agenda.ts +++ b/libs/web/tracker/presenters/src/lib/ui/home/week-agenda.ts @@ -32,7 +32,7 @@ const KIND: Record = {
    @for (day of days(); track day.date) { -
  1. +
  2. {{ day.weekday }} {{ day.number }}

    @@ -128,6 +128,10 @@ const KIND: Record = { li { min-height: 0; } + /* On a phone, only the days with something (and today). */ + li.is-empty:not(.is-today) { + display: none; + } } } `, diff --git a/libs/web/tracker/presenters/src/lib/ui/home/week-stats.ts b/libs/web/tracker/presenters/src/lib/ui/home/week-stats.ts deleted file mode 100644 index ce28e5f..0000000 --- a/libs/web/tracker/presenters/src/lib/ui/home/week-stats.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - computed, - input, -} from '@angular/core'; -import { Home } from '@task-stream/web/tracker/domain'; - -/** «+30 %» or «−10 %» against the week before (nothing when there was none). */ -function change(now: number, before: number): string { - if (!before) return now ? 'nuevo' : ''; - const pct = Math.round(((now - before) / before) * 100); - return `${pct >= 0 ? '+' : '−'}${Math.abs(pct)} %`; -} - -/** How the week goes: mine (and my streak) and my projects' teams. */ -@Component({ - selector: 'ts-week-stats', - changeDetection: ChangeDetectionStrategy.OnPush, - host: { - class: 'ts-panel', - role: 'region', - 'aria-labelledby': 'stats-title', - }, - styleUrl: './home-panel.scss', - template: ` -
    -

    La semana

    - Tickets terminados, últimos 7 días -
    -
    -
    -

    Tú

    -

    - {{ me().thisWeek }} - @if (myChange(); as c) { - {{ - c - }} - } -

    -

    - @if (me().streak > 1) { - {{ me().streak }} días seguidos terminando algo - } @else if (me().streak === 1) { - Terminaste algo hoy o ayer - } @else { - Sin racha: hoy puede empezar una - } -

    -
    -
    -

    Tus equipos

    -

    - {{ team().thisWeek }} - @if (teamChange(); as c) { - {{ - c - }} - } -

    - -
    -
    - `, - styles: ` - .ts-week { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 1rem; - } - .ts-week__label { - margin: 0; - color: var(--ts-muted); - font-size: 0.75rem; - } - .ts-week__value { - display: flex; - align-items: baseline; - gap: 0.5rem; - margin: 0.125rem 0 0.25rem; - font-size: 1.75rem; - font-weight: 700; - } - .ts-week__change { - color: var(--ts-c-green, #2e9e6a); - font-size: 0.8125rem; - font-weight: 600; - &.is-down { - color: var(--ts-muted); - } - } - .ts-week__sub { - margin: 0; - color: var(--ts-ink-soft); - font-size: 0.8125rem; - } - .ts-week__bars { - display: flex; - align-items: flex-end; - gap: 2px; - height: 2.5rem; - span { - flex: 1; - min-height: 2px; - border-radius: 2px 2px 0 0; - background: color-mix(in srgb, var(--ts-brand) 35%, transparent); - &.is-this-week { - background: var(--ts-brand); - } - } - } - `, -}) -export class WeekStatsComponent { - readonly me = input.required(); - readonly team = input.required(); - - protected readonly myChange = computed(() => - change(this.me().thisWeek, this.me().lastWeek), - ); - protected readonly teamChange = computed(() => - change(this.team().thisWeek, this.team().lastWeek), - ); - private readonly max = computed(() => Math.max(1, ...this.team().perDay)); - - protected height(n: number): number { - return (n / this.max()) * 100; - } -} diff --git a/libs/web/tracker/presenters/src/lib/ui/project-card.ts b/libs/web/tracker/presenters/src/lib/ui/project-card.ts index 85981f0..5785a23 100644 --- a/libs/web/tracker/presenters/src/lib/ui/project-card.ts +++ b/libs/web/tracker/presenters/src/lib/ui/project-card.ts @@ -60,6 +60,8 @@ import { projectTone, PUBLIC_TONE, timeAgo } from './copy'; · no llega } + } @else { + Sin sprint en curso }

} @@ -74,12 +76,13 @@ import { projectTone, PUBLIC_TONE, timeAgo } from './copy'; `, styles: ` + /* As tall as the tallest card of its row, the team always at the bottom. */ :host { - display: block; + display: grid; } .ts-project { - display: grid; - grid-template-rows: auto auto 1fr auto; + display: flex; + flex-direction: column; gap: 0.375rem; min-height: 12rem; padding: 1.5rem 1.5rem 1.25rem; @@ -103,7 +106,7 @@ import { projectTone, PUBLIC_TONE, timeAgo } from './copy'; .ts-project__stats { display: grid; gap: 0.25rem; - margin-top: 0.25rem; + margin: 0.25rem 0 0.75rem; color: var(--ts-ink-soft); font-size: 0.75rem; } @@ -145,7 +148,7 @@ import { projectTone, PUBLIC_TONE, timeAgo } from './copy'; display: flex; align-items: center; gap: 0.625rem; - margin-top: 0.75rem; + margin-top: auto; padding-top: 0.875rem; border-top: 1px dashed var(--ts-line); }