My-Weekly-ToDo-List/src/components/RecurringTasksManager.tsx
mARTin eebc8eb9b6 feat: drag-to-provider sync, recurring task editing, resizable areas, search improvements
- Sync tasks to external provider when dragged to a synced Someday list (Google/Outlook/Synology)
- Add Synology support to POST /api/tasks/sync endpoint
- Edit recurring task series (title, interval, unit, time) all at once via new API
- Make Someday and all-day areas vertically resizable with drag handles (height persisted in cookies)
- Include Someday tasks in search results with list name display
- Instant calendar event appearance after creation (improved optimistic update + immediate cache refresh)
- Fix project color not updating on tasks when project color changes
- Add database backup script for Supabase LXC

v1.39.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 00:50:42 +01:00

231 lines
14 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-explicit-any */
import React, { useState } from 'react';
import { Repeat, X, Trash2, Pencil, Check } from 'lucide-react';
interface RecurringTasksManagerProps {
isOpen: boolean;
onClose: () => void;
tasks: any[];
onStopRecurring: (task: any) => void;
onSeriesUpdated?: () => void;
}
export default function RecurringTasksManager({ isOpen, onClose, tasks, onStopRecurring, onSeriesUpdated }: RecurringTasksManagerProps) {
const [editingSignature, setEditingSignature] = useState<string | null>(null);
const [editTitle, setEditTitle] = useState('');
const [editInterval, setEditInterval] = useState(1);
const [editUnit, setEditUnit] = useState('weeks');
const [editTime, setEditTime] = useState('');
const [isSaving, setIsSaving] = useState(false);
if (!isOpen) return null;
// Filter and group recurring tasks by series signature
const seriesGroups = new Map<string, { task: any; signature: string }>();
tasks.filter(t => t.isRecurring).forEach(t => {
if (t.recurrenceEndDate && new Date(t.recurrenceEndDate) < new Date()) {
return;
}
const signature = `${t.title}-${t.recurrenceInterval || 1}-${t.recurrenceUnit || 'weeks'}-${t.recurrenceTime || ''}`;
const existing = seriesGroups.get(signature);
if (!existing || (t.scheduledDate && new Date(t.scheduledDate) > new Date(existing.task.scheduledDate || 0))) {
seriesGroups.set(signature, { task: t, signature });
}
});
const groupedTasks = Array.from(seriesGroups.values());
const startEditing = (task: any, signature: string) => {
setEditingSignature(signature);
setEditTitle(task.title);
setEditInterval(task.recurrenceInterval || 1);
setEditUnit(task.recurrenceUnit || 'weeks');
setEditTime(task.recurrenceTime || '');
};
const cancelEditing = () => {
setEditingSignature(null);
};
const saveEditing = async (task: any) => {
setIsSaving(true);
try {
const res = await fetch('/api/tasks/recurring', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
oldTitle: task.title,
oldRecurrenceInterval: task.recurrenceInterval || 1,
oldRecurrenceUnit: task.recurrenceUnit || 'weeks',
oldRecurrenceTime: task.recurrenceTime || '',
newTitle: editTitle,
newRecurrenceInterval: editInterval,
newRecurrenceUnit: editUnit,
newRecurrenceTime: editTime || null,
}),
});
if (res.ok) {
setEditingSignature(null);
onSeriesUpdated?.();
} else {
const err = await res.json();
alert(err.error || 'Failed to update series');
}
} catch (e) {
console.error('Failed to update recurring series:', e);
} finally {
setIsSaving(false);
}
};
const unitLabel = (unit: string, interval: number) => {
if (unit === 'days') return interval === 1 ? 'Tag' : 'Tage';
if (unit === 'weeks') return interval === 1 ? 'Woche' : 'Wochen';
if (unit === 'months') return interval === 1 ? 'Monat' : 'Monate';
return interval === 1 ? 'Jahr' : 'Jahre';
};
return (
<div className="fixed inset-0 bg-black/50 z-[3000] flex items-center justify-center p-4 animate-fadeIn" onClick={onClose}>
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-2xl w-[600px] max-w-full overflow-hidden flex flex-col animate-scaleIn" onClick={e => e.stopPropagation()}>
<div className="p-5 flex items-center justify-between border-b border-gray-100 dark:border-gray-800">
<div className="flex items-center gap-3">
<div className="p-2 bg-teal-100 dark:bg-teal-900/30 rounded-lg text-teal-600">
<Repeat size={20} />
</div>
<h2 className="text-lg font-bold text-gray-800 dark:text-white">Wiederkehrende Aufgaben</h2>
</div>
<button onClick={onClose} className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full text-gray-400 transition-colors">
<X size={20} />
</button>
</div>
<div className="flex-1 overflow-y-auto p-6">
{groupedTasks.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<div className="w-16 h-16 bg-gray-50 dark:bg-gray-800 rounded-full flex items-center justify-center text-gray-300 mb-4">
<Repeat size={32} />
</div>
<p className="text-gray-500 dark:text-gray-400 max-w-xs">
Du hast momentan keine aktiven wiederkehrenden Aufgaben.
</p>
</div>
) : (
<div className="space-y-3">
{groupedTasks.map(({ task, signature }) => (
<div key={signature} className="p-4 bg-gray-50 dark:bg-gray-800/50 rounded-xl border border-gray-100 dark:border-gray-700/50 hover:border-teal-200 dark:hover:border-teal-900/50 transition-all">
{editingSignature === signature ? (
/* Edit mode */
<div className="space-y-3">
<input
type="text"
value={editTitle}
onChange={e => setEditTitle(e.target.value)}
className="w-full px-3 py-2 rounded-lg border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-sm font-semibold outline-none focus:border-teal-400"
placeholder="Aufgabentitel"
/>
<div className="flex items-center gap-2 flex-wrap">
<span className="text-xs text-gray-500">Alle</span>
<input
type="number"
min={1}
max={99}
value={editInterval}
onChange={e => setEditInterval(parseInt(e.target.value) || 1)}
className="w-14 px-2 py-1 rounded border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-xs text-center outline-none"
/>
<select
value={editUnit}
onChange={e => setEditUnit(e.target.value)}
className="px-2 py-1 rounded border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-xs outline-none"
>
<option value="days">Tage</option>
<option value="weeks">Wochen</option>
<option value="months">Monate</option>
<option value="years">Jahre</option>
</select>
<span className="text-xs text-gray-500">um</span>
<input
type="time"
value={editTime}
onChange={e => setEditTime(e.target.value)}
className="px-2 py-1 rounded border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-xs outline-none"
/>
</div>
<div className="flex justify-end gap-2">
<button
onClick={cancelEditing}
className="px-3 py-1.5 text-xs text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
>
Abbrechen
</button>
<button
onClick={() => saveEditing(task)}
disabled={isSaving || !editTitle.trim()}
className="flex items-center gap-1.5 px-3 py-1.5 bg-teal-500 hover:bg-teal-600 text-white rounded-lg text-xs font-bold transition-colors disabled:opacity-50"
>
<Check size={12} />
{isSaving ? 'Speichern...' : 'Alle aktualisieren'}
</button>
</div>
</div>
) : (
/* View mode */
<div className="flex items-center justify-between">
<div className="flex-1 min-w-0 pr-4">
<div className="font-semibold text-gray-900 dark:text-white truncate" title={task.title}>
{task.title}
</div>
<div className="flex items-center gap-2 mt-1">
<span className="text-xs px-2 py-0.5 bg-teal-50 dark:bg-teal-900/20 text-teal-600 dark:text-teal-400 rounded-md font-medium">
Alle {task.recurrenceInterval === 1 ? '' : `${task.recurrenceInterval} `}{unitLabel(task.recurrenceUnit || 'weeks', task.recurrenceInterval || 1)}
</span>
{task.recurrenceTime && (
<span className="text-[10px] text-gray-400">
um {task.recurrenceTime} Uhr
</span>
)}
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => startEditing(task, signature)}
className="flex items-center gap-1.5 px-3 py-2 bg-blue-50 hover:bg-blue-100 dark:bg-blue-900/10 dark:hover:bg-blue-900/20 text-blue-600 dark:text-blue-400 rounded-lg text-xs font-bold transition-colors"
>
<Pencil size={12} />
Bearbeiten
</button>
<button
onClick={() => {
if (confirm(`Serie "${task.title}" wirklich beenden?`)) {
onStopRecurring(task);
}
}}
className="flex items-center gap-1.5 px-3 py-2 bg-red-50 hover:bg-red-100 dark:bg-red-900/10 dark:hover:bg-red-900/20 text-red-600 dark:text-red-400 rounded-lg text-xs font-bold transition-colors"
>
<Trash2 size={14} />
Beenden
</button>
</div>
</div>
)}
</div>
))}
</div>
)}
</div>
<div className="p-3 bg-gray-50 dark:bg-gray-900 border-t border-gray-100 dark:border-gray-800 flex justify-center">
<p className="text-[10px] text-gray-400 font-medium">
Tipp: Du kannst Aufgaben jederzeit beim Bearbeiten wiederkehrend machen.
</p>
</div>
</div>
</div>
);
}