feat(tasks): due dates with optional time; alarms at the right moment
Due dates can carry a time (YYYY-MM-DDTHH:MM). Alarms fire at that time,
or 09:00 for all-day tasks, minus the user's lead time, in TZ (default
Europe/Berlin, tzdata embedded). A new due date or reopening a task
re-arms the alarm. Recurrence keeps the time; Markdown export/import
carry it. The UI shows relative labels ("Heute, 14:30"), marks overdue
tasks to the minute and groups by local day instead of the UTC date.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
300fd6a7be
commit
344f5e4ab2
21 files changed
+391
-50
No files matched your search
@@ -562,7 +562,10 @@ export const HelpView: React.FC<HelpViewProps> = ({ onBack }) => {
|
||||
<strong className="text-main">Wiederkehrende Aufgaben:</strong> Im Detail-Fenster eine Wiederholung wählen (täglich bis jährlich). Beim Abhaken entsteht automatisch die nächste Aufgabe mit fortgeschriebenem Datum; erkennbar am 🔁-Symbol.
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-main">Automatische Alarme:</strong> Ist ein Fälligkeitsdatum erreicht, kann MindSift dich automatisch per <strong className="text-main">E-Mail</strong>, <strong className="text-main">Discord-Webhook</strong> oder individuellem HTTP-Webhook benachrichtigen. Konfigurierbar in deinen Benutzereinstellungen.
|
||||
<strong className="text-main">Fälligkeit mit Uhrzeit:</strong> Im Detail-Fenster neben dem Datum optional eine Uhrzeit setzen; „✕ Uhrzeit“ macht die Aufgabe wieder ganztägig. Angezeigt wird z.B. „Heute, 14:30“, Überfälliges in Rot.
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-main">Automatische Alarme:</strong> Zur Fälligkeit (ganztägige Aufgaben um 9:00, abzüglich deiner Vorlaufzeit) benachrichtigt MindSift dich per <strong className="text-main">E-Mail</strong>, <strong className="text-main">Discord-Webhook</strong> oder individuellem HTTP-Webhook. Verschiebst du die Fälligkeit oder öffnest die Aufgabe wieder, kommt der Alarm erneut. Konfigurierbar in deinen Benutzereinstellungen.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -24,6 +24,7 @@ import { UserAvatar } from '../../components/UserAvatar';
|
||||
import { UserAutocomplete } from '../UserAutocomplete';
|
||||
import { triggerHaptic } from '../../lib/api';
|
||||
import { MarkdownViewer } from '../../lib/markdown';
|
||||
import { addDays, joinDue, splitDue } from '../../lib/due';
|
||||
|
||||
interface TaskDetailModalProps {
|
||||
task: Thought | null;
|
||||
@@ -165,15 +166,15 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const dueParts = splitDue(task.dueDate);
|
||||
|
||||
const handleQuickDueDate = (daysOffset: number | null) => {
|
||||
if (daysOffset === null) {
|
||||
onUpdateDueDate(task, '');
|
||||
return;
|
||||
}
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() + daysOffset);
|
||||
const dateStr = d.toISOString().split('T')[0];
|
||||
onUpdateDueDate(task, dateStr);
|
||||
// Keep a chosen time when only the day changes
|
||||
onUpdateDueDate(task, joinDue(addDays(daysOffset), splitDue(task.dueDate).time));
|
||||
};
|
||||
|
||||
const currentColumn = activeColumns.find((c) => c.id === task.kanbanStatus) ||
|
||||
@@ -469,21 +470,32 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-semibold text-main flex items-center gap-1">
|
||||
<Calendar className="w-3.5 h-3.5 text-accent" />
|
||||
<span>Fälligkeitsdatum:</span>
|
||||
<span>Fällig:</span>
|
||||
</label>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<input
|
||||
type="date"
|
||||
value={task.dueDate || ''}
|
||||
onChange={(e) => onUpdateDueDate(task, e.target.value)}
|
||||
className="flex-1 h-8 input-field px-2 text-xs bg-panel"
|
||||
aria-label="Fälligkeitsdatum"
|
||||
value={dueParts.date}
|
||||
onChange={(e) => onUpdateDueDate(task, joinDue(e.target.value, dueParts.time))}
|
||||
className="flex-1 min-w-0 h-11 sm:h-8 input-field px-2 text-base sm:text-xs bg-panel"
|
||||
/>
|
||||
<input
|
||||
type="time"
|
||||
aria-label="Uhrzeit (optional)"
|
||||
title="Uhrzeit (optional) – ohne Uhrzeit gilt die Aufgabe den ganzen Tag"
|
||||
value={dueParts.time}
|
||||
disabled={!dueParts.date}
|
||||
onChange={(e) => onUpdateDueDate(task, joinDue(dueParts.date, e.target.value))}
|
||||
className="w-[7.5rem] shrink-0 h-11 sm:h-8 input-field px-2 text-base sm:text-xs bg-panel disabled:opacity-50"
|
||||
/>
|
||||
{task.dueDate && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onUpdateDueDate(task, '')}
|
||||
className="btn-action h-8 px-2 text-xs text-muted hover:text-status-err"
|
||||
title="Datum entfernen"
|
||||
className="btn-action h-11 w-11 sm:h-8 sm:w-8 flex items-center justify-center text-muted hover:text-status-err shrink-0"
|
||||
title="Fälligkeit entfernen"
|
||||
aria-label="Fälligkeit entfernen"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
@@ -511,6 +523,17 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
|
||||
>
|
||||
+1 Woche
|
||||
</button>
|
||||
{dueParts.time && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onUpdateDueDate(task, dueParts.date)}
|
||||
className="px-1.5 py-0.5 bg-canvas border border-border rounded-btn text-muted hover:text-main inline-flex items-center gap-1"
|
||||
title="Uhrzeit entfernen – die Aufgabe gilt dann den ganzen Tag"
|
||||
>
|
||||
<X className="w-2.5 h-2.5" />
|
||||
Uhrzeit
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{onUpdateRecurrence && (
|
||||
<div className="pt-1">
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
Repeat,
|
||||
} from 'lucide-react';
|
||||
import { TaskList, Thought, SubTask, Team, User, TaskListShare, TaskListMember, KanbanColumn, PriorityLevel, Recurrence, RECURRENCE_LABELS } from '../../lib/types';
|
||||
import { formatDue, isDueToday, isOverdue, localDay, splitDue } from '../../lib/due';
|
||||
import { api, triggerHaptic } from '../../lib/api';
|
||||
import { UnifiedShareModal } from '../../components/UnifiedShareModal';
|
||||
import { TaskDetailModal } from './TaskDetailModal';
|
||||
@@ -416,10 +417,11 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
if (high.length > 0) groups.push({ key: 'p:high', title: '⚠️ Wichtig', count: high.length, tasks: high });
|
||||
if (normal.length > 0) groups.push({ key: 'p:normal', title: 'Normal', count: normal.length, tasks: normal });
|
||||
} else if (groupBy === 'due') {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const overdue = filteredTasks.filter((t) => t.dueDate && t.dueDate < today);
|
||||
const dueToday = filteredTasks.filter((t) => t.dueDate === today);
|
||||
const upcoming = filteredTasks.filter((t) => t.dueDate && t.dueDate > today);
|
||||
const now = new Date();
|
||||
const today = localDay(now);
|
||||
const overdue = filteredTasks.filter((t) => isOverdue(t.dueDate, now));
|
||||
const dueToday = filteredTasks.filter((t) => isDueToday(t.dueDate, now) && !isOverdue(t.dueDate, now));
|
||||
const upcoming = filteredTasks.filter((t) => t.dueDate && splitDue(t.dueDate).date > today);
|
||||
const noDue = filteredTasks.filter((t) => !t.dueDate);
|
||||
if (overdue.length > 0) groups.push({ key: 'due:overdue', title: '⚠️ Überfällig', count: overdue.length, tasks: overdue });
|
||||
if (dueToday.length > 0) groups.push({ key: 'due:today', title: '📅 Heute fällig', count: dueToday.length, tasks: dueToday });
|
||||
@@ -1311,9 +1313,12 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
|
||||
{/* Due Date */}
|
||||
{task.dueDate && (
|
||||
<span className="inline-flex items-center gap-1 text-[10px] text-muted">
|
||||
<span
|
||||
className={`inline-flex items-center gap-1 text-[10px] ${!task.isCompleted && isOverdue(task.dueDate) ? 'text-status-err font-semibold' : 'text-muted'}`}
|
||||
title={`Fällig: ${task.dueDate.replace('T', ' ')}`}
|
||||
>
|
||||
<Calendar className="w-2.5 h-2.5 text-accent" />
|
||||
<span>{task.dueDate}</span>
|
||||
<span>{formatDue(task.dueDate)}</span>
|
||||
</span>
|
||||
)}
|
||||
{task.recurrence && (
|
||||
@@ -2335,9 +2340,12 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
</span>
|
||||
)}
|
||||
{task.dueDate && (
|
||||
<span className="inline-flex items-center gap-0.5 text-[9px] text-muted">
|
||||
<span
|
||||
className={`inline-flex items-center gap-0.5 text-[9px] ${!task.isCompleted && isOverdue(task.dueDate) ? 'text-status-err font-semibold' : 'text-muted'}`}
|
||||
title={`Fällig: ${task.dueDate.replace('T', ' ')}`}
|
||||
>
|
||||
<Calendar className="w-2.5 h-2.5" />
|
||||
<span>{task.dueDate}</span>
|
||||
<span>{formatDue(task.dueDate)}</span>
|
||||
</span>
|
||||
)}
|
||||
{task.recurrence && (
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
MoreHorizontal,
|
||||
} from 'lucide-react';
|
||||
import { SharedList, Thought } from '../../lib/types';
|
||||
import { addDays, formatDue } from '../../lib/due';
|
||||
import { api, triggerHaptic } from '../../lib/api';
|
||||
|
||||
interface TriageDeckProps {
|
||||
@@ -223,10 +224,7 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
|
||||
const [cards, setCards] = useState<Thought[]>(deck);
|
||||
const [lastTriaged, setLastTriaged] = useState<Thought | null>(null);
|
||||
const [showDateModal, setShowDateModal] = useState(false);
|
||||
const [taskDueDate, setTaskDueDate] = useState(() => {
|
||||
const tomorrow = new Date(Date.now() + 86400000);
|
||||
return tomorrow.toISOString().slice(0, 10);
|
||||
});
|
||||
const [taskDueDate, setTaskDueDate] = useState(() => addDays(1));
|
||||
const [showListSelector, setShowListSelector] = useState(false);
|
||||
|
||||
// Long-Press Action Sheet state (for Triage Card or Queue Item)
|
||||
@@ -666,7 +664,7 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
|
||||
|
||||
{activeCard.dueDate && (
|
||||
<span className="px-2 py-0.5 bg-canvas text-muted border border-border rounded-btn text-xs">
|
||||
📅 {activeCard.dueDate}
|
||||
📅 {formatDue(activeCard.dueDate)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user