From 29a515f3fe4eef198fea75dfe1856c8de49424ec Mon Sep 17 00:00:00 2001
From: xentara1 <30550215+xentara1@users.noreply.github.com>
Date: Sat, 3 Feb 2024 19:00:27 +0000
Subject: [PATCH] [Feature] Add ability to create schedules from existing
 transactions (#2222)

---
 .../desktop-client/src/components/Modals.tsx  |  3 ++
 .../components/schedules/ScheduleDetails.jsx  | 47 +++++++++++++++----
 .../src/components/schedules/ScheduleLink.tsx | 30 ++++++++++++
 .../transactions/SelectedTransactions.jsx     |  2 +
 .../transactions/SimpleTransactionsTable.jsx  | 10 ++--
 upcoming-release-notes/2222.md                |  6 +++
 6 files changed, 83 insertions(+), 15 deletions(-)
 create mode 100644 upcoming-release-notes/2222.md

diff --git a/packages/desktop-client/src/components/Modals.tsx b/packages/desktop-client/src/components/Modals.tsx
index 5974ec0e2..5163c587f 100644
--- a/packages/desktop-client/src/components/Modals.tsx
+++ b/packages/desktop-client/src/components/Modals.tsx
@@ -310,6 +310,7 @@ export function Modals() {
               modalProps={modalProps}
               id={options?.id || null}
               actions={actions}
+              transaction={options?.transaction || null}
             />
           );
 
@@ -320,6 +321,8 @@ export function Modals() {
               modalProps={modalProps}
               actions={actions}
               transactionIds={options?.transactionIds}
+              getTransaction={options?.getTransaction}
+              pushModal={options?.pushModal}
             />
           );
 
diff --git a/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx b/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx
index 8459e937c..85446055b 100644
--- a/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx
+++ b/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx
@@ -67,14 +67,14 @@ function updateScheduleConditions(schedule, fields) {
   };
 }
 
-export function ScheduleDetails({ modalProps, actions, id }) {
+export function ScheduleDetails({ modalProps, actions, id, transaction }) {
   const adding = id == null;
+  const fromTrans = transaction != null;
   const payees = useCachedPayees({ idKey: true });
   const globalDispatch = useDispatch();
   const dateFormat = useSelector(state => {
     return state.prefs.local.dateFormat || 'MM/dd/yyyy';
   });
-
   const [state, dispatch] = useReducer(
     (state, action) => {
       switch (action.type) {
@@ -143,6 +143,11 @@ export function ScheduleDetails({ modalProps, actions, id }) {
             fields: { ...state.fields, ...fields },
           };
         case 'set-transactions':
+          if (fromTrans && action.transactions) {
+            action.transactions.sort(a => {
+              return transaction.id === a.id ? -1 : 1;
+            });
+          }
           return { ...state, transactions: action.transactions };
         case 'set-repeats':
           return {
@@ -210,12 +215,30 @@ export function ScheduleDetails({ modalProps, actions, id }) {
           endOccurrences: '1',
           endDate: monthUtils.currentDay(),
         };
-        const schedule = {
-          posts_transaction: false,
-          _date: date,
-          _conditions: [{ op: 'isapprox', field: 'date', value: date }],
-          _actions: [],
-        };
+
+        const schedule = fromTrans
+          ? {
+              posts_transaction: false,
+              _conditions: [{ op: 'isapprox', field: 'date', value: date }],
+              _actions: [],
+              _account: transaction.account,
+              _amount: transaction.amount,
+              _amountOp: 'is',
+              name: transaction.payee ? payees[transaction.payee].name : '',
+              _payee: transaction.payee ? transaction.payee : '',
+              _date: {
+                ...date,
+                frequency: 'monthly',
+                start: transaction.date,
+                patterns: [],
+              },
+            }
+          : {
+              posts_transaction: false,
+              _date: date,
+              _conditions: [{ op: 'isapprox', field: 'date', value: date }],
+              _actions: [],
+            };
 
         dispatch({ type: 'set-schedule', schedule });
       } else {
@@ -226,6 +249,7 @@ export function ScheduleDetails({ modalProps, actions, id }) {
         }
       }
     }
+
     run();
   }, []);
 
@@ -321,7 +345,11 @@ export function ScheduleDetails({ modalProps, actions, id }) {
     };
   }, [state.schedule, state.transactionsMode, state.fields]);
 
-  const selectedInst = useSelected('transactions', state.transactions, []);
+  const selectedInst = useSelected(
+    'transactions',
+    state.transactions,
+    transaction ? [transaction.id] : [],
+  );
 
   async function onSave() {
     dispatch({ type: 'form-error', error: null });
@@ -415,7 +443,6 @@ export function ScheduleDetails({ modalProps, actions, id }) {
   }
 
   const payee = payees ? payees[state.fields.payee] : null;
-
   // This is derived from the date
   const repeats = state.fields.date ? !!state.fields.date.frequency : false;
   return (
diff --git a/packages/desktop-client/src/components/schedules/ScheduleLink.tsx b/packages/desktop-client/src/components/schedules/ScheduleLink.tsx
index c6251c12a..ed924fca2 100644
--- a/packages/desktop-client/src/components/schedules/ScheduleLink.tsx
+++ b/packages/desktop-client/src/components/schedules/ScheduleLink.tsx
@@ -4,8 +4,11 @@ import React, { useCallback, useRef, useState } from 'react';
 import { useSchedules } from 'loot-core/src/client/data-hooks/schedules';
 import { send } from 'loot-core/src/platform/client/fetch';
 import { type Query } from 'loot-core/src/shared/query';
+import { type TransactionEntity } from 'loot-core/src/types/models';
 
 import { type BoundActions } from '../../hooks/useActions';
+import { SvgAdd } from '../../icons/v0';
+import { Button } from '../common/Button';
 import { Modal } from '../common/Modal';
 import { Search } from '../common/Search';
 import { Text } from '../common/Text';
@@ -14,14 +17,23 @@ import { type CommonModalProps } from '../Modals';
 
 import { ROW_HEIGHT, SchedulesTable } from './SchedulesTable';
 
+type ModalParams = {
+  id: string;
+  transaction: TransactionEntity;
+};
+
 export function ScheduleLink({
   modalProps,
   actions,
   transactionIds: ids,
+  getTransaction,
+  pushModal,
 }: {
   actions: BoundActions;
   modalProps?: CommonModalProps;
   transactionIds: string[];
+  getTransaction: (transactionId: string) => TransactionEntity;
+  pushModal: (name: string, params: ModalParams) => void;
 }) {
   const [filter, setFilter] = useState('');
 
@@ -45,6 +57,14 @@ export function ScheduleLink({
     actions.popModal();
   }
 
+  async function onCreate() {
+    actions.popModal();
+    pushModal('schedule-edit', {
+      id: null,
+      transaction: getTransaction(ids[0]),
+    });
+  }
+
   return (
     <Modal title="Link Schedule" size={{ width: 600 }} {...modalProps}>
       <View
@@ -70,6 +90,16 @@ export function ScheduleLink({
           value={filter}
           onChange={setFilter}
         />
+        {ids.length === 1 && (
+          <Button
+            type="primary"
+            style={{ marginLeft: 15, padding: '4px 10px' }}
+            onClick={onCreate}
+          >
+            <SvgAdd style={{ width: '20', padding: '3' }} />
+            Create New
+          </Button>
+        )}
       </View>
 
       <View
diff --git a/packages/desktop-client/src/components/transactions/SelectedTransactions.jsx b/packages/desktop-client/src/components/transactions/SelectedTransactions.jsx
index d0c360982..fa4cffa4b 100644
--- a/packages/desktop-client/src/components/transactions/SelectedTransactions.jsx
+++ b/packages/desktop-client/src/components/transactions/SelectedTransactions.jsx
@@ -135,6 +135,8 @@ export function SelectedTransactionsButton({
           case 'link-schedule':
             pushModal('schedule-link', {
               transactionIds: [...selectedItems],
+              getTransaction,
+              pushModal,
             });
             break;
           case 'unlink-schedule':
diff --git a/packages/desktop-client/src/components/transactions/SimpleTransactionsTable.jsx b/packages/desktop-client/src/components/transactions/SimpleTransactionsTable.jsx
index 4d6f6610a..ea43f6adb 100644
--- a/packages/desktop-client/src/components/transactions/SimpleTransactionsTable.jsx
+++ b/packages/desktop-client/src/components/transactions/SimpleTransactionsTable.jsx
@@ -1,10 +1,10 @@
-import React, { memo, useMemo, useCallback } from 'react';
+import React, { memo, useCallback, useMemo } from 'react';
 import { useSelector } from 'react-redux';
 
 import {
   format as formatDate,
-  parseISO,
   isValid as isDateValid,
+  parseISO,
 } from 'date-fns';
 
 import {
@@ -14,10 +14,10 @@ import {
 import { integerToCurrency } from 'loot-core/src/shared/util';
 
 import { useCategories } from '../../hooks/useCategories';
-import { useSelectedItems, useSelectedDispatch } from '../../hooks/useSelected';
+import { useSelectedDispatch, useSelectedItems } from '../../hooks/useSelected';
 import { SvgArrowsSynchronize } from '../../icons/v2';
-import { theme, styles } from '../../style';
-import { Table, Row, Field, Cell, SelectCell } from '../table';
+import { styles, theme } from '../../style';
+import { Cell, Field, Row, SelectCell, Table } from '../table';
 import { DisplayId } from '../util/DisplayId';
 
 function serializeTransaction(transaction, dateFormat) {
diff --git a/upcoming-release-notes/2222.md b/upcoming-release-notes/2222.md
new file mode 100644
index 000000000..1412f1fd8
--- /dev/null
+++ b/upcoming-release-notes/2222.md
@@ -0,0 +1,6 @@
+---
+category: Features
+authors: [xentara1]
+---
+
+Add ability to create schedules from existing transactions
-- 
GitLab