diff --git a/docs/04-cliente-web.md b/docs/04-cliente-web.md index 5bdce11..f608ffb 100644 --- a/docs/04-cliente-web.md +++ b/docs/04-cliente-web.md @@ -296,24 +296,24 @@ visualización de la plataforma: ## 4.6 Rutas -| Ruta | Página | -| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `/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, 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 | -| `/portfolio` | 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; en directo: se vuelve a leer sola cuando cambia un ticket, sprint o release de esos proyectos | -| `/explore` | Proyectos públicos (sin sesión) | -| `/p/:key/board` | Tablero kanban (`?release=` y `?assignee=` preseleccionan los filtros) | -| `/p/:key/issues/:issueKey` | Ticket. Encima del título, quién más lo está viendo o editando ahora (`PresenceFacade`: late cada 10 s; editar es tener abierto el formulario del ticket o de un comentario, o estar escribiendo uno) | -| `/p/:key/releases` | Releases | -| `/p/:key/report/:kind/:id` | Informe para imprimir de un sprint (`sprint`) o una release (`release`): datos, totales, notas y tickets por etapa. «Imprimir o guardar en PDF» abre el diálogo del navegador; al imprimir se quitan la cabecera, las pestañas y los fondos (`styles.scss`) | -| `/p/:key/roadmap` | Hoja de ruta: épicas en el tiempo, con sprints y releases; flechas «bloquea» / «depende de» entre épicas y aviso si una empieza antes de que acabe la que la bloquea | -| `/p/:key/dashboard` | Seguimiento: cifras, flujo por estado, lo que ha cambiado desde tu última visita (guardada en este navegador), sprint en curso, tickets en riesgo, previsión de las próximas releases, antigüedad de lo que está en curso, cuellos de botella, salud del backlog (cada número abre la lista filtrada: `?missing=estimate`, `area`, `assignee` o `?staleDays=90`), calidad (bugs por prioridad y por semana, QA por área, bugs tras cada release), abiertos por un campo de lista a elegir, el recorrido de un ticket (dónde se va el tiempo de creado a hecho y qué parte es trabajo), la actividad de 12 semanas en un mapa de calor, el embudo de propuestas del público (si las hay), equipo con sus puntos abiertos y tipos. Se recarga solo con cada cambio | -| `/p/:key/analytics` | Estadísticas: creados vs. resueltos, flujo acumulado, burnup de una release, tiempos de ciclo y entrega, QA y resueltos por persona (30 días a 1 año) | -| `/p/:key/settings` | Configuración (propietario y admins; pestaña oculta para el resto), con un menú de secciones (`?section=`): General (visibilidad, propuestas), Flujo de trabajo (estados, transiciones, definición de «Hecho»), Tickets (tipos, áreas, campos), Equipos, Automatizaciones, Integraciones (Git, webhooks), Importar y exportar y Zona peligrosa; archivar en lugar de borrar, y un punto en el menú donde hay cambios sin guardar | -| `/p/:key/activity` | Actividad en vivo | -| `/p/:key/propose` | Formulario público para reportar un error o proponer una idea sin cuenta (si el proyecto lo tiene abierto); llegan como tickets con la etiqueta «propuesta» | -| `/p/:key/members` | Miembros, con su rol (propietario, admin, miembro, lector o invitado). Un invitado de un proyecto privado solo tiene Tablero, Lista y Miembros, ve solo sus tickets y abre tickets sin asignarlos ni planificarlos | +| Ruta | Página | +| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `/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, 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 | +| `/portfolio` | 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; en directo: se vuelve a leer sola cuando cambia un ticket, sprint o release de esos proyectos | +| `/explore` | Proyectos públicos (sin sesión) | +| `/p/:key/board` | Tablero kanban (`?release=` y `?assignee=` preseleccionan los filtros) | +| `/p/:key/issues/:issueKey` | Ticket. Encima del título, quién más lo está viendo o editando ahora (`PresenceFacade`: late cada 10 s; editar es tener abierto el formulario del ticket o de un comentario, o estar escribiendo uno) | +| `/p/:key/releases` | Releases | +| `/p/:key/report/:kind/:id` | Informe para imprimir de un sprint (`sprint`) o una release (`release`): datos, totales, notas y tickets por etapa. «Imprimir o guardar en PDF» abre el diálogo del navegador; al imprimir se quitan la cabecera, las pestañas y los fondos (`styles.scss`) | +| `/p/:key/roadmap` | Hoja de ruta: épicas en el tiempo, con sprints y releases; flechas «bloquea» / «depende de» entre épicas y aviso si una empieza antes de que acabe la que la bloquea | +| `/p/:key/dashboard` | Seguimiento: cifras, flujo por estado, lo que ha cambiado desde tu última visita (guardada en este navegador), sprint en curso, tickets en riesgo, previsión de las próximas releases, antigüedad de lo que está en curso, cuellos de botella, salud del backlog (cada número abre la lista filtrada: `?missing=estimate`, `area`, `assignee` o `?staleDays=90`), calidad (bugs por prioridad y por semana, QA por área, bugs tras cada release), abiertos por un campo de lista a elegir, el recorrido de un ticket (dónde se va el tiempo de creado a hecho y qué parte es trabajo), la actividad de 12 semanas en un mapa de calor, el embudo de propuestas del público (si las hay), equipo con sus puntos abiertos y tipos. Se recarga solo con cada cambio. «Modo pantalla» lo pone a pantalla completa para una tele del equipo: un bloque cada vez, ampliado, que cambia cada 15 s (← → para moverse, espacio para pausar, Esc para salir), con la hora y en directo | +| `/p/:key/analytics` | Estadísticas: creados vs. resueltos, flujo acumulado, burnup de una release, tiempos de ciclo y entrega, QA y resueltos por persona (30 días a 1 año) | +| `/p/:key/settings` | Configuración (propietario y admins; pestaña oculta para el resto), con un menú de secciones (`?section=`): General (visibilidad, propuestas), Flujo de trabajo (estados, transiciones, definición de «Hecho»), Tickets (tipos, áreas, campos), Equipos, Automatizaciones, Integraciones (Git, webhooks), Importar y exportar y Zona peligrosa; archivar en lugar de borrar, y un punto en el menú donde hay cambios sin guardar | +| `/p/:key/activity` | Actividad en vivo | +| `/p/:key/propose` | Formulario público para reportar un error o proponer una idea sin cuenta (si el proyecto lo tiene abierto); llegan como tickets con la etiqueta «propuesta» | +| `/p/:key/members` | Miembros, con su rol (propietario, admin, miembro, lector o invitado). Un invitado de un proyecto privado solo tiene Tablero, Lista y Miembros, ve solo sus tickets y abre tickets sin asignarlos ni planificarlos | Solo `/` está protegida por `authGuard` (redirige a `/login?returnUrl=…`); `/login` y `/auth/callback` son para quien no tiene sesión, y el resto se puede abrir sin ella si el proyecto es público. Los parámetros de ruta llegan como inputs de diff --git a/docs/10-futuro.md b/docs/10-futuro.md index 5ce3dc7..95cefaa 100644 --- a/docs/10-futuro.md +++ b/docs/10-futuro.md @@ -128,3 +128,5 @@ Mucho trabajo para lo que aportan hoy; se pueden recuperar si alguien los necesi - Panel del proyecto, cuarta parte: actividad de 12 semanas en un mapa de calor, el recorrido de un ticket (dónde se va el tiempo de creado a hecho, espera incluida, y la parte que es trabajo) y el embudo de propuestas del público con las más votadas sin respuesta. +- Modo pantalla del panel: a pantalla completa para una tele del equipo, un bloque cada vez que cambia solo, en + directo y con la hora. diff --git a/docs/TaskStream-Documentacion.docx b/docs/TaskStream-Documentacion.docx index 0376926..e9dff2b 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 fb5d201..955add3 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/dashboard.page.ts b/libs/web/tracker/presenters/src/lib/pages/dashboard.page.ts index 31bc62e..fbcbd7f 100644 --- a/libs/web/tracker/presenters/src/lib/pages/dashboard.page.ts +++ b/libs/web/tracker/presenters/src/lib/pages/dashboard.page.ts @@ -1,9 +1,15 @@ import { + afterRenderEffect, ChangeDetectionStrategy, Component, computed, + DestroyRef, + DOCUMENT, effect, + ElementRef, + HostListener, inject, + signal, untracked, } from '@angular/core'; import { RouterLink } from '@angular/router'; @@ -38,6 +44,9 @@ import { ActivityHeatmapComponent } from '../ui/insights/activity-heatmap'; import { JourneyComponent } from '../ui/insights/journey'; import { ProposalsFunnelComponent } from '../ui/insights/proposals-funnel'; +/** Seconds each block stays in «Modo pantalla». */ +const SLIDE_SECONDS = 15; + /** "Seguimiento": the state of the whole project at a glance (live). */ @Component({ selector: 'ts-dashboard-page', @@ -61,7 +70,48 @@ import { ProposalsFunnelComponent } from '../ui/insights/proposals-funnel'; ProposalsFunnelComponent, ], providers: [provideInsightsFacade()], + host: { '[class.is-screen]': 'screen()' }, template: ` + @if (screen()) { +
+ + {{ facade.project()?.key }} + {{ facade.project()?.name }} + + + @for (n of slideNumbers(); track n) { + + } + + + + En directo + + + + +
+ } @else { +
+ +
+ } @if (facade.stats(); as stats) {
@for (kpi of kpis(); track kpi.label) { @@ -262,6 +312,89 @@ import { ProposalsFunnelComponent } from '../ui/insights/proposals-funnel'; } `, styles: ` + .ts-screen-open { + display: flex; + justify-content: flex-end; + margin-bottom: 1rem; + } + /* Modo pantalla: over everything, one block at a time, larger. */ + :host(.is-screen) { + position: fixed; + inset: 0; + z-index: 2000; + display: block; + overflow: auto; + padding: 1.25rem 2.5rem 2.5rem; + background: var(--ts-bg); + } + :host(.is-screen) .ts-dashboard, + :host(.is-screen) .ts-insights { + display: block; + } + :host(.is-screen) .ts-slide-off { + display: none !important; + } + :host(.is-screen) .ts-slide-on { + max-width: 64rem; + margin: 0 auto 1.25rem; + zoom: 1.5; + } + .ts-screen-bar { + display: flex; + align-items: center; + gap: 1.25rem; + margin-bottom: 1.5rem; + font-size: 1.125rem; + } + .ts-screen-bar__project { + margin-right: auto; + white-space: nowrap; + strong { + font-family: var(--ts-font-mono); + margin-right: 0.5rem; + } + } + .ts-screen-bar__dots { + display: flex; + gap: 0.375rem; + /* Plain dots: past the app's button styles. */ + button { + all: unset; + width: 0.625rem !important; + min-width: 0 !important; + height: 0.625rem !important; + padding: 0 !important; + border-radius: 50%; + background: var(--ts-line-strong); + cursor: pointer; + &.is-current { + background: var(--ts-brand); + } + &:focus-visible { + outline: 2px solid var(--ts-brand); + outline-offset: 2px; + } + } + } + .ts-screen-bar__live { + display: inline-flex; + white-space: nowrap; + align-items: center; + gap: 0.5rem; + color: var(--ts-muted); + font-size: 0.875rem; + font-weight: 600; + } + .ts-screen-bar__dot { + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + background: var(--ts-c-green); + } + .ts-screen-bar__clock { + font-family: var(--ts-font-mono); + font-weight: 600; + } /* Same stat row as pireon.dev's hero. */ .ts-kpis { display: grid; @@ -480,11 +613,130 @@ import { ProposalsFunnelComponent } from '../ui/insights/proposals-funnel'; export class DashboardPage { protected readonly facade = inject(ProjectFacade); protected readonly insights = inject(InsightsFacade); + private readonly host = inject>(ElementRef); + private readonly document = inject(DOCUMENT); + + /* --- Modo pantalla: for a team's TV, a block at a time, rotating --- */ + protected readonly screen = signal(false); + protected readonly paused = signal(false); + private readonly slide = signal(0); + private readonly slides = signal(0); + protected readonly current = computed(() => + this.slides() ? this.slide() % this.slides() : 0, + ); + protected readonly slideNumbers = computed(() => + Array.from({ length: this.slides() }, (_, n) => n), + ); + protected readonly clock = signal(''); + private timer: ReturnType | null = null; + private ticks = 0; + + private readonly marks = afterRenderEffect(() => { + this.screen(); + this.current(); + this.insights.insights(); + this.markSlides(); + }); + + protected enterScreen(): void { + this.screen.set(true); + this.paused.set(false); + this.slide.set(0); + this.updateClock(); + this.host.nativeElement.requestFullscreen?.().catch(() => undefined); + this.timer = setInterval(() => this.tick(), 1000); + } + + protected exitScreen(): void { + if (!this.screen()) return; + this.screen.set(false); + if (this.timer) clearInterval(this.timer); + this.timer = null; + if (this.document.fullscreenElement) + this.document.exitFullscreen().catch(() => undefined); + } + + protected togglePause(): void { + this.paused.update((p) => !p); + this.ticks = 0; + } + + protected go(n: number): void { + this.slide.set(n); + this.ticks = 0; + } + + /** Every second: the clock, and every 15 s (unless paused) the next block. */ + private tick(): void { + this.updateClock(); + if (this.paused()) return; + if (++this.ticks >= SLIDE_SECONDS) { + this.ticks = 0; + this.slide.update((n) => n + 1); + } + } + + private updateClock(): void { + this.clock.set( + new Date().toLocaleTimeString('es', { + hour: '2-digit', + minute: '2-digit', + }), + ); + } + + /** + * The blocks to rotate: the figures and the flow together first, then + * every panel on its own. Shown or hidden by a class, so the live + * updates keep landing on the same elements. + */ + private markSlides(): void { + const root = this.host.nativeElement; + const groups: Element[][] = []; + const lead = [ + ...root.querySelectorAll(':scope > .ts-kpis, :scope > .ts-flow'), + ]; + if (lead.length) groups.push(lead); + root + .querySelectorAll(':scope > .ts-dashboard > *') + .forEach((panel) => groups.push([panel])); + this.slides.set(groups.length); + const on = this.screen(); + groups.forEach((group, n) => + group.forEach((el) => { + el.classList.toggle('ts-slide-on', on && n === this.current()); + el.classList.toggle('ts-slide-off', on && n !== this.current()); + }), + ); + } + + @HostListener('document:keydown', ['$event']) + protected onKey(event: KeyboardEvent): void { + if (!this.screen()) return; + if (event.key === 'ArrowRight') this.go(this.current() + 1); + else if (event.key === 'ArrowLeft') + this.go( + (this.current() - 1 + this.slides()) % Math.max(1, this.slides()), + ); + else if (event.key === ' ') this.togglePause(); + else if (event.key === 'Escape') this.exitScreen(); + else return; + event.preventDefault(); + } + + /** Leaving the browser's full screen (Esc) leaves the mode too. */ + @HostListener('document:fullscreenchange') + protected onFullscreenChange(): void { + if (!this.document.fullscreenElement) this.exitScreen(); + } /** Opened once per project: the project itself changes on every update. */ private openedFor: string | null = null; constructor() { + inject(DestroyRef).onDestroy(() => { + if (this.timer) clearInterval(this.timer); + }); effect(() => { const projectId = this.facade.project()?.id; if (!projectId || projectId === this.openedFor) return;