feat: due-date reminders

Every hour the API reminds whoever has a ticket, and whoever follows it,
the day before its due date, on the day and once when it is overdue, in
the app and by e-mail for those who asked. Cards on the board flag what
is due soon or late.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 18:36:19 +02:00
parent d6a434ca1d
commit 71b807b94b
19 changed files with 371 additions and 7 deletions

View File

@@ -6,7 +6,12 @@ import {
} from '@angular/core';
import { RouterLink } from '@angular/router';
import { PiIconComponent } from '@pi/ui';
import { IssueSummary, Workflow } from '@task-stream/web/tracker/domain';
import {
DueState,
dueState,
IssueSummary,
Workflow,
} from '@task-stream/web/tracker/domain';
import { initials, PRIORITY, PRIORITY_LEVEL, typeLook } from './copy';
/** A ticket on the board / in lists: type stripe, key, title, signals. */
@@ -45,6 +50,14 @@ import { initials, PRIORITY, PRIORITY_LEVEL, typeLook } from './copy';
>↑ {{ parent.key }}</span
>
}
@if (due(); as state) {
<span
class="ts-card__due"
[attr.data-due]="state"
[attr.title]="'Fecha límite: ' + issue().dueDate"
>{{ dueLabel(state) }}</span
>
}
@if (issue().storyPoints !== null) {
<span class="ts-card__points" title="Estimación">{{
issue().storyPoints
@@ -78,6 +91,25 @@ import { initials, PRIORITY, PRIORITY_LEVEL, typeLook } from './copy';
:host {
display: block;
}
.ts-card__due {
padding: 0 0.375rem;
border-radius: 999px;
font-size: 0.6875rem;
font-weight: 600;
background: color-mix(
in srgb,
var(--ts-c-amber, #d97706) 18%,
transparent
);
&[data-due='overdue'] {
background: color-mix(
in srgb,
var(--ts-c-red, #dc2626) 18%,
transparent
);
color: var(--ts-c-red, inherit);
}
}
.ts-card__points {
display: inline-grid;
place-items: center;
@@ -227,6 +259,16 @@ import { initials, PRIORITY, PRIORITY_LEVEL, typeLook } from './copy';
`,
})
export class IssueCardComponent {
/** Late, due today or in the next two days. */
protected readonly due = computed(() =>
dueState(this.issue().dueDate, this.issue().statusCategory),
);
protected dueLabel(state: DueState): string {
if (state === 'overdue') return 'Vencido';
if (state === 'today') return 'Vence hoy';
const day = new Date(`${this.issue().dueDate?.slice(0, 10)}T00:00:00`);
return day.toLocaleDateString('es', { day: 'numeric', month: 'short' });
}
readonly issue = input.required<IssueSummary>();
/** The project's settings: names, icons and colours of types and areas. */
readonly workflow = input.required<Workflow>();