mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-27 17:48:17 -05:00
add option to complete non-recurring schedules from transaction menu (#4180)
This commit is contained in:
@@ -131,18 +131,22 @@ export function ManageRules({
|
||||
[payees, accounts, schedules, categories],
|
||||
);
|
||||
|
||||
const filteredRules = useMemo(
|
||||
() =>
|
||||
(filter === ''
|
||||
? allRules
|
||||
: allRules.filter(rule =>
|
||||
const filteredRules = useMemo(() => {
|
||||
const rules = allRules.filter(rule => {
|
||||
const schedule = schedules.find(schedule => schedule.rule === rule.id);
|
||||
return schedule ? schedule.completed === false : true;
|
||||
});
|
||||
|
||||
return (
|
||||
filter === ''
|
||||
? rules
|
||||
: rules.filter(rule =>
|
||||
getNormalisedString(ruleToString(rule, filterData)).includes(
|
||||
getNormalisedString(filter),
|
||||
),
|
||||
)
|
||||
).slice(0, 100 + page * 50),
|
||||
[allRules, filter, filterData, page],
|
||||
);
|
||||
).slice(0, 100 + page * 50);
|
||||
}, [allRules, filter, filterData, page]);
|
||||
const selectedInst = useSelected('manage-rules', allRules, []);
|
||||
const [hoveredRule, setHoveredRule] = useState(null);
|
||||
|
||||
|
||||
@@ -555,6 +555,7 @@ export function Modals() {
|
||||
transactionId={options.transactionId}
|
||||
onPost={options.onPost}
|
||||
onSkip={options.onSkip}
|
||||
onComplete={options.onComplete}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -1497,21 +1497,26 @@ class AccountInternal extends PureComponent<
|
||||
};
|
||||
|
||||
onScheduleAction = async (
|
||||
name: 'skip' | 'post-transaction',
|
||||
name: 'skip' | 'post-transaction' | 'complete',
|
||||
ids: string[],
|
||||
) => {
|
||||
const scheduleIds = ids.map(id => id.split('/')[1]);
|
||||
|
||||
switch (name) {
|
||||
case 'post-transaction':
|
||||
for (const id of ids) {
|
||||
const parts = id.split('/');
|
||||
await send('schedule/post-transaction', { id: parts[1] });
|
||||
for (const id of scheduleIds) {
|
||||
await send('schedule/post-transaction', { id });
|
||||
}
|
||||
this.refetchTransactions();
|
||||
break;
|
||||
case 'skip':
|
||||
for (const id of ids) {
|
||||
const parts = id.split('/');
|
||||
await send('schedule/skip-next-date', { id: parts[1] });
|
||||
for (const id of scheduleIds) {
|
||||
await send('schedule/skip-next-date', { id });
|
||||
}
|
||||
break;
|
||||
case 'complete':
|
||||
for (const id of scheduleIds) {
|
||||
await send('schedule/update', { schedule: { id, completed: true } });
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -306,6 +306,13 @@ function TransactionListWithPreviews({
|
||||
await send('schedule/skip-next-date', { id: parts[1] });
|
||||
dispatch(collapseModals('scheduled-transaction-menu'));
|
||||
},
|
||||
onComplete: async transactionId => {
|
||||
const parts = transactionId.split('/');
|
||||
await send('schedule/update', {
|
||||
schedule: { id: parts[1], completed: true },
|
||||
});
|
||||
dispatch(collapseModals('scheduled-transaction-menu'));
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useSchedules } from 'loot-core/client/data-hooks/schedules';
|
||||
import { format } from 'loot-core/shared/months';
|
||||
import { q } from 'loot-core/shared/query';
|
||||
import {
|
||||
scheduleIsRecurring,
|
||||
extractScheduleConds,
|
||||
} from 'loot-core/shared/schedules';
|
||||
|
||||
import { theme, styles } from '../../style';
|
||||
import { Menu } from '../common/Menu';
|
||||
@@ -26,6 +30,7 @@ export function ScheduledTransactionMenuModal({
|
||||
transactionId,
|
||||
onSkip,
|
||||
onPost,
|
||||
onComplete,
|
||||
}: ScheduledTransactionMenuModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const defaultMenuItemStyle: CSSProperties = {
|
||||
@@ -48,6 +53,10 @@ export function ScheduledTransactionMenuModal({
|
||||
}
|
||||
|
||||
const schedule = schedules?.[0];
|
||||
const { date: dateCond } = extractScheduleConds(schedule._conditions);
|
||||
|
||||
const canBeSkipped = scheduleIsRecurring(dateCond);
|
||||
const canBeCompleted = !scheduleIsRecurring(dateCond);
|
||||
|
||||
return (
|
||||
<Modal name="scheduled-transaction-menu">
|
||||
@@ -75,6 +84,9 @@ export function ScheduledTransactionMenuModal({
|
||||
transactionId={transactionId}
|
||||
onPost={onPost}
|
||||
onSkip={onSkip}
|
||||
onComplete={onComplete}
|
||||
canBeSkipped={canBeSkipped}
|
||||
canBeCompleted={canBeCompleted}
|
||||
getItemStyle={() => defaultMenuItemStyle}
|
||||
/>
|
||||
</>
|
||||
@@ -90,15 +102,23 @@ type ScheduledTransactionMenuProps = Omit<
|
||||
transactionId: string;
|
||||
onSkip: (transactionId: string) => void;
|
||||
onPost: (transactionId: string) => void;
|
||||
onComplete: (transactionId: string) => void;
|
||||
};
|
||||
|
||||
function ScheduledTransactionMenu({
|
||||
transactionId,
|
||||
onSkip,
|
||||
onPost,
|
||||
onComplete,
|
||||
canBeSkipped,
|
||||
canBeCompleted,
|
||||
...props
|
||||
}: ScheduledTransactionMenuProps) {
|
||||
}: ScheduledTransactionMenuProps & {
|
||||
canBeCompleted: boolean;
|
||||
canBeSkipped: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Menu
|
||||
{...props}
|
||||
@@ -110,19 +130,21 @@ function ScheduledTransactionMenu({
|
||||
case 'skip':
|
||||
onSkip?.(transactionId);
|
||||
break;
|
||||
case 'complete':
|
||||
onComplete?.(transactionId);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unrecognized menu option: ${name}`);
|
||||
}
|
||||
}}
|
||||
items={[
|
||||
{
|
||||
name: 'post',
|
||||
text: t('Post transaction today'),
|
||||
},
|
||||
{
|
||||
name: 'skip',
|
||||
text: t('Skip scheduled date'),
|
||||
},
|
||||
{ name: 'post', text: t('Post transaction today') },
|
||||
...(canBeSkipped
|
||||
? [{ name: 'skip', text: t('Skip next scheduled date') }]
|
||||
: []),
|
||||
...(canBeCompleted
|
||||
? [{ name: 'complete', text: t('Mark as completed') }]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -3,8 +3,14 @@ import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { pushModal } from 'loot-core/client/actions';
|
||||
import {
|
||||
scheduleIsRecurring,
|
||||
extractScheduleConds,
|
||||
} from 'loot-core/shared/schedules';
|
||||
import { isPreviewId } from 'loot-core/shared/transactions';
|
||||
import { useSchedules } from 'loot-core/src/client/data-hooks/schedules';
|
||||
import { validForTransfer } from 'loot-core/src/client/transfer';
|
||||
import { q } from 'loot-core/src/shared/query';
|
||||
import { type TransactionEntity } from 'loot-core/types/models';
|
||||
|
||||
import { useSelectedItems } from '../../hooks/useSelected';
|
||||
@@ -34,7 +40,7 @@ type SelectedTransactionsButtonProps = {
|
||||
onRunRules: (selectedIds: string[]) => void;
|
||||
onSetTransfer: (selectedIds: string[]) => void;
|
||||
onScheduleAction: (
|
||||
action: 'post-transaction' | 'skip',
|
||||
action: 'post-transaction' | 'skip' | 'complete',
|
||||
selectedIds: string[],
|
||||
) => void;
|
||||
showMakeTransfer: boolean;
|
||||
@@ -63,6 +69,22 @@ export function SelectedTransactionsButton({
|
||||
const selectedItems = useSelectedItems();
|
||||
const selectedIds = useMemo(() => [...selectedItems], [selectedItems]);
|
||||
|
||||
const scheduleIds = useMemo(() => {
|
||||
return selectedIds
|
||||
.filter(id => isPreviewId(id))
|
||||
.map(id => id.split('/')[1]);
|
||||
}, [selectedIds]);
|
||||
|
||||
const scheduleQuery = useMemo(() => {
|
||||
return q('schedules')
|
||||
.filter({ id: { $oneof: scheduleIds } })
|
||||
.select('*');
|
||||
}, [scheduleIds]);
|
||||
|
||||
const { schedules: selectedSchedules } = useSchedules({
|
||||
query: scheduleQuery,
|
||||
});
|
||||
|
||||
const types = useMemo(() => {
|
||||
const items = selectedIds;
|
||||
return {
|
||||
@@ -103,6 +125,24 @@ export function SelectedTransactionsButton({
|
||||
return validForTransfer(fromTrans, toTrans);
|
||||
}, [selectedIds, getTransaction]);
|
||||
|
||||
const canBeSkipped = useMemo(() => {
|
||||
const recurringSchedules = selectedSchedules.filter(s => {
|
||||
const { date: dateCond } = extractScheduleConds(s._conditions);
|
||||
return scheduleIsRecurring(dateCond);
|
||||
});
|
||||
|
||||
return recurringSchedules.length === selectedSchedules.length;
|
||||
}, [selectedSchedules]);
|
||||
|
||||
const canBeCompleted = useMemo(() => {
|
||||
const singleSchedules = selectedSchedules.filter(s => {
|
||||
const { date: dateCond } = extractScheduleConds(s._conditions);
|
||||
return !scheduleIsRecurring(dateCond);
|
||||
});
|
||||
|
||||
return singleSchedules.length === selectedSchedules.length;
|
||||
}, [selectedSchedules]);
|
||||
|
||||
const canMakeAsSplitTransaction = useMemo(() => {
|
||||
if (selectedIds.length <= 1 || types.preview) {
|
||||
return false;
|
||||
@@ -222,7 +262,13 @@ export function SelectedTransactionsButton({
|
||||
name: 'post-transaction',
|
||||
text: t('Post transaction today'),
|
||||
} as const,
|
||||
{ name: 'skip', text: t('Skip next scheduled date') } as const,
|
||||
canBeSkipped &&
|
||||
({
|
||||
name: 'skip',
|
||||
text: t('Skip next scheduled date'),
|
||||
} as const),
|
||||
canBeCompleted &&
|
||||
({ name: 'complete', text: t('Mark as completed') } as const),
|
||||
]
|
||||
: [
|
||||
{ name: 'show', text: t('Show'), key: 'F' } as const,
|
||||
@@ -318,6 +364,7 @@ export function SelectedTransactionsButton({
|
||||
break;
|
||||
case 'post-transaction':
|
||||
case 'skip':
|
||||
case 'complete':
|
||||
onScheduleAction(name, selectedIds);
|
||||
break;
|
||||
case 'view-schedule':
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import React, { type ComponentPropsWithoutRef } from 'react';
|
||||
import React, { useMemo, type ComponentPropsWithoutRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { pushModal } from 'loot-core/client/actions';
|
||||
import {
|
||||
scheduleIsRecurring,
|
||||
extractScheduleConds,
|
||||
} from 'loot-core/shared/schedules';
|
||||
import { isPreviewId } from 'loot-core/shared/transactions';
|
||||
import { useSchedules } from 'loot-core/src/client/data-hooks/schedules';
|
||||
import { q } from 'loot-core/src/shared/query';
|
||||
import { type TransactionEntity } from 'loot-core/types/models';
|
||||
|
||||
import { useDispatch } from '../../redux';
|
||||
@@ -43,6 +49,29 @@ export function TransactionMenu({
|
||||
const canUnsplitTransactions =
|
||||
!transaction.reconciled && (transaction.is_parent || transaction.is_child);
|
||||
|
||||
const scheduleId = isPreview ? transaction.id?.split('/')?.[1] : null;
|
||||
const schedulesQuery = useMemo(
|
||||
() => q('schedules').filter({ id: scheduleId }).select('*'),
|
||||
[scheduleId],
|
||||
);
|
||||
const { isLoading: isSchedulesLoading, schedules } = useSchedules({
|
||||
query: schedulesQuery,
|
||||
});
|
||||
|
||||
if (isSchedulesLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let canBeSkipped = false;
|
||||
let canBeCompleted = false;
|
||||
if (isPreview) {
|
||||
const schedule = schedules?.[0];
|
||||
const { date: dateCond } = extractScheduleConds(schedule._conditions);
|
||||
|
||||
canBeSkipped = scheduleIsRecurring(dateCond);
|
||||
canBeCompleted = !scheduleIsRecurring(dateCond);
|
||||
}
|
||||
|
||||
function onViewSchedule() {
|
||||
const firstId = transaction.id;
|
||||
let scheduleId;
|
||||
@@ -74,6 +103,7 @@ export function TransactionMenu({
|
||||
break;
|
||||
case 'post-transaction':
|
||||
case 'skip':
|
||||
case 'complete':
|
||||
onScheduleAction(name, transaction.id);
|
||||
break;
|
||||
case 'view-schedule':
|
||||
@@ -98,7 +128,12 @@ export function TransactionMenu({
|
||||
? [
|
||||
{ name: 'view-schedule', text: t('View schedule') },
|
||||
{ name: 'post-transaction', text: t('Post transaction today') },
|
||||
{ name: 'skip', text: t('Skip next scheduled date') },
|
||||
...(canBeSkipped
|
||||
? [{ name: 'skip', text: t('Skip next scheduled date') }]
|
||||
: []),
|
||||
...(canBeCompleted
|
||||
? [{ name: 'complete', text: t('Mark as completed') }]
|
||||
: []),
|
||||
]
|
||||
: [
|
||||
{
|
||||
|
||||
@@ -292,6 +292,7 @@ type FinanceModals = {
|
||||
transactionId: string;
|
||||
onPost: (transactionId: string) => void;
|
||||
onSkip: (transactionId: string) => void;
|
||||
onComplete: (transactionId: string) => void;
|
||||
};
|
||||
'budget-page-menu': {
|
||||
onAddCategoryGroup: () => void;
|
||||
|
||||
6
upcoming-release-notes/4180.md
Normal file
6
upcoming-release-notes/4180.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [matt-fidd]
|
||||
---
|
||||
|
||||
Add option to complete non-recurring schedules from transaction menu
|
||||
Reference in New Issue
Block a user