feat: a calendar to subscribe to, and the backlog's corners

- Each person can subscribe their dates in Google Calendar, Outlook or
  Apple Calendar: GET /api/calendar/{token}.ics (no session) brings
  their unfinished tickets with a due date (assigned or followed), the
  planned and running sprints and the planned releases of their
  projects, as all-day events linking to the web. The token is made,
  renewed or dropped under /me/calendar (and in GraphQL); in the web,
  from «Calendario» in «Mi trabajo».
- The backlog panel keeps the selected rows' background inside its
  rounded corners.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 22:48:33 +02:00
parent 993f80b4b9
commit f45069a327
38 changed files with 1417 additions and 54 deletions

View File

@@ -1,5 +1,6 @@
export * from './lib/notifications';
export * from './lib/facades/projects.facade';
export * from './lib/facades/calendar.facade';
export * from './lib/facades/project.facade';
export * from './lib/facades/board.facade';
export * from './lib/facades/issue.facade';

View File

@@ -0,0 +1,56 @@
import { inject, Injectable, signal } from '@angular/core';
import { CalendarFeed, CalendarGateway } from '@task-stream/web/tracker/domain';
import { Observable } from 'rxjs';
/**
* My calendar subscription for presenters: the secret URL to paste into a
* calendar app, made, renewed or dropped here. Per person and seldom
* touched, so it lives in this facade alone, without a store.
*/
@Injectable({ providedIn: 'root' })
export class CalendarFacade {
private readonly gateway = inject(CalendarGateway);
readonly feed = signal<CalendarFeed | null>(null);
readonly loaded = signal(false);
readonly busy = signal(false);
readonly error = signal<string | null>(null);
load(): void {
this.run(this.gateway.mine());
}
/** A new URL: the previous one, if any, stops working. */
create(): void {
this.run(this.gateway.create());
}
revoke(): void {
this.busy.set(true);
this.gateway.revoke().subscribe({
next: () => this.done(null),
error: () => this.failed(),
});
}
private run(call: Observable<CalendarFeed | null>): void {
this.busy.set(true);
this.error.set(null);
call.subscribe({
next: (feed) => this.done(feed),
error: () => this.failed(),
});
}
private done(feed: CalendarFeed | null): void {
this.feed.set(feed);
this.loaded.set(true);
this.busy.set(false);
}
private failed(): void {
this.loaded.set(true);
this.busy.set(false);
this.error.set('No se ha podido. Inténtalo de nuevo en un momento.');
}
}