Skip to content

Commit

Permalink
add option to complete non-recurring schedules from transaction menu (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
matt-fidd authored Jan 23, 2025
1 parent 5448a5c commit caaa801
Show file tree
Hide file tree
Showing 9 changed files with 156 additions and 28 deletions.
20 changes: 12 additions & 8 deletions packages/desktop-client/src/components/ManageRules.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
1 change: 1 addition & 0 deletions packages/desktop-client/src/components/Modals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,7 @@ export function Modals() {
transactionId={options.transactionId}
onPost={options.onPost}
onSkip={options.onSkip}
onComplete={options.onComplete}
/>
);

Expand Down
19 changes: 12 additions & 7 deletions packages/desktop-client/src/components/accounts/Account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
},
}),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -26,6 +30,7 @@ export function ScheduledTransactionMenuModal({
transactionId,
onSkip,
onPost,
onComplete,
}: ScheduledTransactionMenuModalProps) {
const { t } = useTranslation();
const defaultMenuItemStyle: CSSProperties = {
Expand All @@ -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">
Expand Down Expand Up @@ -75,6 +84,9 @@ export function ScheduledTransactionMenuModal({
transactionId={transactionId}
onPost={onPost}
onSkip={onSkip}
onComplete={onComplete}
canBeSkipped={canBeSkipped}
canBeCompleted={canBeCompleted}
getItemStyle={() => defaultMenuItemStyle}
/>
</>
Expand All @@ -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}
Expand All @@ -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') }]
: []),
]}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -318,6 +364,7 @@ export function SelectedTransactionsButton({
break;
case 'post-transaction':
case 'skip':
case 'complete':
onScheduleAction(name, selectedIds);
break;
case 'view-schedule':
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -74,6 +103,7 @@ export function TransactionMenu({
break;
case 'post-transaction':
case 'skip':
case 'complete':
onScheduleAction(name, transaction.id);
break;
case 'view-schedule':
Expand All @@ -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') }]
: []),
]
: [
{
Expand Down
1 change: 1 addition & 0 deletions packages/loot-core/src/client/state-types/modals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions upcoming-release-notes/4180.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
category: Enhancements
authors: [matt-fidd]
---

Add option to complete non-recurring schedules from transaction menu

0 comments on commit caaa801

Please sign in to comment.