mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-28 18:40:34 -05:00
add prompt to convert future transactions to single time schedules (#6065)
This commit is contained in:
@@ -10,6 +10,7 @@ packages/desktop-client/bundle.browser.js
|
||||
packages/desktop-client/stats.json
|
||||
packages/desktop-client/.swc/
|
||||
packages/desktop-client/build/
|
||||
packages/desktop-client/dev-dist/
|
||||
packages/desktop-client/locale/
|
||||
packages/desktop-client/build-electron/
|
||||
packages/desktop-client/build-stats/
|
||||
@@ -26,5 +27,7 @@ packages/loot-core/**/node_modules/*
|
||||
packages/loot-core/**/lib-dist/*
|
||||
packages/loot-core/**/proto/*
|
||||
packages/sync-server/coverage/
|
||||
packages/sync-server/user-files/
|
||||
packages/sync-server/server-files/
|
||||
.yarn/*
|
||||
upcoming-release-notes/*
|
||||
|
||||
@@ -84,6 +84,7 @@ export default defineConfig(
|
||||
'packages/component-library/src/icons/**/*',
|
||||
'packages/desktop-client/bundle.browser.js',
|
||||
'packages/desktop-client/build/',
|
||||
'packages/desktop-client/dev-dist/',
|
||||
'packages/desktop-client/service-worker/*',
|
||||
'packages/desktop-client/build-electron/',
|
||||
'packages/desktop-client/build-stats/',
|
||||
@@ -100,6 +101,8 @@ export default defineConfig(
|
||||
'packages/loot-core/**/lib-dist/*',
|
||||
'packages/loot-core/**/proto/*',
|
||||
'packages/sync-server/build/',
|
||||
'packages/sync-server/user-files/',
|
||||
'packages/sync-server/server-files/',
|
||||
'packages/plugins-service/dist/',
|
||||
'.yarn/*',
|
||||
'.github/*',
|
||||
|
||||
@@ -19,6 +19,7 @@ import { ConfirmCategoryDeleteModal } from './modals/ConfirmCategoryDeleteModal'
|
||||
import { ConfirmDeleteModal } from './modals/ConfirmDeleteModal';
|
||||
import { ConfirmTransactionEditModal } from './modals/ConfirmTransactionEditModal';
|
||||
import { ConfirmUnlinkAccountModal } from './modals/ConfirmUnlinkAccountModal';
|
||||
import { ConvertToScheduleModal } from './modals/ConvertToScheduleModal';
|
||||
import { CoverModal } from './modals/CoverModal';
|
||||
import { CreateAccountModal } from './modals/CreateAccountModal';
|
||||
import { CreateEncryptionKeyModal } from './modals/CreateEncryptionKeyModal';
|
||||
@@ -140,6 +141,9 @@ export function Modals() {
|
||||
case 'confirm-transaction-edit':
|
||||
return <ConfirmTransactionEditModal key={key} {...modal.options} />;
|
||||
|
||||
case 'convert-to-schedule':
|
||||
return <ConvertToScheduleModal key={key} {...modal.options} />;
|
||||
|
||||
case 'confirm-delete':
|
||||
return <ConfirmDeleteModal key={key} {...modal.options} />;
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ import { send } from 'loot-core/platform/client/fetch';
|
||||
import * as monthUtils from 'loot-core/shared/months';
|
||||
import * as Platform from 'loot-core/shared/platform';
|
||||
import { q } from 'loot-core/shared/query';
|
||||
import { getStatusLabel } from 'loot-core/shared/schedules';
|
||||
import { getStatusLabel, getUpcomingDays } from 'loot-core/shared/schedules';
|
||||
import {
|
||||
ungroupTransactions,
|
||||
updateTransaction,
|
||||
@@ -65,6 +65,7 @@ import {
|
||||
} from '@desktop-client/components/mobile/MobileForms';
|
||||
import { getPrettyPayee } from '@desktop-client/components/mobile/utils';
|
||||
import { MobilePageHeader, Page } from '@desktop-client/components/Page';
|
||||
import { createSingleTimeScheduleFromTransaction } from '@desktop-client/components/transactions/TransactionList';
|
||||
import { AmountInput } from '@desktop-client/components/util/AmountInput';
|
||||
import { useAccounts } from '@desktop-client/hooks/useAccounts';
|
||||
import { useCategories } from '@desktop-client/hooks/useCategories';
|
||||
@@ -77,7 +78,9 @@ import {
|
||||
SingleActiveEditFormProvider,
|
||||
useSingleActiveEditForm,
|
||||
} from '@desktop-client/hooks/useSingleActiveEditForm';
|
||||
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
|
||||
import { pushModal } from '@desktop-client/modals/modalsSlice';
|
||||
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
|
||||
import { aqlQuery } from '@desktop-client/queries/aqlQuery';
|
||||
import { useSelector, useDispatch } from '@desktop-client/redux';
|
||||
import { setLastTransaction } from '@desktop-client/transactions/transactionsSlice';
|
||||
@@ -488,6 +491,9 @@ const TransactionEditInner = memo(function TransactionEditInner({
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const [showHiddenCategories] = useLocalPref('budget.showHiddenCategories');
|
||||
const [upcomingLength = '7'] = useSyncedPref(
|
||||
'upcomingScheduledTransactionLength',
|
||||
);
|
||||
const transactions = useMemo(
|
||||
() =>
|
||||
unserializedTransactions.map(t => serializeTransaction(t, dateFormat)) ||
|
||||
@@ -589,6 +595,69 @@ const TransactionEditInner = memo(function TransactionEditInner({
|
||||
navigate(-1);
|
||||
};
|
||||
|
||||
const today = monthUtils.currentDay();
|
||||
const isFuture = unserializedTransaction.date > today;
|
||||
|
||||
if (isFuture) {
|
||||
const upcomingDays = getUpcomingDays(upcomingLength, today);
|
||||
const daysUntilTransaction = monthUtils.differenceInCalendarDays(
|
||||
unserializedTransaction.date,
|
||||
today,
|
||||
);
|
||||
const isBeyondWindow = daysUntilTransaction > upcomingDays;
|
||||
|
||||
dispatch(
|
||||
pushModal({
|
||||
modal: {
|
||||
name: 'convert-to-schedule',
|
||||
options: {
|
||||
isBeyondWindow,
|
||||
daysUntilTransaction,
|
||||
upcomingDays,
|
||||
onConfirm: async () => {
|
||||
if (
|
||||
!isAdding &&
|
||||
unserializedTransaction.id &&
|
||||
!unserializedTransaction.id.startsWith('temp')
|
||||
) {
|
||||
await send('transaction-delete', {
|
||||
id: unserializedTransaction.id,
|
||||
});
|
||||
}
|
||||
|
||||
const transactionForSchedule = unserializedTransaction.is_parent
|
||||
? {
|
||||
...unserializedTransaction,
|
||||
subtransactions: unserializedTransactions.filter(
|
||||
t =>
|
||||
t.is_child &&
|
||||
t.parent_id === unserializedTransaction.id,
|
||||
),
|
||||
}
|
||||
: unserializedTransaction;
|
||||
|
||||
await createSingleTimeScheduleFromTransaction(
|
||||
transactionForSchedule,
|
||||
);
|
||||
|
||||
dispatch(
|
||||
addNotification({
|
||||
notification: {
|
||||
type: 'message',
|
||||
message: t('Schedule created successfully'),
|
||||
},
|
||||
}),
|
||||
);
|
||||
navigate(-1);
|
||||
},
|
||||
onCancel: onConfirmSave,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (unserializedTransaction.reconciled) {
|
||||
// On mobile any save gives the warning.
|
||||
// On the web only certain changes trigger a warning.
|
||||
@@ -608,7 +677,15 @@ const TransactionEditInner = memo(function TransactionEditInner({
|
||||
} else {
|
||||
onConfirmSave();
|
||||
}
|
||||
}, [isAdding, dispatch, navigate, onSave, unserializedTransactions]);
|
||||
}, [
|
||||
isAdding,
|
||||
dispatch,
|
||||
navigate,
|
||||
onSave,
|
||||
unserializedTransactions,
|
||||
upcomingLength,
|
||||
t,
|
||||
]);
|
||||
|
||||
const onUpdateInner = useCallback(
|
||||
async (serializedTransaction, name, value) => {
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import React from 'react';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
|
||||
import { Block } from '@actual-app/components/block';
|
||||
import { Button } from '@actual-app/components/button';
|
||||
import { useResponsive } from '@actual-app/components/hooks/useResponsive';
|
||||
import { InitialFocus } from '@actual-app/components/initial-focus';
|
||||
import { styles } from '@actual-app/components/styles';
|
||||
import { theme } from '@actual-app/components/theme';
|
||||
import { View } from '@actual-app/components/view';
|
||||
|
||||
import {
|
||||
Modal,
|
||||
ModalCloseButton,
|
||||
ModalHeader,
|
||||
} from '@desktop-client/components/common/Modal';
|
||||
import { type Modal as ModalType } from '@desktop-client/modals/modalsSlice';
|
||||
|
||||
type ConvertToScheduleModalProps = Extract<
|
||||
ModalType,
|
||||
{ name: 'convert-to-schedule' }
|
||||
>['options'];
|
||||
|
||||
export function ConvertToScheduleModal({
|
||||
onCancel,
|
||||
onConfirm,
|
||||
isBeyondWindow,
|
||||
daysUntilTransaction,
|
||||
upcomingDays,
|
||||
}: ConvertToScheduleModalProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { isNarrowWidth } = useResponsive();
|
||||
const narrowButtonStyle = isNarrowWidth
|
||||
? {
|
||||
height: styles.mobileMinHeight,
|
||||
}
|
||||
: {};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
name="convert-to-schedule"
|
||||
containerProps={{ style: { width: '30vw' } }}
|
||||
>
|
||||
{({ state: { close } }) => (
|
||||
<>
|
||||
<ModalHeader
|
||||
title={t('Convert to Schedule')}
|
||||
rightContent={<ModalCloseButton onPress={close} />}
|
||||
/>
|
||||
<View style={{ lineHeight: 1.5 }}>
|
||||
<Block>
|
||||
<Trans>
|
||||
This transaction has a future date. Would you like to convert it
|
||||
to a single-time schedule instead?
|
||||
</Trans>
|
||||
</Block>
|
||||
{isBeyondWindow ? (
|
||||
<Block
|
||||
style={{
|
||||
marginTop: 10,
|
||||
padding: 10,
|
||||
backgroundColor: theme.warningBackground,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
<Trans>
|
||||
<strong>Warning:</strong> This transaction is{' '}
|
||||
{{ daysUntilTransaction }} days away, which is beyond your
|
||||
configured upcoming length of {{ upcomingDays }} days. The
|
||||
schedule preview will not be visible in your account until it
|
||||
gets closer to the date.
|
||||
</Trans>
|
||||
</Block>
|
||||
) : (
|
||||
<Block style={{ marginTop: 10 }}>
|
||||
<Trans>
|
||||
The transaction will appear as a schedule preview in your
|
||||
account.
|
||||
</Trans>
|
||||
</Block>
|
||||
)}
|
||||
<View
|
||||
style={{
|
||||
marginTop: 20,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
aria-label={t('Cancel')}
|
||||
style={{
|
||||
marginRight: 10,
|
||||
...narrowButtonStyle,
|
||||
...(isNarrowWidth && { flex: 1 }),
|
||||
}}
|
||||
onPress={() => {
|
||||
close();
|
||||
onCancel?.();
|
||||
}}
|
||||
>
|
||||
<Trans>No, keep as transaction</Trans>
|
||||
</Button>
|
||||
<InitialFocus>
|
||||
<Button
|
||||
aria-label={t('Convert to Schedule')}
|
||||
variant="primary"
|
||||
style={{
|
||||
...narrowButtonStyle,
|
||||
...(isNarrowWidth && { flex: 1 }),
|
||||
}}
|
||||
onPress={() => {
|
||||
close();
|
||||
onConfirm();
|
||||
}}
|
||||
>
|
||||
<Trans>Yes, create schedule</Trans>
|
||||
</Button>
|
||||
</InitialFocus>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
// @ts-strict-ignore
|
||||
// TODO: remove strict
|
||||
import { useCallback, useLayoutEffect, useRef, type RefObject } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { theme } from '@actual-app/components/theme';
|
||||
|
||||
import { send } from 'loot-core/platform/client/fetch';
|
||||
import * as monthUtils from 'loot-core/shared/months';
|
||||
import { q } from 'loot-core/shared/query';
|
||||
import { getUpcomingDays } from 'loot-core/shared/schedules';
|
||||
import {
|
||||
addSplitTransaction,
|
||||
applyTransactionDiff,
|
||||
@@ -17,6 +21,7 @@ import {
|
||||
type AccountEntity,
|
||||
type CategoryEntity,
|
||||
type PayeeEntity,
|
||||
type RuleActionEntity,
|
||||
type RuleConditionEntity,
|
||||
type ScheduleEntity,
|
||||
type TransactionEntity,
|
||||
@@ -78,6 +83,157 @@ async function saveDiffAndApply(diff, changes, onChange, learnCategories) {
|
||||
);
|
||||
}
|
||||
|
||||
export async function createSingleTimeScheduleFromTransaction(
|
||||
transaction: TransactionEntity,
|
||||
): Promise<ScheduleEntity['id']> {
|
||||
const conditions: RuleConditionEntity[] = [
|
||||
{ op: 'is', field: 'date', value: transaction.date },
|
||||
];
|
||||
|
||||
const actions: RuleActionEntity[] = [];
|
||||
|
||||
const conditionFields = ['amount', 'payee', 'account'];
|
||||
|
||||
conditionFields.forEach(field => {
|
||||
const value = transaction[field];
|
||||
if (value != null && value !== '') {
|
||||
conditions.push({
|
||||
op: 'is',
|
||||
field,
|
||||
value,
|
||||
} as RuleConditionEntity);
|
||||
}
|
||||
});
|
||||
|
||||
if (transaction.is_parent && transaction.subtransactions) {
|
||||
if (transaction.notes) {
|
||||
actions.push({
|
||||
op: 'set',
|
||||
field: 'notes',
|
||||
value: transaction.notes,
|
||||
options: {
|
||||
splitIndex: 0,
|
||||
},
|
||||
} as RuleActionEntity);
|
||||
}
|
||||
|
||||
transaction.subtransactions.forEach((split, index) => {
|
||||
const splitIndex = index + 1;
|
||||
|
||||
if (split.amount != null) {
|
||||
actions.push({
|
||||
op: 'set-split-amount',
|
||||
value: split.amount,
|
||||
options: {
|
||||
splitIndex,
|
||||
method: 'fixed-amount',
|
||||
},
|
||||
} as RuleActionEntity);
|
||||
}
|
||||
|
||||
if (split.category) {
|
||||
actions.push({
|
||||
op: 'set',
|
||||
field: 'category',
|
||||
value: split.category,
|
||||
options: {
|
||||
splitIndex,
|
||||
},
|
||||
} as RuleActionEntity);
|
||||
}
|
||||
|
||||
if (split.notes) {
|
||||
actions.push({
|
||||
op: 'set',
|
||||
field: 'notes',
|
||||
value: split.notes,
|
||||
options: {
|
||||
splitIndex,
|
||||
},
|
||||
} as RuleActionEntity);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (transaction.category) {
|
||||
actions.push({
|
||||
op: 'set',
|
||||
field: 'category',
|
||||
value: transaction.category,
|
||||
} as RuleActionEntity);
|
||||
}
|
||||
|
||||
if (transaction.notes) {
|
||||
actions.push({
|
||||
op: 'set',
|
||||
field: 'notes',
|
||||
value: transaction.notes,
|
||||
} as RuleActionEntity);
|
||||
}
|
||||
}
|
||||
|
||||
const formattedDate = monthUtils.format(transaction.date, 'MMM dd, yyyy');
|
||||
const timestamp = Date.now();
|
||||
const scheduleName = `Auto-created future transaction (${formattedDate}) - ${timestamp}`;
|
||||
|
||||
const scheduleId = await send('schedule/create', {
|
||||
conditions,
|
||||
schedule: {
|
||||
posts_transaction: true,
|
||||
name: scheduleName,
|
||||
},
|
||||
});
|
||||
|
||||
if (actions.length > 0) {
|
||||
const schedules = await send(
|
||||
'query',
|
||||
q('schedules').filter({ id: scheduleId }).select('rule').serialize(),
|
||||
);
|
||||
|
||||
const ruleId = schedules?.data?.[0]?.rule;
|
||||
|
||||
if (ruleId) {
|
||||
const rule = await send('rule-get', { id: ruleId });
|
||||
|
||||
if (rule) {
|
||||
const linkScheduleActions = rule.actions.filter(
|
||||
a => a.op === 'link-schedule',
|
||||
);
|
||||
|
||||
await send('rule-update', {
|
||||
...rule,
|
||||
actions: [...linkScheduleActions, ...actions],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return scheduleId;
|
||||
}
|
||||
|
||||
function isFutureTransaction(transaction: TransactionEntity): boolean {
|
||||
const today = monthUtils.currentDay();
|
||||
return transaction.date > today;
|
||||
}
|
||||
|
||||
function calculateFutureTransactionInfo(
|
||||
transaction: TransactionEntity,
|
||||
upcomingLength: string,
|
||||
) {
|
||||
const today = monthUtils.currentDay();
|
||||
const upcomingDays = getUpcomingDays(upcomingLength, today);
|
||||
const daysUntilTransaction = monthUtils.differenceInCalendarDays(
|
||||
transaction.date,
|
||||
today,
|
||||
);
|
||||
const isBeyondWindow = daysUntilTransaction > upcomingDays;
|
||||
|
||||
return {
|
||||
isBeyondWindow,
|
||||
daysUntilTransaction,
|
||||
upcomingDays,
|
||||
};
|
||||
}
|
||||
|
||||
type TransactionListProps = Pick<
|
||||
TransactionTableProps,
|
||||
| 'accounts'
|
||||
@@ -164,54 +320,152 @@ export function TransactionList({
|
||||
onScheduleAction,
|
||||
onMakeAsNonSplitTransactions,
|
||||
}: TransactionListProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const [learnCategories = 'true'] = useSyncedPref('learn-categories');
|
||||
const isLearnCategoriesEnabled = String(learnCategories) === 'true';
|
||||
const [upcomingLength = '7'] = useSyncedPref(
|
||||
'upcomingScheduledTransactionLength',
|
||||
);
|
||||
|
||||
const transactionsLatest = useRef<readonly TransactionEntity[]>([]);
|
||||
useLayoutEffect(() => {
|
||||
transactionsLatest.current = transactions;
|
||||
}, [transactions]);
|
||||
|
||||
const promptToConvertToSchedule = useCallback(
|
||||
(
|
||||
transaction: TransactionEntity,
|
||||
onConfirm: () => Promise<void>,
|
||||
onCancel: () => Promise<void>,
|
||||
) => {
|
||||
const futureInfo = calculateFutureTransactionInfo(
|
||||
transaction,
|
||||
upcomingLength,
|
||||
);
|
||||
|
||||
dispatch(
|
||||
pushModal({
|
||||
modal: {
|
||||
name: 'convert-to-schedule',
|
||||
options: {
|
||||
...futureInfo,
|
||||
onConfirm: async () => {
|
||||
await onConfirm();
|
||||
dispatch(
|
||||
addNotification({
|
||||
notification: {
|
||||
type: 'message',
|
||||
message: t('Schedule created successfully'),
|
||||
},
|
||||
}),
|
||||
);
|
||||
onRefetch();
|
||||
},
|
||||
onCancel: async () => {
|
||||
await onCancel();
|
||||
onRefetch();
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
[dispatch, onRefetch, upcomingLength, t],
|
||||
);
|
||||
|
||||
const onAdd = useCallback(
|
||||
async (newTransactions: TransactionEntity[]) => {
|
||||
newTransactions = realizeTempTransactions(newTransactions);
|
||||
|
||||
const parentTransaction = newTransactions.find(t => !t.is_child);
|
||||
|
||||
if (parentTransaction && isFutureTransaction(parentTransaction)) {
|
||||
const transactionWithSubtransactions = {
|
||||
...parentTransaction,
|
||||
subtransactions: newTransactions.filter(
|
||||
t => t.is_child && t.parent_id === parentTransaction.id,
|
||||
),
|
||||
};
|
||||
|
||||
promptToConvertToSchedule(
|
||||
transactionWithSubtransactions,
|
||||
async () => {
|
||||
await createSingleTimeScheduleFromTransaction(
|
||||
transactionWithSubtransactions,
|
||||
);
|
||||
},
|
||||
async () => {
|
||||
await saveDiff(
|
||||
{ added: newTransactions },
|
||||
isLearnCategoriesEnabled,
|
||||
);
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await saveDiff({ added: newTransactions }, isLearnCategoriesEnabled);
|
||||
onRefetch();
|
||||
},
|
||||
[isLearnCategoriesEnabled, onRefetch],
|
||||
[isLearnCategoriesEnabled, onRefetch, promptToConvertToSchedule],
|
||||
);
|
||||
|
||||
const onSave = useCallback(
|
||||
async (transaction: TransactionEntity) => {
|
||||
const changes = updateTransaction(
|
||||
transactionsLatest.current,
|
||||
transaction,
|
||||
);
|
||||
transactionsLatest.current = changes.data;
|
||||
const saveTransaction = async () => {
|
||||
const changes = updateTransaction(
|
||||
transactionsLatest.current,
|
||||
transaction,
|
||||
);
|
||||
transactionsLatest.current = changes.data;
|
||||
|
||||
if (changes.diff.updated.length > 0) {
|
||||
const dateChanged = !!changes.diff.updated[0].date;
|
||||
if (dateChanged) {
|
||||
// Make sure it stays at the top of the list of transactions
|
||||
// for that date
|
||||
changes.diff.updated[0].sort_order = Date.now();
|
||||
await saveDiff(changes.diff, isLearnCategoriesEnabled);
|
||||
onRefetch();
|
||||
} else {
|
||||
onChange(changes.newTransaction, changes.data);
|
||||
saveDiffAndApply(
|
||||
changes.diff,
|
||||
changes,
|
||||
onChange,
|
||||
isLearnCategoriesEnabled,
|
||||
if (changes.diff.updated.length > 0) {
|
||||
const dateChanged = !!changes.diff.updated[0].date;
|
||||
if (dateChanged) {
|
||||
changes.diff.updated[0].sort_order = Date.now();
|
||||
await saveDiff(changes.diff, isLearnCategoriesEnabled);
|
||||
onRefetch();
|
||||
} else {
|
||||
onChange(changes.newTransaction, changes.data);
|
||||
saveDiffAndApply(
|
||||
changes.diff,
|
||||
changes,
|
||||
onChange,
|
||||
isLearnCategoriesEnabled,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (isFutureTransaction(transaction)) {
|
||||
const originalTransaction = transactionsLatest.current.find(
|
||||
t => t.id === transaction.id,
|
||||
);
|
||||
const dateChanged =
|
||||
!originalTransaction || originalTransaction.date !== transaction.date;
|
||||
|
||||
if (dateChanged || !originalTransaction) {
|
||||
promptToConvertToSchedule(
|
||||
transaction,
|
||||
async () => {
|
||||
if (transaction.id && !transaction.id.startsWith('temp')) {
|
||||
await send('transaction-delete', { id: transaction.id });
|
||||
}
|
||||
|
||||
await createSingleTimeScheduleFromTransaction(transaction);
|
||||
},
|
||||
saveTransaction,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await saveTransaction();
|
||||
},
|
||||
[isLearnCategoriesEnabled, onChange, onRefetch],
|
||||
[isLearnCategoriesEnabled, onChange, onRefetch, promptToConvertToSchedule],
|
||||
);
|
||||
|
||||
const onAddSplit = useCallback(
|
||||
|
||||
@@ -482,6 +482,16 @@ export type Modal =
|
||||
confirmReason: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
name: 'convert-to-schedule';
|
||||
options: {
|
||||
onConfirm: () => void;
|
||||
onCancel?: () => void;
|
||||
isBeyondWindow?: boolean;
|
||||
daysUntilTransaction?: number;
|
||||
upcomingDays?: number;
|
||||
};
|
||||
}
|
||||
| {
|
||||
name: 'confirm-delete';
|
||||
options: {
|
||||
|
||||
6
upcoming-release-notes/6065.md
Normal file
6
upcoming-release-notes/6065.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Features
|
||||
authors: [matt-fidd]
|
||||
---
|
||||
|
||||
Add prompt to convert future transactions to single time schedules
|
||||
Reference in New Issue
Block a user