diff --git a/packages/desktop-client/src/components/schedules/CustomUpcomingLength.tsx b/packages/desktop-client/src/components/schedules/CustomUpcomingLength.tsx
new file mode 100644
index 00000000000..d84b50c5290
--- /dev/null
+++ b/packages/desktop-client/src/components/schedules/CustomUpcomingLength.tsx
@@ -0,0 +1,58 @@
+import React, { useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import { Input } from '../common/Input';
+import { Select } from '../common/Select';
+
+type CustomUpcomingLengthProps = {
+ onChange: (value: string) => void;
+ tempValue: string;
+};
+
+export function CustomUpcomingLength({
+ onChange,
+ tempValue,
+}: CustomUpcomingLengthProps) {
+ const { t } = useTranslation();
+
+ const options = [
+ { value: 'day', label: t('Days') },
+ { value: 'week', label: t('Weeks') },
+ { value: 'month', label: t('Months') },
+ { value: 'year', label: t('Years') },
+ ];
+
+ let timePeriod = [];
+ if (tempValue === 'custom') {
+ timePeriod = ['1', 'day'];
+ } else {
+ timePeriod = tempValue.split('-');
+ }
+
+ const [numValue, setNumValue] = useState(parseInt(timePeriod[0]));
+ const [unit, setUnit] = useState(timePeriod[1]);
+
+ useEffect(() => {
+ onChange(`${numValue}-${unit}`);
+ }, [numValue, onChange, unit]);
+
+ return (
+
+ setNumValue(parseInt(e.target.value))}
+ defaultValue={numValue || 1}
+ />
+
+ );
+}
diff --git a/packages/desktop-client/src/components/schedules/UpcomingLength.tsx b/packages/desktop-client/src/components/schedules/UpcomingLength.tsx
index 5195110eee3..7ca665b6112 100644
--- a/packages/desktop-client/src/components/schedules/UpcomingLength.tsx
+++ b/packages/desktop-client/src/components/schedules/UpcomingLength.tsx
@@ -1,14 +1,17 @@
-import React from 'react';
+import React, { useEffect, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { type SyncedPrefs } from 'loot-core/types/prefs';
import { useSyncedPref } from '../../hooks/useSyncedPref';
+import { Button } from '../common/Button2';
import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal';
import { Paragraph } from '../common/Paragraph';
import { Select } from '../common/Select';
import { View } from '../common/View';
+import { CustomUpcomingLength } from './CustomUpcomingLength';
+
function useUpcomingLengthOptions() {
const { t } = useTranslation();
@@ -20,22 +23,48 @@ function useUpcomingLengthOptions() {
{ value: '7', label: t('1 week') },
{ value: '14', label: t('2 weeks') },
{ value: 'oneMonth', label: t('1 month') },
- { value: 'currentMonth', label: t('end of the current month') },
+ { value: 'currentMonth', label: t('End of the current month') },
+ { value: 'custom', label: t('Custom length') },
];
return { upcomingLengthOptions };
}
+function nonCustomUpcomingLengthValues(value: string) {
+ return (
+ ['1', '7', '14', 'oneMonth', 'currentMonth'].findIndex(x => x === value) ===
+ -1
+ );
+}
+
export function UpcomingLength() {
const { t } = useTranslation();
const [_upcomingLength, setUpcomingLength] = useSyncedPref(
'upcomingScheduledTransactionLength',
);
+ const saveUpcomingLength = () => {
+ setUpcomingLength(tempUpcomingLength);
+ };
+
const { upcomingLengthOptions } = useUpcomingLengthOptions();
const upcomingLength = _upcomingLength || '7';
+ const [tempUpcomingLength, setTempUpcomingLength] = useState(upcomingLength);
+ const [useCustomLength, setUseCustomLength] = useState(
+ nonCustomUpcomingLengthValues(tempUpcomingLength),
+ );
+ const [saveActive, setSaveActive] = useState(false);
+
+ useEffect(() => {
+ if (tempUpcomingLength !== upcomingLength) {
+ setSaveActive(true);
+ } else {
+ setSaveActive(false);
+ }
+ }, [tempUpcomingLength, upcomingLength]);
+
return (
setUpcomingLength(newValue)}
+ value={
+ nonCustomUpcomingLengthValues(tempUpcomingLength)
+ ? 'custom'
+ : tempUpcomingLength
+ }
+ onChange={newValue => {
+ setUseCustomLength(newValue === 'custom');
+ setTempUpcomingLength(newValue);
+ }}
/>
+ {useCustomLength && (
+
+ )}
+
+
+
>
)}
diff --git a/packages/loot-core/src/shared/schedules.test.ts b/packages/loot-core/src/shared/schedules.test.ts
index 67f82e6ce81..69b55ae12ed 100644
--- a/packages/loot-core/src/shared/schedules.test.ts
+++ b/packages/loot-core/src/shared/schedules.test.ts
@@ -1,7 +1,11 @@
import MockDate from 'mockdate';
import * as monthUtils from './months';
-import { getRecurringDescription, getStatus } from './schedules';
+import {
+ getRecurringDescription,
+ getStatus,
+ getUpcomingDays,
+} from './schedules';
describe('schedules', () => {
const today = new Date(2017, 0, 1); // Global date when testing is set to 2017-01-01 per monthUtils.currentDay()
@@ -339,4 +343,20 @@ describe('schedules', () => {
).toBe('Every 2 months on the 17th, until 2021-06-01');
});
});
+
+ describe('getUpcomingDays', () => {
+ it.each([
+ ['1', 1],
+ ['7', 7],
+ ['14', 14],
+ ['oneMonth', 32],
+ ['currentMonth', 31],
+ ['2-day', 2],
+ ['5-week', 35],
+ ['3-month', 91],
+ ['4-year', 1462],
+ ])('value of %s returns %i days', (value: string, expected: number) => {
+ expect(getUpcomingDays(value)).toEqual(expected);
+ });
+ });
});
diff --git a/packages/loot-core/src/shared/schedules.ts b/packages/loot-core/src/shared/schedules.ts
index 43e91e09d47..584f66fe4f5 100644
--- a/packages/loot-core/src/shared/schedules.ts
+++ b/packages/loot-core/src/shared/schedules.ts
@@ -352,6 +352,7 @@ export function describeSchedule(schedule, payee) {
export function getUpcomingDays(upcomingLength = '7'): number {
const today = monthUtils.currentDay();
+ const month = monthUtils.getMonth(today);
switch (upcomingLength) {
case 'currentMonth': {
@@ -360,7 +361,6 @@ export function getUpcomingDays(upcomingLength = '7'): number {
return end - day + 1;
}
case 'oneMonth': {
- const month = monthUtils.getMonth(today);
return (
monthUtils.differenceInCalendarDays(
monthUtils.nextMonth(month),
@@ -369,6 +369,24 @@ export function getUpcomingDays(upcomingLength = '7'): number {
);
}
default:
+ if (upcomingLength.includes('-')) {
+ const [num, unit] = upcomingLength.split('-');
+ const value = Math.max(1, parseInt(num, 10));
+ switch (unit) {
+ case 'day':
+ return value;
+ case 'week':
+ return value * 7;
+ case 'month':
+ const future = monthUtils.addMonths(today, value);
+ return monthUtils.differenceInCalendarDays(future, month) + 1;
+ case 'year':
+ const futureYear = monthUtils.addYears(today, value);
+ return monthUtils.differenceInCalendarDays(futureYear, month) + 1;
+ default:
+ return 7;
+ }
+ }
return parseInt(upcomingLength, 10);
}
}
diff --git a/upcoming-release-notes/4206.md b/upcoming-release-notes/4206.md
new file mode 100644
index 00000000000..e9b1929f737
--- /dev/null
+++ b/upcoming-release-notes/4206.md
@@ -0,0 +1,6 @@
+---
+category: Features
+authors: [ SamBobBarnes ]
+---
+
+Add option for custom upcoming length