Skip to content

Commit

Permalink
Repeating task fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
alexd-bes committed Aug 22, 2024
1 parent 7f660c9 commit 7de581f
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 34 deletions.
23 changes: 12 additions & 11 deletions packages/datatrak-web-server/src/utils/formatTaskChanges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,29 @@ type Output = Partial<Task> & {
comment?: string;
};

const convertDateToEndOfDay = (date: Date | number) => {
const dateObj = new Date(date);
const endOfDay = new Date(dateObj.setHours(23, 59, 59, 999));
return endOfDay;
};

export const formatTaskChanges = (task: Input, originalTask?: Task) => {
const { due_date: dueDate, repeat_frequency: frequency, assignee, ...restOfTask } = task;

const taskDetails: Output = restOfTask;

if (isNotNullish(frequency)) {
if (
isNotNullish(frequency) ||
(originalTask?.repeat_schedule && frequency === undefined && dueDate)
) {
// if there is no due date to use, use the original task's due date (this will be the case when editing a task's repeat schedule without changing the due date)
const dueDateToUse = dueDate || originalTask?.due_date;

// if there is no due date to use, throw an error - this should never happen but is a safety check
if (!dueDateToUse) {
throw new Error('Must have a due date');
}
const endOfDay = convertDateToEndOfDay(dueDateToUse);

const dueDateObj = new Date(dueDateToUse);

// if frequency is explicitly set, use that, otherwise use the original task's frequency. This is for when editing a repeating task's due date, because we will want to update the 'dtstart' of the rrule
const frequencyToUse = frequency ?? originalTask?.repeat_schedule?.freq;
// if task is repeating, generate rrule
const rrule = generateRRule(endOfDay, frequency);
const rrule = generateRRule(dueDateToUse, frequencyToUse);
// set repeat_schedule to the original options object so we can use it to generate next occurrences and display the schedule
taskDetails.repeat_schedule = rrule.origOptions;
}
Expand All @@ -46,8 +48,7 @@ export const formatTaskChanges = (task: Input, originalTask?: Task) => {

// if there is a due date, convert it to unix
if (dueDate) {
const endOfDay = convertDateToEndOfDay(dueDate);
const unix = new Date(endOfDay).getTime();
const unix = new Date(dueDate).getTime();

taskDetails.due_date = unix;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/datatrak-web/src/features/Tasks/DueDatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const DueDatePicker = ({
const endOfDay = new Date(new Date(newValue).setHours(23, 59, 59, 999));

// format the date to include timezone
const newDate = format(endOfDay, `yyyy-MM-dd HH:mm:ss.SSSXXX`);
const newDate = format(endOfDay, `yyyy-MM-dd'T'HH:mm:ss.SSSXXX`);

setDate(newDate);
};
Expand Down
130 changes: 122 additions & 8 deletions packages/utils/src/__tests__/rrule.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,23 @@ describe('RRule', () => {
interval: 1,
}),
);
expect(rrule.all()[0]).toEqual(new Date('2021-01-01'));
expect(rrule.all()[1]).toEqual(new Date('2021-01-02'));
const all = rrule.all();
expect(all[0]).toEqual(new Date('2021-01-01'));
expect(all[1]).toEqual(new Date('2021-01-02'));
});

it('generateDailyRRule should return an RRule that will repeat daily from the given start date when date is a string', () => {
const rrule = generateDailyRRule('2021-01-01T00:00:00.000-08:00');
expect(rrule).toEqual(
new RRule({
freq: RRule.DAILY,
dtstart: new Date('2021-01-01T00:00:00.000-08:00'),
interval: 1,
}),
);
const all = rrule.all();
expect(all[0]).toEqual(new Date('2021-01-01T00:00:00.000-08:00'));
expect(all[1]).toEqual(new Date('2021-01-02T00:00:00.000-08:00'));
});

it('generateWeeklyRRule should return an RRule that will repeat weekly from the given start date', () => {
Expand All @@ -37,8 +52,24 @@ describe('RRule', () => {
interval: 1,
}),
);
expect(rrule.all()[0]).toEqual(startDate);
expect(rrule.all()[1]).toEqual(new Date('2021-01-08'));
const all = rrule.all();
expect(all[0]).toEqual(startDate);
expect(all[1]).toEqual(new Date('2021-01-08'));
});

it('generateWeeklyRRule should return an RRule that will repeat weekly from the given start date, when startDate is a string', () => {
const startDate = '2021-01-01T00:00:00.000-08:00';
const rrule = generateWeeklyRRule(startDate);
expect(rrule).toEqual(
new RRule({
freq: RRule.WEEKLY,
dtstart: new Date(startDate),
interval: 1,
}),
);
const all = rrule.all();
expect(all[0]).toEqual(new Date(startDate));
expect(all[1]).toEqual(new Date('2021-01-08T00:00:00.000-08:00'));
});

it('generateMonthlyRRule should return an RRule that will repeat monthly from the given start date', () => {
Expand All @@ -52,8 +83,26 @@ describe('RRule', () => {
interval: 1,
}),
);
expect(rrule.all()[0]).toEqual(startDate);
expect(rrule.all()[1]).toEqual(new Date('2021-02-01'));
const all = rrule.all();
expect(all[0]).toEqual(startDate);
expect(all[1]).toEqual(new Date('2021-02-01'));
});

it('generateMonthlyRRule should handle when startDate is last day of the month utc time', () => {
const startDate = '2021-01-30T23:59:59.999-08:00';
const utcDate = new Date(startDate);
const rrule = generateMonthlyRRule(startDate);
expect(rrule).toEqual(
new RRule({
freq: RRule.MONTHLY,
dtstart: utcDate,
bymonthday: [-1],
interval: 1,
}),
);
const all = rrule.all();
expect(all[0]).toEqual(utcDate);
expect(all[1]).toEqual(new Date('2021-02-27T23:59:59.999-08:00'));
});

it('generateMonthlyRRule should return an RRule that will repeat monthly on the last day of the month if the given start date is the last day of that month', () => {
Expand All @@ -73,6 +122,34 @@ describe('RRule', () => {
expect(rrule.after(new Date('2024-01-31'))).toEqual(new Date('2024-02-29'));
});

it('generateMonthlyRRule should handle when date is a string with a timezone for last day of month local time', () => {
// 31 Jan 2021 23:59:59.999 in Los Angeles timezone
const startDate = '2021-01-31T23:59:59.999-08:00';
const rrule = generateMonthlyRRule(startDate);
expect(rrule).toEqual(
new RRule({
freq: RRule.MONTHLY,
// saves the date as UTC
dtstart: new Date(startDate),
// by month day is 1 because the date is the last day of the month in local time but the first day of the month in UTC
bymonthday: [1],
interval: 1,
}),
);

// the first occurrence should be the equivalent of the last day of january in UTC
expect(rrule.all()[0]).toEqual(new Date(startDate));
expect(rrule.after(new Date('2021-02-01T00:00:00.000-08:00'))).toEqual(
new Date('2021-02-28T23:59:59.999-08:00'),
);
expect(rrule.after(new Date('2021-04-01T00:00:00.000-08:00'))).toEqual(
new Date('2021-04-30T23:59:59.999-08:00'),
);
expect(rrule.after(new Date('2024-01-31T23:59:59.999-08:00'))).toEqual(
new Date('2024-02-29T23:59:59.999-08:00'),
);
});

it('generateMonthlyRRule should return an RRule that will repeat monthly on either the 30th or the last day of the month for February if the given start date is the 30th', () => {
const startDate = new Date('2021-01-30');
const rrule = generateMonthlyRRule(startDate);
Expand All @@ -90,6 +167,27 @@ describe('RRule', () => {
expect(rrule.after(new Date('2024-01-31'))).toEqual(new Date('2024-02-29'));
});

it('generateMonthlyRRule should return an RRule that will repeat monthly on either the 30th or the last day of the month for February if the given start date is the 30th in UTC time', () => {
const startDate = '2021-01-29T23:59:59.999-08:00';
const rrule = generateMonthlyRRule(startDate);
expect(rrule).toEqual(
new RRule({
freq: RRule.MONTHLY,
dtstart: new Date(startDate),
bymonthday: [28, 29, 30],
bysetpos: -1,
interval: 1,
}),
);

const all = rrule.all();
expect(all[0]).toEqual(new Date(startDate));
expect(all[1]).toEqual(new Date('2021-02-27T23:59:59.999-08:00'));
expect(rrule.after(new Date('2024-01-31T23:59:59.999-08:00'))).toEqual(
new Date('2024-02-28T23:59:59.999-08:00'),
);
});

it('generateYearlyRRule should return an RRule that will repeat yearly from the given start date', () => {
const startDate = new Date('2021-01-30');
const rrule = generateYearlyRRule(startDate);
Expand All @@ -100,8 +198,24 @@ describe('RRule', () => {
interval: 1,
}),
);
expect(rrule.all()[0]).toEqual(startDate);
expect(rrule.all()[1]).toEqual(new Date('2022-01-30'));
const all = rrule.all();
expect(all[0]).toEqual(startDate);
expect(all[1]).toEqual(new Date('2022-01-30'));
});

it('generateYearlyRRule should return an RRule that will repeat yearly from the given start date, and startDate is a string', () => {
const startDate = '2021-01-30T00:00:00.000-08:00';
const rrule = generateYearlyRRule(startDate);
expect(rrule).toEqual(
new RRule({
freq: RRule.YEARLY,
dtstart: new Date(startDate),
interval: 1,
}),
);
const all = rrule.all();
expect(all[0]).toEqual(new Date(startDate));
expect(all[1]).toEqual(new Date('2022-01-30T00:00:00.000-08:00'));
});

it('generateRRule should return an RRule using the start date and frequency', () => {
Expand Down
31 changes: 17 additions & 14 deletions packages/utils/src/rrule.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/
import { Frequency, RRule } from 'rrule';
import { isLastDayOfMonth } from 'date-fns';
import { getDaysInMonth } from 'date-fns';

export const RRULE_FREQUENCIES = {
DAILY: Frequency.DAILY,
Expand All @@ -29,52 +29,55 @@ export const generateRRule = (startDate, frequency) => {

/**
*
* @param {Date} startDate
* @param {string | Date} startDate - The date to start the rule from
* @returns {RRule} RRule that will repeat daily from the given start date
*/
export const generateDailyRRule = startDate => {
return new RRule({
freq: RRule.DAILY,
dtstart: startDate,
dtstart: new Date(startDate),
interval: 1,
});
};

/**
*
* @param {Date} startDate
* @param {string | Date} startDate - The date to start the rule from
* @returns {RRule} RRule that will repeat weekly on the given days of the week from the given start date
*/
export const generateWeeklyRRule = startDate => {
return new RRule({
freq: RRule.WEEKLY,
dtstart: startDate,
dtstart: new Date(startDate),
interval: 1,
});
};

/**
*
* @param {Date} startDate
* @param {string | Date} startDate - The date to start the rule from
* @returns {RRule} RRule that will repeat monthly on the same day of the month as the given start date
*/
export const generateMonthlyRRule = startDate => {
const dayOfMonth = startDate.getDate();
const utcDate = new Date(startDate);

if (dayOfMonth <= 28) {
const dayOfMonth = utcDate.getDate();
const numDaysInMonth = getDaysInMonth(utcDate);

if (dayOfMonth < 28) {
return new RRule({
freq: RRule.MONTHLY,
dtstart: startDate,
dtstart: utcDate,
interval: 1,
bymonthday: [dayOfMonth],
});
}

// If the day of the month is the last day of the month, return a rule that will be applied to the last day of the month every month
if (isLastDayOfMonth(startDate)) {
if (dayOfMonth === numDaysInMonth) {
return new RRule({
freq: RRule.MONTHLY,
dtstart: startDate,
dtstart: utcDate,
interval: 1,
bymonthday: [-1],
});
Expand All @@ -85,7 +88,7 @@ export const generateMonthlyRRule = startDate => {

return new RRule({
freq: RRule.MONTHLY,
dtstart: startDate,
dtstart: utcDate,
interval: 1,
bymonthday,
// This will get the last available date from the bymonthday array. For example, if the dayOfMonth is 30, the bymonthday array will be [28, 29, 30] and the bysetpos will be -1, which will get the 30th day of the month if it exists, otherwise the 28th or 29th (for February)
Expand All @@ -95,13 +98,13 @@ export const generateMonthlyRRule = startDate => {

/**
*
* @param {Date} startDate
* @param {string | Date} startDate - The date to start the rule from
* @returns {RRule} RRule that will repeat yearly on the same day of the year as the given start date
*/
export const generateYearlyRRule = startDate => {
return new RRule({
freq: RRule.YEARLY,
dtstart: startDate,
dtstart: new Date(startDate),
interval: 1,
});
};
Expand Down

0 comments on commit 7de581f

Please sign in to comment.