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:
@@ -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.
|
||||||
@@ -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`
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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.`);
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 }) => {
|
|||||||
|
|
||||||
{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>
|
||||||
|
|||||||
Reference in New Issue
Block a user