feat(recurring-tasks): enhance recurring task functionality and documentation

- Expanded schedule options for recurring tasks, including new intervals for every X days, weeks, and months.
- Added future task creation logic to ensure tasks are created within defined limits based on their schedule type.
- Updated user guide to reflect new scheduling options and future task creation details.
- Improved backend logic for recurring task creation, including batch processing and future limit calculations.
- Added environment configuration for enabling recurring jobs.
- Enhanced frontend localization for recurring task configuration labels.
This commit is contained in:
chamiakJ
2025-05-21 08:07:59 +05:30
parent a568ee808f
commit 84c7428fed
10 changed files with 255 additions and 129 deletions

View File

@@ -16,24 +16,45 @@ Recurring tasks are tasks that repeat automatically on a schedule you choose. Th
5. Save the task. It will now be created automatically based on your chosen schedule. 5. Save the task. It will now be created automatically based on your chosen schedule.
## Schedule Options ## Schedule Options
You can choose how often your task repeats. Here are the most common options: You can choose how often your task repeats. Here are the available options:
- **Daily:** The task is created every day. - **Daily:** The task is created every day.
- **Weekly:** The task is created once a week. You can pick the day (e.g., every Monday). - **Weekly:** The task is created once a week. You can pick one or more days (e.g., every Monday and Thursday).
- **Monthly:** The task is created once a month. You can pick the date (e.g., the 1st of every month). - **Monthly:** The task is created once a month. You have two options:
- **Weekdays:** The task is created every Monday to Friday. - **On a specific date:** Choose a date from 1 to 28 (limited to 28 to ensure consistency across all months)
- **Custom:** Set your own schedule, such as every 2 days, every 3 weeks, or only on certain days. - **On a specific day:** Choose a week (first, second, third, fourth, or last) and a day of the week
- **Every X Days:** The task is created every specified number of days (e.g., every 3 days)
- **Every X Weeks:** The task is created every specified number of weeks (e.g., every 2 weeks)
- **Every X Months:** The task is created every specified number of months (e.g., every 3 months)
### Examples ### Examples
- "Send team update" every Friday (weekly) - "Send team update" every Friday (weekly)
- "Submit expense report" on the 1st of each month (monthly) - "Submit expense report" on the 15th of each month (monthly, specific date)
- "Monthly team meeting" on the first Monday of each month (monthly, specific day)
- "Check backups" every day (daily) - "Check backups" every day (daily)
- "Review project status" every Monday and Thursday (custom) - "Review project status" every Monday and Thursday (weekly, multiple days)
- "Quarterly report" every 3 months (every X months)
## Future Task Creation
The system automatically creates tasks up to a certain point in the future to ensure timely scheduling:
- **Daily Tasks:** Created up to 7 days in advance
- **Weekly Tasks:** Created up to 2 weeks in advance
- **Monthly Tasks:** Created up to 2 months in advance
- **Every X Days/Weeks/Months:** Created up to 2 intervals in advance
This ensures that:
- You always have upcoming tasks visible in your schedule
- Tasks are created at appropriate intervals
- The system maintains a reasonable number of future tasks
## Tips ## Tips
- You can edit or stop a recurring task at any time. - You can edit or stop a recurring task at any time.
- Assign team members and labels to recurring tasks for better organization. - Assign team members and labels to recurring tasks for better organization.
- Check your task list regularly to see newly created recurring tasks. - Check your task list regularly to see newly created recurring tasks.
- For monthly tasks, dates are limited to 1-28 to ensure the task occurs on the same date every month.
- Tasks are created automatically within the future limit window - you don't need to manually create them.
- If you need to see tasks further in the future, they will be created automatically as the current tasks are completed.
## Need Help? ## Need Help?
If you have questions or need help setting up recurring tasks, contact your workspace admin or support team. If you have questions or need help setting up recurring tasks, contact your workspace admin or support team.

View File

@@ -17,6 +17,51 @@ The recurring tasks cron job automates the creation of tasks based on predefined
3. Checks if a task for the next occurrence already exists. 3. Checks if a task for the next occurrence already exists.
4. Creates a new task if it does not exist and the next occurrence is within the allowed future window. 4. Creates a new task if it does not exist and the next occurrence is within the allowed future window.
## Future Limit Logic
The system implements different future limits based on the schedule type to maintain an appropriate number of future tasks:
```typescript
const FUTURE_LIMITS = {
daily: moment.duration(7, 'days'),
weekly: moment.duration(2, 'weeks'),
monthly: moment.duration(2, 'months'),
every_x_days: (interval: number) => moment.duration(interval * 2, 'days'),
every_x_weeks: (interval: number) => moment.duration(interval * 2, 'weeks'),
every_x_months: (interval: number) => moment.duration(interval * 2, 'months')
};
```
### Implementation Details
- **Base Calculation:**
```typescript
const futureLimit = moment(template.last_checked_at || template.created_at)
.add(getFutureLimit(schedule.schedule_type, schedule.interval), 'days');
```
- **Task Creation Rules:**
1. Only create tasks if the next occurrence is before the future limit
2. Skip creation if a task already exists for that date
3. Update `last_checked_at` after processing
- **Benefits:**
- Prevents excessive task creation
- Maintains system performance
- Ensures timely task visibility
- Allows for schedule modifications
## Date Handling
- **Monthly Tasks:**
- Dates are limited to 1-28 to ensure consistency across all months
- This prevents issues with months having different numbers of days
- No special handling needed for February or months with 30/31 days
- **Weekly Tasks:**
- Supports multiple days of the week (0-6, where 0 is Sunday)
- Tasks are created for each selected day
- **Interval-based Tasks:**
- Every X days/weeks/months from the last task's end date
- Minimum interval is 1 day/week/month
- No maximum limit, but tasks are only created up to the future limit
## Database Interactions ## Database Interactions
- **Templates and Schedules:** - **Templates and Schedules:**
- Templates are stored in `task_recurring_templates`. - Templates are stored in `task_recurring_templates`.
@@ -27,6 +72,7 @@ The recurring tasks cron job automates the creation of tasks based on predefined
- Assigns team members and labels by calling appropriate functions/controllers. - Assigns team members and labels by calling appropriate functions/controllers.
- **State Tracking:** - **State Tracking:**
- Updates `last_checked_at` and `last_created_task_end_date` in the schedule after processing. - Updates `last_checked_at` and `last_created_task_end_date` in the schedule after processing.
- Maintains future limits based on schedule type.
## Task Creation Process ## Task Creation Process
1. **Fetch Templates:** Retrieve all templates and their associated schedules. 1. **Fetch Templates:** Retrieve all templates and their associated schedules.
@@ -41,10 +87,12 @@ The recurring tasks cron job automates the creation of tasks based on predefined
- **Cron Expression:** Modify the `TIME` constant in the code to change the schedule. - **Cron Expression:** Modify the `TIME` constant in the code to change the schedule.
- **Task Template Structure:** Extend the template or schedule interfaces to support additional fields. - **Task Template Structure:** Extend the template or schedule interfaces to support additional fields.
- **Task Creation Logic:** Customize the task creation process or add new assignment/labeling logic as needed. - **Task Creation Logic:** Customize the task creation process or add new assignment/labeling logic as needed.
- **Future Window:** Adjust the future limits by modifying the `FUTURE_LIMITS` configuration.
## Error Handling ## Error Handling
- Errors are logged using the `log_error` utility. - Errors are logged using the `log_error` utility.
- The job continues processing other templates even if one fails. - The job continues processing other templates even if one fails.
- Failed task creations are not retried automatically.
## References ## References
- Source: `src/cron_jobs/recurring-tasks.ts` - Source: `src/cron_jobs/recurring-tasks.ts`

View File

@@ -78,4 +78,8 @@ GOOGLE_CAPTCHA_SECRET_KEY=your_captcha_secret_key
GOOGLE_CAPTCHA_PASS_SCORE=0.8 GOOGLE_CAPTCHA_PASS_SCORE=0.8
# Email Cronjobs # Email Cronjobs
ENABLE_EMAIL_CRONJOBS=true ENABLE_EMAIL_CRONJOBS=true
# RECURRING_JOBS
ENABLE_RECURRING_JOBS=true
RECURRING_JOBS_INTERVAL="0 11 */1 * 1-5"

View File

@@ -1,6 +1,6 @@
import WorklenzControllerBase from "./worklenz-controller-base"; import WorklenzControllerBase from "./worklenz-controller-base";
import {getColor} from "../shared/utils"; import { getColor } from "../shared/utils";
import {PriorityColorCodes, TASK_PRIORITY_COLOR_ALPHA, TASK_STATUS_COLOR_ALPHA} from "../shared/constants"; import { PriorityColorCodes, TASK_PRIORITY_COLOR_ALPHA, TASK_STATUS_COLOR_ALPHA } from "../shared/constants";
import moment from "moment/moment"; import moment from "moment/moment";
export const GroupBy = { export const GroupBy = {
@@ -32,23 +32,14 @@ export default class TasksControllerBase extends WorklenzControllerBase {
} }
public static updateTaskViewModel(task: any) { public static updateTaskViewModel(task: any) {
console.log(`Processing task ${task.id} (${task.name})`);
console.log(` manual_progress: ${task.manual_progress}, progress_value: ${task.progress_value}`);
console.log(` project_use_manual_progress: ${task.project_use_manual_progress}, project_use_weighted_progress: ${task.project_use_weighted_progress}`);
console.log(` has subtasks: ${task.sub_tasks_count > 0}`);
// For parent tasks (with subtasks), always use calculated progress from subtasks // For parent tasks (with subtasks), always use calculated progress from subtasks
if (task.sub_tasks_count > 0) { if (task.sub_tasks_count > 0) {
// For parent tasks without manual progress, calculate from subtasks (already done via db function)
console.log(` Parent task with subtasks: complete_ratio=${task.complete_ratio}`);
// Ensure progress matches complete_ratio for consistency // Ensure progress matches complete_ratio for consistency
task.progress = task.complete_ratio || 0; task.progress = task.complete_ratio || 0;
// Important: Parent tasks should not have manual progress // Important: Parent tasks should not have manual progress
// If they somehow do, reset it // If they somehow do, reset it
if (task.manual_progress) { if (task.manual_progress) {
console.log(` WARNING: Parent task ${task.id} had manual_progress set to true, resetting`);
task.manual_progress = false; task.manual_progress = false;
task.progress_value = null; task.progress_value = null;
} }
@@ -58,28 +49,24 @@ export default class TasksControllerBase extends WorklenzControllerBase {
// For manually set progress, use that value directly // For manually set progress, use that value directly
task.progress = parseInt(task.progress_value); task.progress = parseInt(task.progress_value);
task.complete_ratio = parseInt(task.progress_value); task.complete_ratio = parseInt(task.progress_value);
}
console.log(` Using manual progress: progress=${task.progress}, complete_ratio=${task.complete_ratio}`);
}
// For tasks with no subtasks and no manual progress, calculate based on time // For tasks with no subtasks and no manual progress, calculate based on time
else { else {
task.progress = task.total_minutes_spent && task.total_minutes task.progress = task.total_minutes_spent && task.total_minutes
? ~~(task.total_minutes_spent / task.total_minutes * 100) ? ~~(task.total_minutes_spent / task.total_minutes * 100)
: 0; : 0;
// Set complete_ratio to match progress // Set complete_ratio to match progress
task.complete_ratio = task.progress; task.complete_ratio = task.progress;
console.log(` Calculated time-based progress: progress=${task.progress}, complete_ratio=${task.complete_ratio}`);
} }
// Ensure numeric values // Ensure numeric values
task.progress = parseInt(task.progress) || 0; task.progress = parseInt(task.progress) || 0;
task.complete_ratio = parseInt(task.complete_ratio) || 0; task.complete_ratio = parseInt(task.complete_ratio) || 0;
task.overdue = task.total_minutes < task.total_minutes_spent; task.overdue = task.total_minutes < task.total_minutes_spent;
task.time_spent = {hours: ~~(task.total_minutes_spent / 60), minutes: task.total_minutes_spent % 60}; task.time_spent = { hours: ~~(task.total_minutes_spent / 60), minutes: task.total_minutes_spent % 60 };
task.comments_count = Number(task.comments_count) ? +task.comments_count : 0; task.comments_count = Number(task.comments_count) ? +task.comments_count : 0;
task.attachments_count = Number(task.attachments_count) ? +task.attachments_count : 0; task.attachments_count = Number(task.attachments_count) ? +task.attachments_count : 0;

View File

@@ -97,7 +97,6 @@ export default class TasksControllerV2 extends TasksControllerBase {
try { try {
const result = await db.query("SELECT get_task_complete_ratio($1) AS info;", [taskId]); const result = await db.query("SELECT get_task_complete_ratio($1) AS info;", [taskId]);
const [data] = result.rows; const [data] = result.rows;
console.log("data", data);
if (data && data.info && data.info.ratio !== undefined) { if (data && data.info && data.info.ratio !== undefined) {
data.info.ratio = +((data.info.ratio || 0).toFixed()); data.info.ratio = +((data.info.ratio || 0).toFixed());
return data.info; return data.info;

View File

@@ -7,12 +7,90 @@ import TasksController from "../controllers/tasks-controller";
// At 11:00+00 (4.30pm+530) on every day-of-month if it's on every day-of-week from Monday through Friday. // At 11:00+00 (4.30pm+530) on every day-of-month if it's on every day-of-week from Monday through Friday.
// const TIME = "0 11 */1 * 1-5"; // const TIME = "0 11 */1 * 1-5";
const TIME = "*/2 * * * *"; // runs every 2 minutes - for testing purposes const TIME = process.env.RECURRING_JOBS_INTERVAL || "0 11 */1 * 1-5";
const TIME_FORMAT = "YYYY-MM-DD"; const TIME_FORMAT = "YYYY-MM-DD";
// const TIME = "0 0 * * *"; // Runs at midnight every day // const TIME = "0 0 * * *"; // Runs at midnight every day
const log = (value: any) => console.log("recurring-task-cron-job:", value); const log = (value: any) => console.log("recurring-task-cron-job:", value);
// Define future limits for different schedule types
// More conservative limits to prevent task list clutter
const FUTURE_LIMITS = {
daily: moment.duration(3, "days"),
weekly: moment.duration(1, "week"),
monthly: moment.duration(1, "month"),
every_x_days: (interval: number) => moment.duration(interval, "days"),
every_x_weeks: (interval: number) => moment.duration(interval, "weeks"),
every_x_months: (interval: number) => moment.duration(interval, "months")
};
// Helper function to get the future limit based on schedule type
function getFutureLimit(scheduleType: string, interval?: number): moment.Duration {
switch (scheduleType) {
case "daily":
return FUTURE_LIMITS.daily;
case "weekly":
return FUTURE_LIMITS.weekly;
case "monthly":
return FUTURE_LIMITS.monthly;
case "every_x_days":
return FUTURE_LIMITS.every_x_days(interval || 1);
case "every_x_weeks":
return FUTURE_LIMITS.every_x_weeks(interval || 1);
case "every_x_months":
return FUTURE_LIMITS.every_x_months(interval || 1);
default:
return moment.duration(3, "days"); // Default to 3 days
}
}
// Helper function to batch create tasks
async function createBatchTasks(template: ITaskTemplate & IRecurringSchedule, endDates: moment.Moment[]) {
const createdTasks = [];
for (const nextEndDate of endDates) {
const existingTaskQuery = `
SELECT id FROM tasks
WHERE schedule_id = $1 AND end_date::DATE = $2::DATE;
`;
const existingTaskResult = await db.query(existingTaskQuery, [template.schedule_id, nextEndDate.format(TIME_FORMAT)]);
if (existingTaskResult.rows.length === 0) {
const createTaskQuery = `SELECT create_quick_task($1::json) as task;`;
const taskData = {
name: template.name,
priority_id: template.priority_id,
project_id: template.project_id,
reporter_id: template.reporter_id,
status_id: template.status_id || null,
end_date: nextEndDate.format(TIME_FORMAT),
schedule_id: template.schedule_id
};
const createTaskResult = await db.query(createTaskQuery, [JSON.stringify(taskData)]);
const createdTask = createTaskResult.rows[0].task;
if (createdTask) {
createdTasks.push(createdTask);
for (const assignee of template.assignees) {
await TasksController.createTaskBulkAssignees(assignee.team_member_id, template.project_id, createdTask.id, assignee.assigned_by);
}
for (const label of template.labels) {
const q = `SELECT add_or_remove_task_label($1, $2) AS labels;`;
await db.query(q, [createdTask.id, label.label_id]);
}
console.log(`Created task for template ${template.name} with end date ${nextEndDate.format(TIME_FORMAT)}`);
}
} else {
console.log(`Skipped creating task for template ${template.name} with end date ${nextEndDate.format(TIME_FORMAT)} - task already exists`);
}
}
return createdTasks;
}
async function onRecurringTaskJobTick() { async function onRecurringTaskJobTick() {
try { try {
log("(cron) Recurring tasks job started."); log("(cron) Recurring tasks job started.");
@@ -33,65 +111,44 @@ async function onRecurringTaskJobTick() {
? moment(template.last_task_end_date) ? moment(template.last_task_end_date)
: moment(template.created_at); : moment(template.created_at);
const futureLimit = moment(template.last_checked_at || template.created_at).add(1, "week"); // Calculate future limit based on schedule type
const futureLimit = moment(template.last_checked_at || template.created_at)
.add(getFutureLimit(
template.schedule_type,
template.interval_days || template.interval_weeks || template.interval_months || 1
));
let nextEndDate = calculateNextEndDate(template, lastTaskEndDate); let nextEndDate = calculateNextEndDate(template, lastTaskEndDate);
const endDatesToCreate: moment.Moment[] = [];
// Find the next future occurrence // Find all future occurrences within the limit
while (nextEndDate.isSameOrBefore(now)) { while (nextEndDate.isSameOrBefore(futureLimit)) {
if (nextEndDate.isAfter(now)) {
endDatesToCreate.push(moment(nextEndDate));
}
nextEndDate = calculateNextEndDate(template, nextEndDate); nextEndDate = calculateNextEndDate(template, nextEndDate);
} }
// Only create a task if it's within the future limit // Batch create tasks for all future dates
if (nextEndDate.isSameOrBefore(futureLimit)) { if (endDatesToCreate.length > 0) {
const existingTaskQuery = ` const createdTasks = await createBatchTasks(template, endDatesToCreate);
SELECT id FROM tasks createdTaskCount += createdTasks.length;
WHERE schedule_id = $1 AND end_date::DATE = $2::DATE;
// Update the last_checked_at in the schedule
const updateScheduleQuery = `
UPDATE task_recurring_schedules
SET last_checked_at = $1::DATE,
last_created_task_end_date = $2
WHERE id = $3;
`; `;
const existingTaskResult = await db.query(existingTaskQuery, [template.schedule_id, nextEndDate.format(TIME_FORMAT)]); await db.query(updateScheduleQuery, [
moment().format(TIME_FORMAT),
if (existingTaskResult.rows.length === 0) { endDatesToCreate[endDatesToCreate.length - 1].format(TIME_FORMAT),
const createTaskQuery = `SELECT create_quick_task($1::json) as task;`; template.schedule_id
const taskData = { ]);
name: template.name,
priority_id: template.priority_id,
project_id: template.project_id,
reporter_id: template.reporter_id,
status_id: template.status_id || null,
end_date: nextEndDate.format(TIME_FORMAT),
schedule_id: template.schedule_id
};
const createTaskResult = await db.query(createTaskQuery, [JSON.stringify(taskData)]);
const createdTask = createTaskResult.rows[0].task;
if (createdTask) {
createdTaskCount++;
for (const assignee of template.assignees) {
await TasksController.createTaskBulkAssignees(assignee.team_member_id, template.project_id, createdTask.id, assignee.assigned_by);
}
for (const label of template.labels) {
const q = `SELECT add_or_remove_task_label($1, $2) AS labels;`;
await db.query(q, [createdTask.id, label.label_id]);
}
console.log(`Created task for template ${template.name} with end date ${nextEndDate.format(TIME_FORMAT)}`);
}
} else {
console.log(`Skipped creating task for template ${template.name} with end date ${nextEndDate.format(TIME_FORMAT)} - task already exists`);
}
} else { } else {
console.log(`No task created for template ${template.name} - next occurrence is beyond the future limit`); console.log(`No tasks created for template ${template.name} - next occurrence is beyond the future limit`);
} }
// Update the last_checked_at in the schedule
const updateScheduleQuery = `
UPDATE task_recurring_schedules
SET last_checked_at = $1::DATE, last_created_task_end_date = $2
WHERE id = $3;
`;
await db.query(updateScheduleQuery, [moment(template.last_checked_at || template.created_at).add(1, "day").format(TIME_FORMAT), nextEndDate.format(TIME_FORMAT), template.schedule_id]);
} }
log(`(cron) Recurring tasks job ended with ${createdTaskCount} new tasks created.`); log(`(cron) Recurring tasks job ended with ${createdTaskCount} new tasks created.`);

View File

@@ -2,6 +2,7 @@
"recurring": "Recurring", "recurring": "Recurring",
"recurringTaskConfiguration": "Recurring task configuration", "recurringTaskConfiguration": "Recurring task configuration",
"repeats": "Repeats", "repeats": "Repeats",
"daily": "Daily",
"weekly": "Weekly", "weekly": "Weekly",
"everyXDays": "Every X Days", "everyXDays": "Every X Days",
"everyXWeeks": "Every X Weeks", "everyXWeeks": "Every X Weeks",

View File

@@ -2,6 +2,7 @@
"recurring": "Recurrente", "recurring": "Recurrente",
"recurringTaskConfiguration": "Configuración de tarea recurrente", "recurringTaskConfiguration": "Configuración de tarea recurrente",
"repeats": "Repeticiones", "repeats": "Repeticiones",
"daily": "Diario",
"weekly": "Semanal", "weekly": "Semanal",
"everyXDays": "Cada X días", "everyXDays": "Cada X días",
"everyXWeeks": "Cada X semanas", "everyXWeeks": "Cada X semanas",

View File

@@ -2,6 +2,7 @@
"recurring": "Recorrente", "recurring": "Recorrente",
"recurringTaskConfiguration": "Configuração de tarefa recorrente", "recurringTaskConfiguration": "Configuração de tarefa recorrente",
"repeats": "Repete", "repeats": "Repete",
"daily": "Diário",
"weekly": "Semanal", "weekly": "Semanal",
"everyXDays": "A cada X dias", "everyXDays": "A cada X dias",
"everyXWeeks": "A cada X semanas", "everyXWeeks": "A cada X semanas",

View File

@@ -24,44 +24,46 @@ import { taskRecurringApiService } from '@/api/tasks/task-recurring.api.service'
import logger from '@/utils/errorLogger'; import logger from '@/utils/errorLogger';
import { setTaskRecurringSchedule } from '@/features/task-drawer/task-drawer.slice'; import { setTaskRecurringSchedule } from '@/features/task-drawer/task-drawer.slice';
const repeatOptions: IRepeatOption[] = [
{ label: 'Daily', value: ITaskRecurring.Daily },
{ label: 'Weekly', value: ITaskRecurring.Weekly },
{ label: 'Every X Days', value: ITaskRecurring.EveryXDays },
{ label: 'Every X Weeks', value: ITaskRecurring.EveryXWeeks },
{ label: 'Every X Months', value: ITaskRecurring.EveryXMonths },
{ label: 'Monthly', value: ITaskRecurring.Monthly },
];
const daysOfWeek = [
{ label: 'Sunday', value: 0, checked: false },
{ label: 'Monday', value: 1, checked: false },
{ label: 'Tuesday', value: 2, checked: false },
{ label: 'Wednesday', value: 3, checked: false },
{ label: 'Thursday', value: 4, checked: false },
{ label: 'Friday', value: 5, checked: false },
{ label: 'Saturday', value: 6, checked: false }
];
const monthlyDateOptions = Array.from({ length: 28 }, (_, i) => i + 1); const monthlyDateOptions = Array.from({ length: 28 }, (_, i) => i + 1);
const weekOptions = [
{ label: 'First', value: 1 },
{ label: 'Second', value: 2 },
{ label: 'Third', value: 3 },
{ label: 'Fourth', value: 4 },
{ label: 'Last', value: 5 }
];
const dayOptions = daysOfWeek.map(d => ({ label: d.label, value: d.value }));
const TaskDrawerRecurringConfig = ({ task }: { task: ITaskViewModel }) => { const TaskDrawerRecurringConfig = ({ task }: { task: ITaskViewModel }) => {
const { socket, connected } = useSocket(); const { socket, connected } = useSocket();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation('task-drawer/task-drawer-recurring-config'); const { t } = useTranslation('task-drawer/task-drawer-recurring-config');
const repeatOptions: IRepeatOption[] = [
{ label: t('daily'), value: ITaskRecurring.Daily },
{ label: t('weekly'), value: ITaskRecurring.Weekly },
{ label: t('everyXDays'), value: ITaskRecurring.EveryXDays },
{ label: t('everyXWeeks'), value: ITaskRecurring.EveryXWeeks },
{ label: t('everyXMonths'), value: ITaskRecurring.EveryXMonths },
{ label: t('monthly'), value: ITaskRecurring.Monthly },
];
const daysOfWeek = [
{ label: t('sun'), value: 0, checked: false },
{ label: t('mon'), value: 1, checked: false },
{ label: t('tue'), value: 2, checked: false },
{ label: t('wed'), value: 3, checked: false },
{ label: t('thu'), value: 4, checked: false },
{ label: t('fri'), value: 5, checked: false },
{ label: t('sat'), value: 6, checked: false }
];
const weekOptions = [
{ label: t('first'), value: 1 },
{ label: t('second'), value: 2 },
{ label: t('third'), value: 3 },
{ label: t('fourth'), value: 4 },
{ label: t('last'), value: 5 }
];
const dayOptions = daysOfWeek.map(d => ({ label: d.label, value: d.value }));
const [recurring, setRecurring] = useState(false); const [recurring, setRecurring] = useState(false);
const [showConfig, setShowConfig] = useState(false); const [showConfig, setShowConfig] = useState(false);
const [repeatOption, setRepeatOption] = useState<IRepeatOption>({}); const [repeatOption, setRepeatOption] = useState<IRepeatOption>({});
const [selectedDays, setSelectedDays] = useState([]); const [selectedDays, setSelectedDays] = useState<number[]>([]);
const [monthlyOption, setMonthlyOption] = useState('date'); const [monthlyOption, setMonthlyOption] = useState('date');
const [selectedMonthlyDate, setSelectedMonthlyDate] = useState(1); const [selectedMonthlyDate, setSelectedMonthlyDate] = useState(1);
const [selectedMonthlyWeek, setSelectedMonthlyWeek] = useState(weekOptions[0].value); const [selectedMonthlyWeek, setSelectedMonthlyWeek] = useState(weekOptions[0].value);
@@ -106,8 +108,8 @@ const TaskDrawerRecurringConfig = ({ task }: { task: ITaskViewModel }) => {
[repeatOption] [repeatOption]
); );
const handleDayCheckboxChange = (checkedValues: string[]) => { const handleDayCheckboxChange = (checkedValues: number[]) => {
setSelectedDays(checkedValues as unknown as string[]); setSelectedDays(checkedValues);
}; };
const getSelectedDays = () => { const getSelectedDays = () => {
@@ -165,7 +167,9 @@ const TaskDrawerRecurringConfig = ({ task }: { task: ITaskViewModel }) => {
const res = await taskRecurringApiService.updateTaskRecurringData(task.schedule_id, body); const res = await taskRecurringApiService.updateTaskRecurringData(task.schedule_id, body);
if (res.done) { if (res.done) {
setRecurring(true);
setShowConfig(false); setShowConfig(false);
configVisibleChange(false);
} }
} catch (e) { } catch (e) {
logger.error("handleSave", e); logger.error("handleSave", e);
@@ -220,9 +224,9 @@ const TaskDrawerRecurringConfig = ({ task }: { task: ITaskViewModel }) => {
if (!task) return; if (!task) return;
if (task) setRecurring(!!task.schedule_id); if (task) setRecurring(!!task.schedule_id);
if (recurring) void getScheduleData(); if (task.schedule_id) void getScheduleData();
socket?.on(SocketEvents.TASK_RECURRING_CHANGE.toString(), handleResponse); socket?.on(SocketEvents.TASK_RECURRING_CHANGE.toString(), handleResponse);
}, [task]); }, [task?.schedule_id]);
return ( return (
<div> <div>
@@ -232,11 +236,11 @@ const TaskDrawerRecurringConfig = ({ task }: { task: ITaskViewModel }) => {
&nbsp; &nbsp;
{recurring && ( {recurring && (
<Popover <Popover
title="Recurring task configuration" title={t('recurringTaskConfiguration')}
content={ content={
<Skeleton loading={loadingData} active> <Skeleton loading={loadingData} active>
<Form layout="vertical"> <Form layout="vertical">
<Form.Item label="Repeats"> <Form.Item label={t('repeats')}>
<Select <Select
value={repeatOption.value} value={repeatOption.value}
onChange={val => { onChange={val => {
@@ -251,9 +255,12 @@ const TaskDrawerRecurringConfig = ({ task }: { task: ITaskViewModel }) => {
</Form.Item> </Form.Item>
{repeatOption.value === ITaskRecurring.Weekly && ( {repeatOption.value === ITaskRecurring.Weekly && (
<Form.Item label="Select Days of the Week"> <Form.Item label={t('selectDaysOfWeek')}>
<Checkbox.Group <Checkbox.Group
options={daysOfWeek} options={daysOfWeek.map(day => ({
label: day.label,
value: day.value
}))}
value={selectedDays} value={selectedDays}
onChange={handleDayCheckboxChange} onChange={handleDayCheckboxChange}
style={{ width: '100%' }} style={{ width: '100%' }}
@@ -271,17 +278,17 @@ const TaskDrawerRecurringConfig = ({ task }: { task: ITaskViewModel }) => {
{isMonthlySelected && ( {isMonthlySelected && (
<> <>
<Form.Item label="Monthly repeat type"> <Form.Item label={t('monthlyRepeatType')}>
<Radio.Group <Radio.Group
value={monthlyOption} value={monthlyOption}
onChange={e => setMonthlyOption(e.target.value)} onChange={e => setMonthlyOption(e.target.value)}
> >
<Radio.Button value="date">On a specific date</Radio.Button> <Radio.Button value="date">{t('onSpecificDate')}</Radio.Button>
<Radio.Button value="day">On a specific day</Radio.Button> <Radio.Button value="day">{t('onSpecificDay')}</Radio.Button>
</Radio.Group> </Radio.Group>
</Form.Item> </Form.Item>
{monthlyOption === 'date' && ( {monthlyOption === 'date' && (
<Form.Item label="Date of the month"> <Form.Item label={t('dateOfMonth')}>
<Select <Select
value={selectedMonthlyDate} value={selectedMonthlyDate}
onChange={setSelectedMonthlyDate} onChange={setSelectedMonthlyDate}
@@ -295,7 +302,7 @@ const TaskDrawerRecurringConfig = ({ task }: { task: ITaskViewModel }) => {
)} )}
{monthlyOption === 'day' && ( {monthlyOption === 'day' && (
<> <>
<Form.Item label="Week of the month"> <Form.Item label={t('weekOfMonth')}>
<Select <Select
value={selectedMonthlyWeek} value={selectedMonthlyWeek}
onChange={setSelectedMonthlyWeek} onChange={setSelectedMonthlyWeek}
@@ -303,7 +310,7 @@ const TaskDrawerRecurringConfig = ({ task }: { task: ITaskViewModel }) => {
style={{ width: 150 }} style={{ width: 150 }}
/> />
</Form.Item> </Form.Item>
<Form.Item label="Day of the week"> <Form.Item label={t('dayOfWeek')}>
<Select <Select
value={selectedMonthlyDay} value={selectedMonthlyDay}
onChange={setSelectedMonthlyDay} onChange={setSelectedMonthlyDay}
@@ -317,7 +324,7 @@ const TaskDrawerRecurringConfig = ({ task }: { task: ITaskViewModel }) => {
)} )}
{repeatOption.value === ITaskRecurring.EveryXDays && ( {repeatOption.value === ITaskRecurring.EveryXDays && (
<Form.Item label="Interval (days)"> <Form.Item label={t('intervalDays')}>
<InputNumber <InputNumber
min={1} min={1}
value={intervalDays} value={intervalDays}
@@ -326,7 +333,7 @@ const TaskDrawerRecurringConfig = ({ task }: { task: ITaskViewModel }) => {
</Form.Item> </Form.Item>
)} )}
{repeatOption.value === ITaskRecurring.EveryXWeeks && ( {repeatOption.value === ITaskRecurring.EveryXWeeks && (
<Form.Item label="Interval (weeks)"> <Form.Item label={t('intervalWeeks')}>
<InputNumber <InputNumber
min={1} min={1}
value={intervalWeeks} value={intervalWeeks}
@@ -335,7 +342,7 @@ const TaskDrawerRecurringConfig = ({ task }: { task: ITaskViewModel }) => {
</Form.Item> </Form.Item>
)} )}
{repeatOption.value === ITaskRecurring.EveryXMonths && ( {repeatOption.value === ITaskRecurring.EveryXMonths && (
<Form.Item label="Interval (months)"> <Form.Item label={t('intervalMonths')}>
<InputNumber <InputNumber
min={1} min={1}
value={intervalMonths} value={intervalMonths}
@@ -350,7 +357,7 @@ const TaskDrawerRecurringConfig = ({ task }: { task: ITaskViewModel }) => {
loading={updatingData} loading={updatingData}
onClick={handleSave} onClick={handleSave}
> >
Save Changes {t('saveChanges')}
</Button> </Button>
</Form.Item> </Form.Item>
</Form> </Form>