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

@@ -10,3 +10,4 @@ export * from './lib/models/estimation';
export * from './lib/models/swimlanes';
export * from './lib/models/roadmap';
export * from './lib/models/shortcuts';
export * from './lib/models/due';

View File

@@ -0,0 +1,16 @@
import { StatusCategory } from './enums';
import { dueState } from './due';
describe('due state', () => {
const today = '2026-10-08';
it('flags late, today and the next two days, never finished tickets', () => {
expect(dueState('2026-10-07', StatusCategory.TODO, today)).toBe('overdue');
expect(dueState('2026-10-08', StatusCategory.IN_PROGRESS, today)).toBe(
'today',
);
expect(dueState('2026-10-10', StatusCategory.QA, today)).toBe('soon');
expect(dueState('2026-10-11', StatusCategory.TODO, today)).toBeNull();
expect(dueState('2026-10-01', StatusCategory.DONE, today)).toBeNull();
expect(dueState(null, StatusCategory.TODO, today)).toBeNull();
});
});

View File

@@ -0,0 +1,27 @@
import { StatusCategory } from './enums';
export type DueState = 'overdue' | 'today' | 'soon' | null;
/** `YYYY-MM-DD` of a local date. */
export function localDay(date: Date): string {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
}
/**
* How urgent a due date looks: overdue, today, or within the next two days.
* Finished tickets are never late.
*/
export function dueState(
dueDate: string | null,
category: StatusCategory,
today = localDay(new Date()),
): DueState {
if (!dueDate) return null;
if (category === StatusCategory.DONE || category === StatusCategory.CANCELLED)
return null;
const due = dueDate.slice(0, 10);
if (due < today) return 'overdue';
if (due === today) return 'today';
const days = (Date.parse(due) - Date.parse(today)) / 86_400_000;
return days <= 2 ? 'soon' : null;
}