feat(recurring-tasks): implement recurring task scheduling and API integration

This commit is contained in:
chamikaJ
2025-05-16 14:32:45 +05:30
parent 8e74f1ddb5
commit 2e985bd051
6 changed files with 185 additions and 59 deletions

View File

@@ -7,5 +7,5 @@ export function startCronJobs() {
startNotificationsJob(); startNotificationsJob();
startDailyDigestJob(); startDailyDigestJob();
startProjectDigestJob(); startProjectDigestJob();
// startRecurringTasksJob(); startRecurringTasksJob();
} }

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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);
}; };
const getScheduleData = () => {}; 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 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>

View File

@@ -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;

View File

@@ -1,15 +1,28 @@
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 {
@@ -17,3 +30,8 @@ export interface ITaskRecurringScheduleData {
id?: string, id?: string,
schedule_type?: string schedule_type?: string
} }
export interface IRepeatOption {
value?: ITaskRecurring
label?: string
}