mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-18 02:09:39 +02:00
feat(shared-stores,shared-ui): add shared reminder system
Add notificationService (Browser Notification API wrapper), createReminderScheduler (30s poller with source pattern for checking due reminders), and ReminderPicker UI component. Todo module gets todoReminderSource (checks task dueDate - minutesBefore) and ReminderSelector now delegates to shared ReminderPicker. Scheduler supports multiple sources (todo, calendar, planta, etc.), tag-based dedup, graceful error handling, and runtime source addition. 22 new tests (8 notification + 8 scheduler + 6 ReminderPicker). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b995d52146
commit
4fa096147c
11 changed files with 624 additions and 26 deletions
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { Bell } from '@manacore/shared-icons';
|
||||
import { ReminderPicker } from '@manacore/shared-ui';
|
||||
|
||||
interface Props {
|
||||
value: number | null;
|
||||
|
|
@ -8,30 +8,6 @@
|
|||
}
|
||||
|
||||
let { value, onChange, disabled = false }: Props = $props();
|
||||
|
||||
const options = [
|
||||
{ label: 'Keine', value: null },
|
||||
{ label: '5 Min', value: 5 },
|
||||
{ label: '15 Min', value: 15 },
|
||||
{ label: '30 Min', value: 30 },
|
||||
{ label: '1 Std', value: 60 },
|
||||
{ label: '1 Tag', value: 1440 },
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Bell size={14} class="text-muted-foreground" />
|
||||
<select
|
||||
{disabled}
|
||||
value={value ?? ''}
|
||||
onchange={(e) => {
|
||||
const v = e.currentTarget.value;
|
||||
onChange(v === '' ? null : Number(v));
|
||||
}}
|
||||
class="rounded-md border border-border bg-transparent px-2 py-1 text-xs text-foreground focus:border-primary focus:outline-none disabled:opacity-40"
|
||||
>
|
||||
{#each options as opt}
|
||||
<option value={opt.value ?? ''}>{opt.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<ReminderPicker {value} {onChange} {disabled} />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
/**
|
||||
* Todo Reminder Source — provides due reminders to the shared scheduler.
|
||||
*
|
||||
* Checks all pending reminders against their task's dueDate.
|
||||
* A reminder is due when: dueDate - minutesBefore <= now.
|
||||
*/
|
||||
|
||||
import { db } from '$lib/data/database';
|
||||
import type { ReminderSource, DueReminder } from '@manacore/shared-stores';
|
||||
import type { LocalTask, LocalReminder } from './types';
|
||||
|
||||
export const todoReminderSource: ReminderSource = {
|
||||
id: 'todo',
|
||||
|
||||
async checkDue(): Promise<DueReminder[]> {
|
||||
const reminders = await db.table<LocalReminder>('reminders').toArray();
|
||||
const pending = reminders.filter((r) => r.status === 'pending' && !r.deletedAt);
|
||||
if (pending.length === 0) return [];
|
||||
|
||||
const tasks = await db.table<LocalTask>('tasks').toArray();
|
||||
const taskMap = new Map(tasks.map((t) => [t.id, t]));
|
||||
const now = Date.now();
|
||||
const due: DueReminder[] = [];
|
||||
|
||||
for (const r of pending) {
|
||||
const task = taskMap.get(r.taskId);
|
||||
if (!task?.dueDate || task.isCompleted) continue;
|
||||
|
||||
const triggerAt = new Date(task.dueDate).getTime() - r.minutesBefore * 60_000;
|
||||
if (triggerAt <= now) {
|
||||
due.push({
|
||||
id: r.id,
|
||||
title: task.title || 'Aufgabe fällig',
|
||||
body: r.minutesBefore > 0 ? `In ${formatMinutes(r.minutesBefore)}` : 'Jetzt fällig',
|
||||
tag: `todo-${r.id}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return due;
|
||||
},
|
||||
|
||||
async markSent(reminderId: string): Promise<void> {
|
||||
await db.table('reminders').update(reminderId, {
|
||||
status: 'sent',
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
function formatMinutes(minutes: number): string {
|
||||
if (minutes < 60) return `${minutes} Minuten`;
|
||||
if (minutes === 60) return '1 Stunde';
|
||||
if (minutes < 1440) return `${Math.round(minutes / 60)} Stunden`;
|
||||
return `${Math.round(minutes / 1440)} Tag(e)`;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue