gitshark

Clone repository

git clone https://git.vilip.de/git/vilip/taskflow.git
git clone git@git.vilip.de:vilip/taskflow.git
/**
 * Daily reminder helpers
 *
 * Pure, platform-independent logic for the daily task reminder so it can be
 * unit-tested without the native notifications module.
 */

export interface ReminderTime {
  /** Hour of day in 24h format (0-23). */
  hour: number;
  /** Minute of hour (0-59). */
  minute: number;
}

/** The subset of a task the reminder needs to count it. */
export interface ReminderTask {
  dueDate?: number;
  completed?: boolean;
}

/** One scheduled reminder: when it fires and what it will say. */
export interface ReminderOccurrence {
  triggerAt: Date;
  taskCount: number;
  body: string;
}

/**
 * A reminder time for every weekday, indexed like `Date.getDay()`:
 * 0 = Sunday ... 6 = Saturday.
 */
export type ReminderSchedule = ReminderTime[];

/** Default reminder time: 12:00 (noon). */
export const DEFAULT_REMINDER_TIME: ReminderTime = { hour: 12, minute: 0 };

/** Weekday names, indexed like `Date.getDay()`. */
export const WEEKDAY_LABELS = [
  'Sunday',
  'Monday',
  'Tuesday',
  'Wednesday',
  'Thursday',
  'Friday',
  'Saturday',
];

/** Weekday indices in the order they are shown, Monday first. */
export const WEEKDAY_DISPLAY_ORDER = [1, 2, 3, 4, 5, 6, 0];

/** Build a schedule that reminds at the same time on every weekday. */
export function createUniformSchedule(time: ReminderTime): ReminderSchedule {
  return WEEKDAY_LABELS.map(() => ({ ...time }));
}

/** Default schedule: the default time on every weekday. */
export const DEFAULT_REMINDER_SCHEDULE: ReminderSchedule =
  createUniformSchedule(DEFAULT_REMINDER_TIME);

/** Copy of `schedule` with `weekday` set to `time`. */
export function withReminderTimeForWeekday(
  schedule: ReminderSchedule,
  weekday: number,
  time: ReminderTime
): ReminderSchedule {
  return schedule.map((existing, day) => (day === weekday ? { ...time } : { ...existing }));
}

/** The reminder time configured for the weekday `date` falls on. */
export function getReminderTimeForDate(
  schedule: ReminderSchedule,
  date: Date
): ReminderTime {
  return schedule[date.getDay()];
}

/** How many days of reminders are kept scheduled ahead. */
export const REMINDER_WINDOW_DAYS = 7;

/**
 * Build the reminder notification body, reflecting how many tasks are
 * scheduled for today.
 */
export function buildReminderBody(taskCount: number): string {
  if (!Number.isFinite(taskCount) || taskCount <= 0) {
    return 'No tasks scheduled for today';
  }
  const noun = taskCount === 1 ? 'task' : 'tasks';
  return `You have ${taskCount} ${noun} scheduled for today`;
}

/** Count the open tasks due on the calendar day of `date`. */
export function countTasksDueOn(tasks: ReminderTask[], date: Date): number {
  const dayStart = new Date(date);
  dayStart.setHours(0, 0, 0, 0);
  const dayEnd = new Date(dayStart);
  dayEnd.setDate(dayEnd.getDate() + 1);

  return tasks.filter(task => {
    if (task.completed) return false;
    if (!task.dueDate) return false;
    return task.dueDate >= dayStart.getTime() && task.dueDate < dayEnd.getTime();
  }).length;
}

/**
 * Build the upcoming reminders for the next `days` days, each at the time its
 * own weekday is configured for and carrying the task count of that day.
 * Occurrences whose time has already passed are left out, so the first entry
 * is always in the future.
 */
export function buildReminderOccurrences(
  tasks: ReminderTask[],
  schedule: ReminderSchedule,
  { now, days }: { now: Date; days: number }
): ReminderOccurrence[] {
  const occurrences: ReminderOccurrence[] = [];

  for (let offset = 0; offset < days; offset++) {
    const triggerAt = new Date(now);
    triggerAt.setDate(triggerAt.getDate() + offset);
    const { hour, minute } = getReminderTimeForDate(schedule, triggerAt);
    triggerAt.setHours(hour, minute, 0, 0);

    if (triggerAt.getTime() <= now.getTime()) {
      continue;
    }

    const taskCount = countTasksDueOn(tasks, triggerAt);
    occurrences.push({ triggerAt, taskCount, body: buildReminderBody(taskCount) });
  }

  return occurrences;
}

/** Format a reminder time as a zero-padded 24h string, e.g. "08:05". */
export function formatReminderTime({ hour, minute }: ReminderTime): string {
  const hh = String(hour).padStart(2, '0');
  const mm = String(minute).padStart(2, '0');
  return `${hh}:${mm}`;
}

/**
 * Describe a schedule in one phrase, naming the time when every weekday
 * shares it.
 */
export function describeReminderSchedule(schedule: ReminderSchedule): string {
  const first = formatReminderTime(schedule[0]);
  const uniform = schedule.every(time => formatReminderTime(time) === first);
  return uniform ? `${first} every day` : 'at a time set per weekday';
}

/** Validate that a reminder time has in-range, integer hour and minute. */
export function isValidReminderTime(time: ReminderTime): boolean {
  const { hour, minute } = time;
  return (
    Number.isInteger(hour) &&
    Number.isInteger(minute) &&
    hour >= 0 &&
    hour <= 23 &&
    minute >= 0 &&
    minute <= 59
  );
}

/** Validate that a schedule covers every weekday with a valid time. */
export function isValidReminderSchedule(schedule: ReminderSchedule): boolean {
  return (
    Array.isArray(schedule) &&
    schedule.length === WEEKDAY_LABELS.length &&
    schedule.every(isValidReminderTime)
  );
}

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog