tududi/frontend/components/Task/TaskForm/TaskRecurrenceSection.tsx

559 lines
24 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState } from 'react';
import { RecurrenceType } from '../../../entities/Task';
import { useTranslation } from 'react-i18next';
import RecurrenceSelectDropdown from '../../Shared/RecurrenceSelectDropdown';
import NumberSelectDropdown from '../../Shared/NumberSelectDropdown';
import ToggleSwitch from '../../Shared/ToggleSwitch';
import DatePicker from '../../Shared/DatePicker';
import WeekdaySelector from './WeekdaySelector';
interface TaskRecurrenceSectionProps {
recurrenceType: RecurrenceType;
recurrenceInterval: number;
recurrenceEndDate?: string;
recurrenceWeekday?: number;
recurrenceWeekdays?: number[];
recurrenceMonthDay?: number;
recurrenceWeekOfMonth?: number;
completionBased: boolean;
onChange: (field: string, value: any) => void;
disabled?: boolean;
isChildTask?: boolean;
parentTaskLoading?: boolean;
onEditParent?: () => void;
onParentRecurrenceChange?: (field: string, value: any) => void;
}
const TaskRecurrenceSection: React.FC<TaskRecurrenceSectionProps> = ({
recurrenceType,
recurrenceInterval,
recurrenceEndDate,
recurrenceWeekday,
recurrenceWeekdays,
recurrenceMonthDay,
recurrenceWeekOfMonth,
completionBased,
onChange,
disabled = false,
isChildTask = false,
parentTaskLoading = false,
onEditParent, // eslint-disable-line @typescript-eslint/no-unused-vars
onParentRecurrenceChange,
}) => {
// Helper function to convert ISO date string to YYYY-MM-DD format for DatePicker
const formatDateForPicker = (dateString?: string) => {
if (!dateString) return '';
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) return '';
return date.toISOString().split('T')[0]; // Returns YYYY-MM-DD
} catch {
return dateString; // Return as-is if it's already in the correct format
}
};
const { t } = useTranslation();
const [editingParentRecurrence, setEditingParentRecurrence] =
useState(false);
const weekdays = [
{ value: 0, label: t('weekdays.sunday', 'Sunday') },
{ value: 1, label: t('weekdays.monday', 'Monday') },
{ value: 2, label: t('weekdays.tuesday', 'Tuesday') },
{ value: 3, label: t('weekdays.wednesday', 'Wednesday') },
{ value: 4, label: t('weekdays.thursday', 'Thursday') },
{ value: 5, label: t('weekdays.friday', 'Friday') },
{ value: 6, label: t('weekdays.saturday', 'Saturday') },
];
const weekOfMonthOptions = [
{ value: 1, label: t('recurrence.firstWeek', 'First') },
{ value: 2, label: t('recurrence.secondWeek', 'Second') },
{ value: 3, label: t('recurrence.thirdWeek', 'Third') },
{ value: 4, label: t('recurrence.fourthWeek', 'Fourth') },
{ value: 5, label: t('recurrence.lastWeek', 'Last') },
];
const recurrenceTypeOptions = [
{ value: 'none', label: t('recurrence.none', 'No repeat') },
{ value: 'daily', label: t('recurrence.daily', 'Daily') },
{ value: 'weekly', label: t('recurrence.weekly', 'Weekly') },
{ value: 'monthly', label: t('recurrence.monthly', 'Monthly') },
{
value: 'monthly_weekday',
label: t('recurrence.monthlyWeekday', 'Monthly on weekday'),
},
{
value: 'monthly_last_day',
label: t('recurrence.monthlyLastDay', 'Monthly on last day'),
},
];
const renderRecurrenceTypeSelect = (
customOnChange?: (field: string, value: any) => void,
isDisabled?: boolean
) => (
<div className="mb-4">
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-2">
{t('forms.task.labels.recurrenceType', 'Repeat')}
</label>
<RecurrenceSelectDropdown
value={recurrenceType}
onChange={(value) =>
(customOnChange || onChange)(
'recurrence_type',
value as RecurrenceType
)
}
options={recurrenceTypeOptions}
disabled={isDisabled}
/>
</div>
);
const renderIntervalInput = (
customOnChange?: (field: string, value: any) => void,
isDisabled?: boolean
) => {
// Determine max value based on recurrence type
const getMaxValue = () => {
if (recurrenceType === 'daily') return 30;
if (recurrenceType === 'weekly') return 52; // Max 52 weeks (1 year)
if (
recurrenceType === 'monthly' ||
recurrenceType === 'monthly_weekday' ||
recurrenceType === 'monthly_last_day'
)
return 24; // Max 24 months (2 years)
return 99;
};
return (
<div className="mb-4">
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-2">
{t('forms.task.labels.recurrenceInterval', 'Every')}
</label>
<div className="flex items-center space-x-2">
<div className="w-20">
<NumberSelectDropdown
value={recurrenceInterval || 1}
onChange={(value) =>
(customOnChange || onChange)(
'recurrence_interval',
value
)
}
min={1}
max={getMaxValue()}
disabled={isDisabled}
/>
</div>
<span className="text-sm text-gray-600 dark:text-gray-400">
{recurrenceType === 'daily' &&
t('recurrence.days', 'days')}
{recurrenceType === 'weekly' &&
t('recurrence.weeks', 'weeks')}
{(recurrenceType === 'monthly' ||
recurrenceType === 'monthly_weekday' ||
recurrenceType === 'monthly_last_day') &&
t('recurrence.months', 'months')}
</span>
</div>
</div>
);
};
const renderWeekdaySelect = (
customOnChange?: (field: string, value: any) => void,
isDisabled?: boolean
) => {
return (
<div className="mb-4">
<WeekdaySelector
selectedDays={recurrenceWeekdays || []}
onChange={(days) =>
(customOnChange || onChange)(
'recurrence_weekdays',
days
)
}
disabled={isDisabled}
/>
</div>
);
};
const renderMonthDayInput = (
customOnChange?: (field: string, value: any) => void,
isDisabled?: boolean
) => (
<div className="mb-4">
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-2">
{t('forms.task.labels.monthDay', 'Day of month')}
</label>
<NumberSelectDropdown
value={recurrenceMonthDay || 1}
onChange={(value) =>
(customOnChange || onChange)('recurrence_month_day', value)
}
min={1}
max={31}
placeholder={t(
'recurrence.monthDayPlaceholder',
'Select day of month'
)}
disabled={isDisabled}
/>
</div>
);
const renderMonthlyWeekdayInputs = () => (
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-2">
{t('forms.task.labels.weekOfMonth', 'Week of month')}
</label>
<RecurrenceSelectDropdown
value={recurrenceWeekOfMonth || 1}
onChange={(value) =>
onChange(
'recurrence_week_of_month',
parseInt(value as string)
)
}
options={weekOfMonthOptions}
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-2">
{t('forms.task.labels.weekday', 'Weekday')}
</label>
<RecurrenceSelectDropdown
value={recurrenceWeekday ?? 1}
onChange={(value) =>
onChange(
'recurrence_weekday',
parseInt(value as string)
)
}
options={weekdays}
/>
</div>
</div>
);
const renderEndDateInput = (
customOnChange?: (field: string, value: any) => void,
isDisabled?: boolean
) => (
<div className="mb-4">
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-2">
{t(
'forms.task.labels.recurrenceEndDate',
'End date (optional)'
)}
</label>
<DatePicker
value={formatDateForPicker(recurrenceEndDate)}
onChange={(value) =>
(customOnChange || onChange)(
'recurrence_end_date',
value || null
)
}
placeholder={t(
'forms.task.endDatePlaceholder',
'Select end date'
)}
disabled={isDisabled}
/>
</div>
);
const renderCompletionBasedToggle = () => (
<div className="mb-4">
<ToggleSwitch
checked={completionBased}
onChange={(checked) => onChange('completion_based', checked)}
label={t(
'forms.task.labels.completionBased',
'Repeat after completion'
)}
description={t(
'forms.task.completionBasedHelp',
'If checked, the next task will be created based on completion date instead of due date'
)}
/>
</div>
);
// Show message for child tasks
if (isChildTask && parentTaskLoading) {
return (
<div className="pb-3 border-t border-gray-200 dark:border-gray-700 pt-4 mt-4">
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-4">
{t('forms.task.recurrenceSettings', 'Recurrence Settings')}
</h3>
<div className="text-sm text-gray-600 dark:text-gray-400">
Loading parent task recurrence settings...
</div>
</div>
);
}
if (isChildTask) {
return (
<div className="pb-3">
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-4">
{t('forms.task.recurrenceSettings', 'Recurrence Settings')}
</h3>
<div className="bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-800 rounded-md p-3 mb-4">
<div className="text-sm text-blue-800 dark:text-blue-200">
<strong>Recurring Task Instance</strong>
<p className="mt-1">
This task was generated from a recurring task. The
recurrence settings shown below are inherited from
the original task and cannot be edited here.
</p>
{onParentRecurrenceChange && (
<button
type="button"
onClick={() =>
setEditingParentRecurrence(
!editingParentRecurrence
)
}
className={`mt-2 inline-flex items-center px-3 py-1 border text-xs font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ${
editingParentRecurrence
? 'border-red-300 dark:border-red-600 text-red-700 dark:text-red-300 bg-red-50 dark:bg-red-900/50 hover:bg-red-100 dark:hover:bg-red-800/50'
: 'border-blue-300 dark:border-blue-600 text-blue-700 dark:text-blue-300 bg-white dark:bg-blue-900/50 hover:bg-blue-50 dark:hover:bg-blue-800/50'
}`}
>
{editingParentRecurrence
? 'Cancel Edit'
: 'Edit Parent Recurrence'}
</button>
)}
</div>
</div>
<div
className={
editingParentRecurrence
? ''
: 'opacity-60 pointer-events-none'
}
>
{editingParentRecurrence && (
<div className="mb-4 p-2 bg-yellow-50 dark:bg-yellow-900/30 border border-yellow-200 dark:border-yellow-800 rounded-md">
<div className="text-xs text-yellow-800 dark:text-yellow-200">
You are editing the parent task&apos;s
recurrence settings. Changes will affect all
future instances of this recurring task.
</div>
</div>
)}
{recurrenceType === 'none' ? (
renderRecurrenceTypeSelect(
editingParentRecurrence
? onParentRecurrenceChange
: undefined,
!editingParentRecurrence
)
) : (
<>
{renderRecurrenceTypeSelect(
editingParentRecurrence
? onParentRecurrenceChange
: undefined,
!editingParentRecurrence
)}
{renderIntervalInput(
editingParentRecurrence
? onParentRecurrenceChange
: undefined,
!editingParentRecurrence
)}
{(recurrenceType === 'weekly' ||
recurrenceType === 'monthly_weekday') &&
renderWeekdaySelect(
editingParentRecurrence
? onParentRecurrenceChange
: undefined,
!editingParentRecurrence
)}
{recurrenceType === 'monthly' &&
renderMonthDayInput(
editingParentRecurrence
? onParentRecurrenceChange
: undefined,
!editingParentRecurrence
)}
{recurrenceType === 'monthly_weekday' &&
renderMonthlyWeekdayInputs()}
{renderEndDateInput(
editingParentRecurrence
? onParentRecurrenceChange
: undefined,
!editingParentRecurrence
)}
{renderCompletionBasedToggle()}
</>
)}
</div>
</div>
);
}
if (recurrenceType === 'none') {
return <div className="pb-3">{renderRecurrenceTypeSelect()}</div>;
}
return (
<div className="pb-3">
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-4">
{t('forms.task.recurrenceSettings', 'Recurrence Settings')}
</h3>
{/* Main recurrence settings in one row */}
<div
className={`grid grid-cols-1 gap-4 mb-4 ${recurrenceType === 'monthly' ? 'md:grid-cols-4' : 'md:grid-cols-3'}`}
>
<div>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-2">
{t('forms.task.labels.recurrenceType', 'Repeat')}
</label>
<RecurrenceSelectDropdown
value={recurrenceType}
onChange={(value) =>
onChange('recurrence_type', value as RecurrenceType)
}
options={recurrenceTypeOptions}
disabled={disabled}
/>
</div>
{(recurrenceType === 'daily' ||
recurrenceType === 'weekly' ||
recurrenceType === 'monthly' ||
recurrenceType === 'monthly_weekday' ||
recurrenceType === 'monthly_last_day') && (
<div>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-2">
{recurrenceType === 'monthly'
? t(
'forms.task.labels.recurrenceInterval',
'Every'
) +
' (' +
t('recurrence.months', 'months') +
')'
: t(
'forms.task.labels.recurrenceInterval',
'Every'
)}
</label>
{recurrenceType === 'monthly' ? (
<NumberSelectDropdown
value={recurrenceInterval || 1}
onChange={(value) =>
onChange('recurrence_interval', value)
}
min={1}
max={24}
disabled={disabled}
/>
) : (
<div className="flex items-center space-x-2">
<div className="w-20">
<NumberSelectDropdown
value={recurrenceInterval || 1}
onChange={(value) =>
onChange(
'recurrence_interval',
value
)
}
min={1}
max={
recurrenceType === 'daily'
? 30
: recurrenceType === 'weekly'
? 52
: recurrenceType ===
'monthly_weekday' ||
recurrenceType ===
'monthly_last_day'
? 24
: 99
}
disabled={disabled}
/>
</div>
<span className="text-sm text-gray-600 dark:text-gray-400">
{recurrenceType === 'daily' &&
t('recurrence.days', 'days')}
{recurrenceType === 'weekly' &&
t('recurrence.weeks', 'weeks')}
{(recurrenceType === 'monthly_weekday' ||
recurrenceType ===
'monthly_last_day') &&
t('recurrence.months', 'months')}
</span>
</div>
)}
</div>
)}
{recurrenceType === 'monthly' && (
<div>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-2">
{t('forms.task.labels.monthDay', 'Day of month')}
</label>
<NumberSelectDropdown
value={recurrenceMonthDay || 1}
onChange={(value) =>
onChange('recurrence_month_day', value)
}
min={1}
max={31}
placeholder={t(
'recurrence.monthDayPlaceholder',
'Select day of month'
)}
disabled={disabled}
/>
</div>
)}
<div>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-2">
{t(
'forms.task.labels.recurrenceEndDate',
'End date (optional)'
)}
</label>
<DatePicker
value={formatDateForPicker(recurrenceEndDate)}
onChange={(value) =>
onChange('recurrence_end_date', value || null)
}
placeholder={t(
'forms.task.endDatePlaceholder',
'Select end date'
)}
disabled={disabled}
/>
</div>
</div>
{/* Additional settings for specific recurrence types */}
{recurrenceType === 'weekly' && renderWeekdaySelect()}
{recurrenceType === 'monthly_weekday' &&
renderMonthlyWeekdayInputs()}
{renderCompletionBasedToggle()}
</div>
);
};
export default TaskRecurrenceSection;