215 lines
11 KiB
TypeScript
215 lines
11 KiB
TypeScript
import React, { useState } from 'react';
|
|
|
|
interface RecurrenceModalProps {
|
|
task: any;
|
|
onClose: () => void;
|
|
onSave: (taskId: string, recurrence: any) => Promise<void>;
|
|
language?: string;
|
|
}
|
|
|
|
const WEEKDAYS_EN = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
const WEEKDAYS_DE = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'];
|
|
|
|
export default function RecurrenceModal({ task, onClose, onSave, language = 'en' }: RecurrenceModalProps) {
|
|
const [isRecurring, setIsRecurring] = useState(task.isRecurring || false);
|
|
const [interval, setInterval] = useState(task.recurrenceInterval || 1);
|
|
const [unit, setUnit] = useState(task.recurrenceUnit || 'weeks');
|
|
const [time, setTime] = useState(task.recurrenceTime || task.startTime || '09:00');
|
|
const [endDate, setEndDate] = useState(task.recurrenceEndDate ? new Date(task.recurrenceEndDate).toISOString().split('T')[0] : '');
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
|
|
// Day-of-week for weekly recurrence (0=Sun, 1=Mon, etc.)
|
|
const taskDow = task.scheduledDate ? new Date(task.scheduledDate).getDay() : (task.dayOfWeek ?? new Date().getDay());
|
|
const [selectedDays, setSelectedDays] = useState<number[]>(task.recurrenceDays?.length ? task.recurrenceDays : [taskDow]);
|
|
|
|
const weekdays = language === 'de' ? WEEKDAYS_DE : WEEKDAYS_EN;
|
|
const isDE = language === 'de';
|
|
|
|
// Helper to format date as DD.MM.YYYY
|
|
const formatDate = (dateStr: string) => {
|
|
if (!dateStr) return '';
|
|
const [y, m, d] = dateStr.split('-');
|
|
return `${d}.${m}.${y}`;
|
|
};
|
|
|
|
// Helper to parse DD.MM.YYYY back to YYYY-MM-DD
|
|
const parseDateInput = (input: string) => {
|
|
const match = input.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/);
|
|
if (match) {
|
|
const d = match[1].padStart(2, '0');
|
|
const m = match[2].padStart(2, '0');
|
|
const y = match[3];
|
|
return `${y}-${m}-${d}`;
|
|
}
|
|
return input; // fallback to raw
|
|
};
|
|
|
|
const toggleDay = (day: number) => {
|
|
setSelectedDays(prev => {
|
|
if (prev.includes(day)) {
|
|
return prev.length > 1 ? prev.filter(d => d !== day) : prev; // keep at least one
|
|
}
|
|
return [...prev, day].sort();
|
|
});
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
setIsSaving(true);
|
|
try {
|
|
const parsedEndDate = parseDateInput(endDate);
|
|
await onSave(task.id, {
|
|
isRecurring,
|
|
recurrenceInterval: isRecurring ? interval : null,
|
|
recurrenceUnit: isRecurring ? unit : null,
|
|
recurrenceTime: isRecurring ? time : null,
|
|
recurrenceEndDate: isRecurring && parsedEndDate ? new Date(parsedEndDate) : null,
|
|
recurrenceDays: isRecurring && unit === 'weeks' ? selectedDays : null,
|
|
});
|
|
onClose();
|
|
} catch (error) {
|
|
console.error('Failed to save recurrence', error);
|
|
setIsSaving(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="weekly-modal-overlay" onClick={onClose}>
|
|
<div className="weekly-modal-content" onClick={e => e.stopPropagation()} style={{ maxWidth: '400px' }}>
|
|
<h3 style={{ marginBottom: '1.5rem' }}>{isDE ? 'Wiederkehrende Aufgabe' : 'Recurring Task'}</h3>
|
|
|
|
<div style={{ marginBottom: '1.5rem' }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '1rem' }}>
|
|
<input
|
|
type="checkbox"
|
|
id="isRecurring"
|
|
checked={isRecurring}
|
|
onChange={e => setIsRecurring(e.target.checked)}
|
|
style={{ width: '18px', height: '18px', marginRight: '10px' }}
|
|
/>
|
|
<label htmlFor="isRecurring" style={{ fontSize: '1rem', fontWeight: 500 }}>{isDE ? 'Wiederholung aktivieren' : 'Enable Recurrence'}</label>
|
|
</div>
|
|
|
|
{isRecurring && (
|
|
<div style={{ paddingLeft: '28px', display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
|
<div>
|
|
<label style={{ display: 'block', marginBottom: '4px', fontSize: '0.9rem', color: '#666' }}>{isDE ? 'Wiederholen alle' : 'Repeat every'}</label>
|
|
<div style={{ display: 'flex', gap: '8px' }}>
|
|
<input
|
|
type="number"
|
|
min="1"
|
|
value={interval}
|
|
onChange={e => setInterval(parseInt(e.target.value) || 1)}
|
|
style={{ width: '60px', padding: '6px', borderRadius: '4px', border: '1px solid #ddd' }}
|
|
/>
|
|
<select
|
|
value={unit}
|
|
onChange={e => setUnit(e.target.value)}
|
|
style={{ flex: 1, padding: '6px', borderRadius: '4px', border: '1px solid #ddd' }}
|
|
>
|
|
<option value="days">{isDE ? 'Tage' : 'Days'}</option>
|
|
<option value="weeks">{isDE ? 'Wochen' : 'Weeks'}</option>
|
|
<option value="months">{isDE ? 'Monate' : 'Months'}</option>
|
|
<option value="years">{isDE ? 'Jahre' : 'Years'}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Weekday selector for weekly recurrence */}
|
|
{unit === 'weeks' && (
|
|
<div>
|
|
<label style={{ display: 'block', marginBottom: '6px', fontSize: '0.9rem', color: '#666' }}>{isDE ? 'An diesen Tagen' : 'On these days'}</label>
|
|
<div style={{ display: 'flex', gap: '4px' }}>
|
|
{weekdays.map((dayName, i) => (
|
|
<button
|
|
key={i}
|
|
type="button"
|
|
onClick={() => toggleDay(i)}
|
|
style={{
|
|
flex: 1,
|
|
padding: '6px 2px',
|
|
borderRadius: '6px',
|
|
border: 'none',
|
|
cursor: 'pointer',
|
|
fontSize: '0.8rem',
|
|
fontWeight: selectedDays.includes(i) ? 700 : 400,
|
|
background: selectedDays.includes(i) ? '#3b82f6' : '#f3f4f6',
|
|
color: selectedDays.includes(i) ? '#fff' : '#666',
|
|
transition: 'all 0.15s',
|
|
}}
|
|
>
|
|
{dayName}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div>
|
|
<label style={{ display: 'block', marginBottom: '4px', fontSize: '0.9rem', color: '#666' }}>{isDE ? 'Uhrzeit' : 'Time'}</label>
|
|
<input
|
|
type="time"
|
|
value={time}
|
|
onChange={e => setTime(e.target.value)}
|
|
style={{ width: '100%', padding: '6px', borderRadius: '4px', border: '1px solid #ddd' }}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label style={{ display: 'block', marginBottom: '4px', fontSize: '0.9rem', color: '#666' }}>{isDE ? 'Enddatum (Optional, TT.MM.JJJJ)' : 'End Date (Optional, DD.MM.YYYY)'}</label>
|
|
<input
|
|
type="text"
|
|
placeholder="DD.MM.YYYY"
|
|
value={endDate.includes('-') ? formatDate(endDate) : endDate}
|
|
onChange={(e) => {
|
|
const input = e.target.value;
|
|
// If user is deleting (new length < old length), just let them delete
|
|
if (input.length < endDate.length) {
|
|
setEndDate(input);
|
|
return;
|
|
}
|
|
|
|
let val = input.replace(/\D/g, ''); // Keep only digits
|
|
if (val.length > 8) val = val.slice(0, 8);
|
|
|
|
let formatted = val;
|
|
if (val.length > 2 && val.length <= 4) {
|
|
formatted = val.slice(0, 2) + '.' + val.slice(2);
|
|
} else if (val.length > 4) {
|
|
formatted = val.slice(0, 2) + '.' + val.slice(2, 4) + '.' + val.slice(4);
|
|
}
|
|
|
|
// If they typed a dot manually at the right position, don't interfere too much
|
|
if (input.endsWith('.') && (input.length === 3 || input.length === 6)) {
|
|
setEndDate(input);
|
|
} else {
|
|
setEndDate(formatted);
|
|
}
|
|
}}
|
|
style={{ width: '100%', padding: '6px', borderRadius: '4px', border: '1px solid #ddd' }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="weekly-modal-actions">
|
|
<button
|
|
className="weekly-btn weekly-btn-primary"
|
|
onClick={handleSave}
|
|
disabled={isSaving}
|
|
>
|
|
{isSaving ? (isDE ? 'Speichere...' : 'Saving...') : (isDE ? 'Speichern' : 'Save')}
|
|
</button>
|
|
<button
|
|
className="weekly-btn weekly-btn-secondary"
|
|
onClick={onClose}
|
|
disabled={isSaving}
|
|
>
|
|
{isDE ? 'Abbrechen' : 'Cancel'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|