feat(tasks): repeating tasks (daily, weekly, monthly, yearly)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m28s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m28s
- recurrence on tasks, set in the detail dialog; 🔁 on rows and Kanban cards - Completing a repeating task (detail, list, Kanban drag to done, note checkbox) creates the next one: title, tags, priority, notes, list, assignees and rule copied, subtasks reset, first open column, due date one interval later (month end clamped); pushed live as TASK_CREATED - Each task spawns at most once (re-open + complete makes no duplicate) - Export/import keep the rule (Obsidian Tasks "🔁 every week", JSON field) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0e9fd1b3ad
commit
588c679c6a
16 files changed
+376
-19
No files matched your search
@@ -558,6 +558,9 @@ export const HelpView: React.FC<HelpViewProps> = ({ onBack }) => {
|
||||
<li>
|
||||
<strong className="text-main">Zuweisen:</strong> In geteilten Aufgabenlisten weist du eine Aufgabe einer oder mehreren Personen mit Zugriff zu (Namen tippen, Enter). Die Zugewiesenen erscheinen als kleine Profilbilder an der Aufgabe; wer den Zugriff auf die Liste verliert, wird automatisch entfernt.
|
||||
</li>
|
||||
<li>
|
||||
<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.
|
||||
</li>
|
||||
|
||||
@@ -17,8 +17,9 @@ import {
|
||||
Webhook,
|
||||
Lock,
|
||||
UserPlus,
|
||||
Repeat,
|
||||
} from 'lucide-react';
|
||||
import { Thought, SubTask, TaskList, TaskListMember, KanbanColumn, User } from '../../lib/types';
|
||||
import { Thought, SubTask, TaskList, TaskListMember, KanbanColumn, User, Recurrence } from '../../lib/types';
|
||||
import { UserAvatar } from '../../components/UserAvatar';
|
||||
import { UserAutocomplete } from '../UserAutocomplete';
|
||||
import { triggerHaptic } from '../../lib/api';
|
||||
@@ -34,6 +35,7 @@ interface TaskDetailModalProps {
|
||||
onUpdateStatus: (task: Thought, newStatus: string) => Promise<void>;
|
||||
onUpdatePriority: (task: Thought, newPriority: string) => Promise<void>;
|
||||
onUpdateDueDate: (task: Thought, newDueDate: string) => Promise<void>;
|
||||
onUpdateRecurrence?: (task: Thought, recurrence: Recurrence) => Promise<void>;
|
||||
onUpdateTaskList: (task: Thought, newListId: string) => Promise<void>;
|
||||
onUpdateNotes: (task: Thought, newNotes: string) => Promise<void>;
|
||||
onUpdateTags?: (task: Thought, newTags: string[]) => Promise<void>;
|
||||
@@ -58,6 +60,7 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
|
||||
onUpdateStatus,
|
||||
onUpdatePriority,
|
||||
onUpdateDueDate,
|
||||
onUpdateRecurrence,
|
||||
onUpdateTaskList,
|
||||
onUpdateNotes,
|
||||
onUpdateTags,
|
||||
@@ -509,6 +512,32 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
|
||||
+1 Woche
|
||||
</button>
|
||||
</div>
|
||||
{onUpdateRecurrence && (
|
||||
<div className="pt-1">
|
||||
<label className="flex items-center gap-1 text-[11px] text-muted mb-1">
|
||||
<Repeat className="w-3 h-3 text-accent" />
|
||||
<span>Wiederholung</span>
|
||||
</label>
|
||||
<select
|
||||
value={task.recurrence || ''}
|
||||
disabled={readOnly}
|
||||
onChange={(e) => onUpdateRecurrence(task, e.target.value as Recurrence)}
|
||||
className="w-full h-11 sm:h-8 input-field px-2 text-base sm:text-xs bg-panel"
|
||||
>
|
||||
<option value="">Keine Wiederholung</option>
|
||||
<option value="daily">🔁 Täglich</option>
|
||||
<option value="weekly">🔁 Wöchentlich</option>
|
||||
<option value="monthly">🔁 Monatlich</option>
|
||||
<option value="yearly">🔁 Jährlich</option>
|
||||
</select>
|
||||
{task.recurrence && (
|
||||
<p className="text-[10px] text-faint mt-1">
|
||||
Beim Erledigen entsteht automatisch die nächste Aufgabe, fällig{' '}
|
||||
{task.dueDate ? 'ein Intervall nach diesem Datum' : 'ein Intervall nach dem Erledigen'}.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Task List assignment */}
|
||||
|
||||
@@ -25,8 +25,9 @@ import {
|
||||
Folder,
|
||||
Filter,
|
||||
Webhook,
|
||||
Repeat,
|
||||
} from 'lucide-react';
|
||||
import { TaskList, Thought, SubTask, Team, User, TaskListShare, TaskListMember, KanbanColumn, PriorityLevel } from '../../lib/types';
|
||||
import { TaskList, Thought, SubTask, Team, User, TaskListShare, TaskListMember, KanbanColumn, PriorityLevel, Recurrence, RECURRENCE_LABELS } from '../../lib/types';
|
||||
import { api, triggerHaptic } from '../../lib/api';
|
||||
import { UnifiedShareModal } from '../../components/UnifiedShareModal';
|
||||
import { TaskDetailModal } from './TaskDetailModal';
|
||||
@@ -504,6 +505,15 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
await api.updateThought(task.id, { notes: newNotes });
|
||||
};
|
||||
|
||||
const handleUpdateTaskRecurrence = async (task: Thought, recurrence: Recurrence) => {
|
||||
setTasks((prev) => prev.map((t) => (t.id === task.id ? { ...t, recurrence } : t)));
|
||||
try {
|
||||
await api.updateThought(task.id, { recurrence });
|
||||
} catch {
|
||||
setTasks((prev) => prev.map((t) => (t.id === task.id ? { ...t, recurrence: task.recurrence } : t)));
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateTaskAssignees = async (task: Thought, assignees: string[]) => {
|
||||
triggerHaptic(10);
|
||||
setTasks((prev) => prev.map((t) => (t.id === task.id ? { ...t, assignees } : t)));
|
||||
@@ -1306,6 +1316,11 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
<span>{task.dueDate}</span>
|
||||
</span>
|
||||
)}
|
||||
{task.recurrence && (
|
||||
<span className="inline-flex items-center text-muted" title={`Wiederholt sich ${RECURRENCE_LABELS[task.recurrence]}`}>
|
||||
<Repeat className="w-3 h-3" />
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Hashtag badges */}
|
||||
{task.tags?.map((tag) => (
|
||||
@@ -2131,6 +2146,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
onUpdateStatus={handleSetKanbanStatus}
|
||||
onUpdatePriority={handleSetPriority}
|
||||
onUpdateDueDate={handleUpdateTaskDueDate}
|
||||
onUpdateRecurrence={handleUpdateTaskRecurrence}
|
||||
onUpdateTaskList={handleUpdateTaskTaskList}
|
||||
onUpdateNotes={handleUpdateTaskNotes}
|
||||
onUpdateTags={handleUpdateTaskTags}
|
||||
@@ -2324,6 +2340,11 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
<span>{task.dueDate}</span>
|
||||
</span>
|
||||
)}
|
||||
{task.recurrence && (
|
||||
<span className="inline-flex items-center text-muted" title={`Wiederholt sich ${RECURRENCE_LABELS[task.recurrence]}`}>
|
||||
<Repeat className="w-2.5 h-2.5" />
|
||||
</span>
|
||||
)}
|
||||
{task.tags?.map((tag) => (
|
||||
<button
|
||||
key={tag}
|
||||
|
||||
@@ -390,6 +390,7 @@ export const api = {
|
||||
subtasks?: SubTask[];
|
||||
tags?: string[];
|
||||
assignees?: string[];
|
||||
recurrence?: string;
|
||||
}
|
||||
): Promise<Thought> {
|
||||
return request<Thought>(`/api/v1/thoughts/${thoughtId}`, {
|
||||
|
||||
@@ -57,6 +57,16 @@ export interface SubTask {
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
// Repeat rule of a task: completing it creates the next one (due date one interval later)
|
||||
export type Recurrence = '' | 'daily' | 'weekly' | 'monthly' | 'yearly';
|
||||
|
||||
export const RECURRENCE_LABELS: Record<Exclude<Recurrence, ''>, string> = {
|
||||
daily: 'täglich',
|
||||
weekly: 'wöchentlich',
|
||||
monthly: 'monatlich',
|
||||
yearly: 'jährlich',
|
||||
};
|
||||
|
||||
export interface Thought {
|
||||
id: string;
|
||||
userId: string;
|
||||
@@ -76,6 +86,7 @@ export interface Thought {
|
||||
notes?: string;
|
||||
subtasks?: SubTask[];
|
||||
assignees?: string[]; // user IDs, only people with access to the task list
|
||||
recurrence?: Recurrence;
|
||||
alarmSent?: boolean;
|
||||
sortOrder?: number;
|
||||
createdAt: string;
|
||||
|
||||
Reference in new issue
Block a user