feat(recurring-tasks): implement recurring task scheduling and API integration
This commit is contained in:
@@ -1,11 +1,11 @@
|
|||||||
import {startDailyDigestJob} from "./daily-digest-job";
|
import {startDailyDigestJob} from "./daily-digest-job";
|
||||||
import {startNotificationsJob} from "./notifications-job";
|
import {startNotificationsJob} from "./notifications-job";
|
||||||
import {startProjectDigestJob} from "./project-digest-job";
|
import {startProjectDigestJob} from "./project-digest-job";
|
||||||
import { startRecurringTasksJob } from "./recurring-tasks";
|
import {startRecurringTasksJob} from "./recurring-tasks";
|
||||||
|
|
||||||
export function startCronJobs() {
|
export function startCronJobs() {
|
||||||
startNotificationsJob();
|
startNotificationsJob();
|
||||||
startDailyDigestJob();
|
startDailyDigestJob();
|
||||||
startProjectDigestJob();
|
startProjectDigestJob();
|
||||||
// startRecurringTasksJob();
|
startRecurringTasksJob();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ 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 * * * *";
|
const TIME = "*/2 * * * *"; // runs every 2 minutes - for testing purposes
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { API_BASE_URL } from "@/shared/constants";
|
||||||
|
import { IServerResponse } from "@/types/common.types";
|
||||||
|
import { ITaskRecurringSchedule } from "@/types/tasks/task-recurring-schedule";
|
||||||
|
import apiClient from "../api-client";
|
||||||
|
|
||||||
|
const rootUrl = `${API_BASE_URL}/task-recurring`;
|
||||||
|
|
||||||
|
export const taskRecurringApiService = {
|
||||||
|
getTaskRecurringData: async (schedule_id: string): Promise<IServerResponse<ITaskRecurringSchedule>> => {
|
||||||
|
const response = await apiClient.get(`${rootUrl}/${schedule_id}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
updateTaskRecurringData: async (schedule_id: string, body: any): Promise<IServerResponse<ITaskRecurringSchedule>> => {
|
||||||
|
return apiClient.put(`${rootUrl}/${schedule_id}`, body);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,21 +15,17 @@ import {
|
|||||||
import { SettingOutlined } from '@ant-design/icons';
|
import { SettingOutlined } from '@ant-design/icons';
|
||||||
import { useSocket } from '@/socket/socketContext';
|
import { useSocket } from '@/socket/socketContext';
|
||||||
import { SocketEvents } from '@/shared/socket-events';
|
import { SocketEvents } from '@/shared/socket-events';
|
||||||
import { ITaskRecurringScheduleData } from '@/types/tasks/task-recurring-schedule';
|
import { IRepeatOption, ITaskRecurring, ITaskRecurringSchedule, ITaskRecurringScheduleData } from '@/types/tasks/task-recurring-schedule';
|
||||||
import { ITaskViewModel } from '@/types/tasks/task.types';
|
import { ITaskViewModel } from '@/types/tasks/task.types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
import { updateRecurringChange } from '@/features/tasks/tasks.slice';
|
import { updateRecurringChange } from '@/features/tasks/tasks.slice';
|
||||||
|
import { taskRecurringApiService } from '@/api/tasks/task-recurring.api.service';
|
||||||
|
import logger from '@/utils/errorLogger';
|
||||||
|
import { setTaskRecurringSchedule } from '@/features/task-drawer/task-drawer.slice';
|
||||||
|
|
||||||
const ITaskRecurring = {
|
const repeatOptions: IRepeatOption[] = [
|
||||||
Weekly: 'weekly',
|
{ label: 'Daily', value: ITaskRecurring.Daily },
|
||||||
EveryXDays: 'every_x_days',
|
|
||||||
EveryXWeeks: 'every_x_weeks',
|
|
||||||
EveryXMonths: 'every_x_months',
|
|
||||||
Monthly: 'monthly',
|
|
||||||
};
|
|
||||||
|
|
||||||
const repeatOptions = [
|
|
||||||
{ label: 'Weekly', value: ITaskRecurring.Weekly },
|
{ label: 'Weekly', value: ITaskRecurring.Weekly },
|
||||||
{ label: 'Every X Days', value: ITaskRecurring.EveryXDays },
|
{ label: 'Every X Days', value: ITaskRecurring.EveryXDays },
|
||||||
{ label: 'Every X Weeks', value: ITaskRecurring.EveryXWeeks },
|
{ label: 'Every X Weeks', value: ITaskRecurring.EveryXWeeks },
|
||||||
@@ -38,22 +34,22 @@ const repeatOptions = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const daysOfWeek = [
|
const daysOfWeek = [
|
||||||
{ label: 'Mon', value: 'mon' },
|
{ label: 'Sunday', value: 0, checked: false },
|
||||||
{ label: 'Tue', value: 'tue' },
|
{ label: 'Monday', value: 1, checked: false },
|
||||||
{ label: 'Wed', value: 'wed' },
|
{ label: 'Tuesday', value: 2, checked: false },
|
||||||
{ label: 'Thu', value: 'thu' },
|
{ label: 'Wednesday', value: 3, checked: false },
|
||||||
{ label: 'Fri', value: 'fri' },
|
{ label: 'Thursday', value: 4, checked: false },
|
||||||
{ label: 'Sat', value: 'sat' },
|
{ label: 'Friday', value: 5, checked: false },
|
||||||
{ label: 'Sun', value: 'sun' },
|
{ label: 'Saturday', value: 6, checked: false }
|
||||||
];
|
];
|
||||||
|
|
||||||
const monthlyDateOptions = Array.from({ length: 31 }, (_, i) => i + 1);
|
const monthlyDateOptions = Array.from({ length: 28 }, (_, i) => i + 1);
|
||||||
const weekOptions = [
|
const weekOptions = [
|
||||||
{ label: 'First', value: 'first' },
|
{ label: 'First', value: 1 },
|
||||||
{ label: 'Second', value: 'second' },
|
{ label: 'Second', value: 2 },
|
||||||
{ label: 'Third', value: 'third' },
|
{ label: 'Third', value: 3 },
|
||||||
{ label: 'Fourth', value: 'fourth' },
|
{ label: 'Fourth', value: 4 },
|
||||||
{ label: 'Last', value: 'last' },
|
{ label: 'Last', value: 5 }
|
||||||
];
|
];
|
||||||
const dayOptions = daysOfWeek.map(d => ({ label: d.label, value: d.value }));
|
const dayOptions = daysOfWeek.map(d => ({ label: d.label, value: d.value }));
|
||||||
|
|
||||||
@@ -64,7 +60,7 @@ const TaskDrawerRecurringConfig = ({ task }: { task: ITaskViewModel }) => {
|
|||||||
|
|
||||||
const [recurring, setRecurring] = useState(false);
|
const [recurring, setRecurring] = useState(false);
|
||||||
const [showConfig, setShowConfig] = useState(false);
|
const [showConfig, setShowConfig] = useState(false);
|
||||||
const [repeatOption, setRepeatOption] = useState(repeatOptions[0]);
|
const [repeatOption, setRepeatOption] = useState<IRepeatOption>({});
|
||||||
const [selectedDays, setSelectedDays] = useState([]);
|
const [selectedDays, setSelectedDays] = useState([]);
|
||||||
const [monthlyOption, setMonthlyOption] = useState('date');
|
const [monthlyOption, setMonthlyOption] = useState('date');
|
||||||
const [selectedMonthlyDate, setSelectedMonthlyDate] = useState(1);
|
const [selectedMonthlyDate, setSelectedMonthlyDate] = useState(1);
|
||||||
@@ -75,6 +71,7 @@ const TaskDrawerRecurringConfig = ({ task }: { task: ITaskViewModel }) => {
|
|||||||
const [intervalMonths, setIntervalMonths] = useState(1);
|
const [intervalMonths, setIntervalMonths] = useState(1);
|
||||||
const [loadingData, setLoadingData] = useState(false);
|
const [loadingData, setLoadingData] = useState(false);
|
||||||
const [updatingData, setUpdatingData] = useState(false);
|
const [updatingData, setUpdatingData] = useState(false);
|
||||||
|
const [scheduleData, setScheduleData] = useState<ITaskRecurringSchedule>({});
|
||||||
|
|
||||||
const handleChange = (checked: boolean) => {
|
const handleChange = (checked: boolean) => {
|
||||||
if (!task.id) return;
|
if (!task.id) return;
|
||||||
@@ -92,6 +89,7 @@ const TaskDrawerRecurringConfig = ({ task }: { task: ITaskViewModel }) => {
|
|||||||
if (selected) setRepeatOption(selected);
|
if (selected) setRepeatOption(selected);
|
||||||
}
|
}
|
||||||
dispatch(updateRecurringChange(schedule));
|
dispatch(updateRecurringChange(schedule));
|
||||||
|
dispatch(setTaskRecurringSchedule({ schedule_id: schedule.id as string, task_id: task.id }));
|
||||||
|
|
||||||
setRecurring(checked);
|
setRecurring(checked);
|
||||||
if (!checked) setShowConfig(false);
|
if (!checked) setShowConfig(false);
|
||||||
@@ -112,35 +110,119 @@ const TaskDrawerRecurringConfig = ({ task }: { task: ITaskViewModel }) => {
|
|||||||
setSelectedDays(checkedValues as unknown as string[]);
|
setSelectedDays(checkedValues as unknown as string[]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = () => {
|
const getSelectedDays = () => {
|
||||||
// Compose the schedule data and call the update handler
|
return daysOfWeek
|
||||||
const data = {
|
.filter(day => day.checked) // Get only the checked days
|
||||||
recurring,
|
.map(day => day.value); // Extract their numeric values
|
||||||
repeatOption,
|
}
|
||||||
selectedDays,
|
|
||||||
monthlyOption,
|
const getUpdateBody = () => {
|
||||||
selectedMonthlyDate,
|
if (!task.id || !task.schedule_id || !repeatOption.value) return;
|
||||||
selectedMonthlyWeek,
|
|
||||||
selectedMonthlyDay,
|
const body: ITaskRecurringSchedule = {
|
||||||
intervalDays,
|
id: task.id,
|
||||||
intervalWeeks,
|
schedule_type: repeatOption.value
|
||||||
intervalMonths,
|
|
||||||
};
|
};
|
||||||
// if (onUpdateSchedule) onUpdateSchedule(data);
|
|
||||||
setShowConfig(false);
|
switch (repeatOption.value) {
|
||||||
|
case ITaskRecurring.Weekly:
|
||||||
|
body.days_of_week = getSelectedDays();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ITaskRecurring.Monthly:
|
||||||
|
if (monthlyOption === 'date') {
|
||||||
|
body.date_of_month = selectedMonthlyDate;
|
||||||
|
setSelectedMonthlyDate(0);
|
||||||
|
setSelectedMonthlyDay(0);
|
||||||
|
} else {
|
||||||
|
body.week_of_month = selectedMonthlyWeek;
|
||||||
|
body.day_of_month = selectedMonthlyDay;
|
||||||
|
setSelectedMonthlyDate(0);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ITaskRecurring.EveryXDays:
|
||||||
|
body.interval_days = intervalDays;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ITaskRecurring.EveryXWeeks:
|
||||||
|
body.interval_weeks = intervalWeeks;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ITaskRecurring.EveryXMonths:
|
||||||
|
body.interval_months = intervalMonths;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!task.id || !task.schedule_id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setUpdatingData(true);
|
||||||
|
const body = getUpdateBody();
|
||||||
|
|
||||||
|
const res = await taskRecurringApiService.updateTaskRecurringData(task.schedule_id, body);
|
||||||
|
if (res.done) {
|
||||||
|
setShowConfig(false);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("handleSave", e);
|
||||||
|
} finally {
|
||||||
|
setUpdatingData(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getScheduleData = () => {};
|
const updateDaysOfWeek = () => {
|
||||||
|
for (let i = 0; i < daysOfWeek.length; i++) {
|
||||||
|
daysOfWeek[i].checked = scheduleData.days_of_week?.includes(daysOfWeek[i].value) ?? false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getScheduleData = async () => {
|
||||||
|
if (!task.schedule_id) return;
|
||||||
|
setLoadingData(true);
|
||||||
|
try {
|
||||||
|
const res = await taskRecurringApiService.getTaskRecurringData(task.schedule_id);
|
||||||
|
if (res.done) {
|
||||||
|
setScheduleData(res.body);
|
||||||
|
if (!res.body) {
|
||||||
|
setRepeatOption(repeatOptions[0]);
|
||||||
|
} else {
|
||||||
|
const selected = repeatOptions.find(e => e.value == res.body.schedule_type);
|
||||||
|
if (selected) {
|
||||||
|
setRepeatOption(selected);
|
||||||
|
setSelectedMonthlyDate(scheduleData.date_of_month || 1);
|
||||||
|
setSelectedMonthlyDay(scheduleData.day_of_month || 0);
|
||||||
|
setSelectedMonthlyWeek(scheduleData.week_of_month || 0);
|
||||||
|
setIntervalDays(scheduleData.interval_days || 1);
|
||||||
|
setIntervalWeeks(scheduleData.interval_weeks || 1);
|
||||||
|
setIntervalMonths(scheduleData.interval_months || 1);
|
||||||
|
setMonthlyOption(selectedMonthlyDate ? 'date' : 'day');
|
||||||
|
updateDaysOfWeek();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("getScheduleData", e);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
setLoadingData(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleResponse = (response: ITaskRecurringScheduleData) => {
|
const handleResponse = (response: ITaskRecurringScheduleData) => {
|
||||||
if (!task || !response.task_id) return;
|
if (!task || !response.task_id) return;
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!task) return;
|
||||||
|
|
||||||
if (task) setRecurring(!!task.schedule_id);
|
if (task) setRecurring(!!task.schedule_id);
|
||||||
if (recurring) void getScheduleData();
|
if (recurring) void getScheduleData();
|
||||||
socket?.on(SocketEvents.TASK_RECURRING_CHANGE.toString(), handleResponse);
|
socket?.on(SocketEvents.TASK_RECURRING_CHANGE.toString(), handleResponse);
|
||||||
}, []);
|
}, [task]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -105,6 +105,15 @@ const taskDrawerSlice = createSlice({
|
|||||||
}>) => {
|
}>) => {
|
||||||
state.timeLogEditing = action.payload;
|
state.timeLogEditing = action.payload;
|
||||||
},
|
},
|
||||||
|
setTaskRecurringSchedule: (state, action: PayloadAction<{
|
||||||
|
schedule_id: string;
|
||||||
|
task_id: string;
|
||||||
|
}>) => {
|
||||||
|
const { schedule_id, task_id } = action.payload;
|
||||||
|
if (state.taskFormViewModel?.task && state.taskFormViewModel.task.id === task_id) {
|
||||||
|
state.taskFormViewModel.task.schedule_id = schedule_id;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers: builder => {
|
extraReducers: builder => {
|
||||||
builder.addCase(fetchTask.pending, state => {
|
builder.addCase(fetchTask.pending, state => {
|
||||||
@@ -133,5 +142,6 @@ export const {
|
|||||||
setTaskLabels,
|
setTaskLabels,
|
||||||
setTaskSubscribers,
|
setTaskSubscribers,
|
||||||
setTimeLogEditing,
|
setTimeLogEditing,
|
||||||
|
setTaskRecurringSchedule
|
||||||
} = taskDrawerSlice.actions;
|
} = taskDrawerSlice.actions;
|
||||||
export default taskDrawerSlice.reducer;
|
export default taskDrawerSlice.reducer;
|
||||||
|
|||||||
@@ -1,19 +1,37 @@
|
|||||||
|
export enum ITaskRecurring {
|
||||||
|
Daily = 'daily',
|
||||||
|
Weekly = 'weekly',
|
||||||
|
Monthly = 'monthly',
|
||||||
|
EveryXDays = 'every_x_days',
|
||||||
|
EveryXWeeks = 'every_x_weeks',
|
||||||
|
EveryXMonths = 'every_x_months'
|
||||||
|
}
|
||||||
|
|
||||||
export interface ITaskRecurringSchedule {
|
export interface ITaskRecurringSchedule {
|
||||||
type: 'daily' | 'weekly' | 'monthly' | 'interval';
|
created_at?: string;
|
||||||
dayOfWeek?: number; // 0 = Sunday, 1 = Monday, ..., 6 = Saturday (for weekly tasks)
|
day_of_month?: number | null;
|
||||||
dayOfMonth?: number; // 1 - 31 (for monthly tasks)
|
date_of_month?: number | null;
|
||||||
weekOfMonth?: number; // 1 = 1st week, 2 = 2nd week, ..., 5 = Last week (for monthly tasks)
|
days_of_week?: number[] | null;
|
||||||
hour: number; // Time of the day in 24-hour format
|
id?: string; // UUID v4
|
||||||
minute: number; // Minute of the hour
|
interval_days?: number | null;
|
||||||
interval?: {
|
interval_months?: number | null;
|
||||||
days?: number; // Interval in days (for every x days)
|
interval_weeks?: number | null;
|
||||||
weeks?: number; // Interval in weeks (for every x weeks)
|
schedule_type?: ITaskRecurring;
|
||||||
months?: number; // Interval in months (for every x months)
|
week_of_month?: number | null;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
export interface IRepeatOption {
|
||||||
|
value?: ITaskRecurring
|
||||||
|
label?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITaskRecurringScheduleData {
|
export interface ITaskRecurringScheduleData {
|
||||||
task_id?: string,
|
task_id?: string,
|
||||||
id?: string,
|
id?: string,
|
||||||
schedule_type?: string
|
schedule_type?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IRepeatOption {
|
||||||
|
value?: ITaskRecurring
|
||||||
|
label?: string
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user