mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-30 10:14:53 -05:00
812 lines
24 KiB
JavaScript
812 lines
24 KiB
JavaScript
import React, { useEffect, useReducer } from 'react';
|
|
import { useDispatch } from 'react-redux';
|
|
|
|
import { pushModal } from 'loot-core/src/client/actions/modals';
|
|
import { runQuery, liveQuery } from 'loot-core/src/client/query-helpers';
|
|
import { send, sendCatch } from 'loot-core/src/platform/client/fetch';
|
|
import * as monthUtils from 'loot-core/src/shared/months';
|
|
import { q } from 'loot-core/src/shared/query';
|
|
import { extractScheduleConds } from 'loot-core/src/shared/schedules';
|
|
|
|
import { useDateFormat } from '../../hooks/useDateFormat';
|
|
import { usePayees } from '../../hooks/usePayees';
|
|
import { useSelected, SelectedProvider } from '../../hooks/useSelected';
|
|
import { theme } from '../../style';
|
|
import { AccountAutocomplete } from '../autocomplete/AccountAutocomplete';
|
|
import { PayeeAutocomplete } from '../autocomplete/PayeeAutocomplete';
|
|
import { Button } from '../common/Button';
|
|
import { Modal } from '../common/Modal';
|
|
import { Stack } from '../common/Stack';
|
|
import { Text } from '../common/Text';
|
|
import { View } from '../common/View';
|
|
import { FormField, FormLabel, Checkbox } from '../forms';
|
|
import { OpSelect } from '../modals/EditRule';
|
|
import { DateSelect } from '../select/DateSelect';
|
|
import { RecurringSchedulePicker } from '../select/RecurringSchedulePicker';
|
|
import { SelectedItemsButton } from '../table';
|
|
import { SimpleTransactionsTable } from '../transactions/SimpleTransactionsTable';
|
|
import { AmountInput, BetweenAmountInput } from '../util/AmountInput';
|
|
import { GenericInput } from '../util/GenericInput';
|
|
|
|
function updateScheduleConditions(schedule, fields) {
|
|
const conds = extractScheduleConds(schedule._conditions);
|
|
|
|
const updateCond = (cond, op, field, value) => {
|
|
if (cond) {
|
|
return { ...cond, value };
|
|
}
|
|
|
|
if (value != null) {
|
|
return { op, field, value };
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
// Validate
|
|
if (fields.date == null) {
|
|
return { error: 'Date is required' };
|
|
}
|
|
|
|
if (fields.amount == null) {
|
|
return { error: 'A valid amount is required' };
|
|
}
|
|
|
|
return {
|
|
conditions: [
|
|
updateCond(conds.payee, 'is', 'payee', fields.payee),
|
|
updateCond(conds.account, 'is', 'account', fields.account),
|
|
updateCond(conds.date, 'isapprox', 'date', fields.date),
|
|
// We don't use `updateCond` for amount because we want to
|
|
// overwrite it completely
|
|
{
|
|
op: fields.amountOp,
|
|
field: 'amount',
|
|
value: fields.amount,
|
|
},
|
|
].filter(Boolean),
|
|
};
|
|
}
|
|
|
|
export function ScheduleDetails({ modalProps, actions, id, transaction }) {
|
|
const adding = id == null;
|
|
const fromTrans = transaction != null;
|
|
const payees = usePayees({ idKey: true });
|
|
const globalDispatch = useDispatch();
|
|
const dateFormat = useDateFormat() || 'MM/dd/yyyy';
|
|
|
|
const [state, dispatch] = useReducer(
|
|
(state, action) => {
|
|
switch (action.type) {
|
|
case 'set-schedule': {
|
|
const schedule = action.schedule;
|
|
|
|
// See if there are custom rules
|
|
const conds = extractScheduleConds(schedule._conditions);
|
|
const condsSet = new Set(Object.values(conds));
|
|
const isCustom =
|
|
schedule._conditions.find(c => !condsSet.has(c)) ||
|
|
schedule._actions.find(a => a.op !== 'link-schedule');
|
|
|
|
return {
|
|
...state,
|
|
schedule: action.schedule,
|
|
isCustom,
|
|
fields: {
|
|
payee: schedule._payee,
|
|
account: schedule._account,
|
|
// defalut to a non-zero value so the sign can be changed before the value
|
|
amount: schedule._amount || -1000,
|
|
amountOp: schedule._amountOp || 'isapprox',
|
|
date: schedule._date,
|
|
posts_transaction: action.schedule.posts_transaction,
|
|
name: schedule.name,
|
|
},
|
|
};
|
|
}
|
|
case 'set-field':
|
|
if (!(action.field in state.fields)) {
|
|
throw new Error('Unknown field: ' + action.field);
|
|
}
|
|
|
|
const fields = { [action.field]: action.value };
|
|
|
|
// If we are changing the amount operator either to or
|
|
// away from the `isbetween` operator, the amount value is
|
|
// different and we need to convert it
|
|
if (
|
|
action.field === 'amountOp' &&
|
|
action.value !== state.fields.amountOp
|
|
) {
|
|
if (action.value === 'isbetween') {
|
|
// We need a range if switching to `isbetween`. The
|
|
// amount field should be a number since we are
|
|
// switching away from the other ops, but check just in
|
|
// case
|
|
fields.amount =
|
|
typeof state.fields.amount === 'number'
|
|
? { num1: state.fields.amount, num2: state.fields.amount }
|
|
: { num1: 0, num2: 0 };
|
|
} else if (state.fields.amountOp === 'isbetween') {
|
|
// We need just a number if switching away from
|
|
// `isbetween`. The amount field should be a range, but
|
|
// also check just in case. We grab just the first
|
|
// number and use it
|
|
fields.amount =
|
|
typeof state.fields.amount === 'number'
|
|
? state.fields.amount
|
|
: state.fields.amount.num1;
|
|
}
|
|
}
|
|
|
|
return {
|
|
...state,
|
|
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 {
|
|
...state,
|
|
fields: {
|
|
...state.fields,
|
|
date: action.repeats
|
|
? {
|
|
frequency: 'monthly',
|
|
start: monthUtils.currentDay(),
|
|
patterns: [],
|
|
}
|
|
: monthUtils.currentDay(),
|
|
},
|
|
};
|
|
case 'set-upcoming-dates':
|
|
return {
|
|
...state,
|
|
upcomingDates: action.dates,
|
|
};
|
|
|
|
case 'form-error':
|
|
return { ...state, error: action.error };
|
|
|
|
case 'switch-transactions':
|
|
return { ...state, transactionsMode: action.mode };
|
|
|
|
default:
|
|
throw new Error('Unknown action: ' + action.type);
|
|
}
|
|
},
|
|
{
|
|
schedule: null,
|
|
upcomingDates: null,
|
|
error: null,
|
|
fields: {
|
|
payee: null,
|
|
account: null,
|
|
amount: null,
|
|
amountOp: null,
|
|
date: null,
|
|
posts_transaction: false,
|
|
name: null,
|
|
},
|
|
transactions: [],
|
|
transactionsMode: adding ? 'matched' : 'linked',
|
|
},
|
|
);
|
|
|
|
async function loadSchedule() {
|
|
const { data } = await runQuery(q('schedules').filter({ id }).select('*'));
|
|
return data[0];
|
|
}
|
|
|
|
useEffect(() => {
|
|
async function run() {
|
|
if (adding) {
|
|
const date = {
|
|
start: monthUtils.currentDay(),
|
|
frequency: 'monthly',
|
|
patterns: [],
|
|
skipWeekend: false,
|
|
weekendSolveMode: 'after',
|
|
endMode: 'never',
|
|
endOccurrences: '1',
|
|
endDate: monthUtils.currentDay(),
|
|
};
|
|
|
|
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 {
|
|
const schedule = await loadSchedule();
|
|
|
|
if (schedule && state.schedule == null) {
|
|
dispatch({ type: 'set-schedule', schedule });
|
|
}
|
|
}
|
|
}
|
|
|
|
run();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
async function run() {
|
|
const date = state.fields.date;
|
|
|
|
if (date == null) {
|
|
dispatch({ type: 'set-upcoming-dates', dates: null });
|
|
} else {
|
|
if (date.frequency) {
|
|
const { data } = await sendCatch('schedule/get-upcoming-dates', {
|
|
config: date,
|
|
count: 3,
|
|
});
|
|
dispatch({ type: 'set-upcoming-dates', dates: data });
|
|
} else {
|
|
const today = monthUtils.currentDay();
|
|
if (date === today || monthUtils.isAfter(date, today)) {
|
|
dispatch({ type: 'set-upcoming-dates', dates: [date] });
|
|
} else {
|
|
dispatch({ type: 'set-upcoming-dates', dates: null });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
run();
|
|
}, [state.fields.date]);
|
|
|
|
useEffect(() => {
|
|
if (
|
|
state.schedule &&
|
|
state.schedule.id &&
|
|
state.transactionsMode === 'linked'
|
|
) {
|
|
const live = liveQuery(
|
|
q('transactions')
|
|
.filter({ schedule: state.schedule.id })
|
|
.select('*')
|
|
.options({ splits: 'all' }),
|
|
data => dispatch({ type: 'set-transactions', transactions: data }),
|
|
);
|
|
return live.unsubscribe;
|
|
}
|
|
}, [state.schedule, state.transactionsMode]);
|
|
|
|
useEffect(() => {
|
|
let current = true;
|
|
let unsubscribe;
|
|
|
|
if (state.schedule && state.transactionsMode === 'matched') {
|
|
const { error, conditions: originalConditions } =
|
|
updateScheduleConditions(state.schedule, state.fields);
|
|
|
|
if (error) {
|
|
dispatch({ type: 'form-error', error });
|
|
return;
|
|
}
|
|
|
|
// *Extremely* gross hack because the rules are not mapped to
|
|
// public names automatically. We really should be doing that
|
|
// at the database layer
|
|
const conditions = originalConditions.map(cond => {
|
|
if (cond.field === 'description') {
|
|
return { ...cond, field: 'payee' };
|
|
} else if (cond.field === 'acct') {
|
|
return { ...cond, field: 'account' };
|
|
}
|
|
return cond;
|
|
});
|
|
|
|
send('make-filters-from-conditions', {
|
|
conditions,
|
|
}).then(({ filters }) => {
|
|
if (current) {
|
|
const live = liveQuery(
|
|
q('transactions')
|
|
.filter({ $and: filters })
|
|
.select('*')
|
|
.options({ splits: 'all' }),
|
|
data => dispatch({ type: 'set-transactions', transactions: data }),
|
|
);
|
|
unsubscribe = live.unsubscribe;
|
|
}
|
|
});
|
|
}
|
|
|
|
return () => {
|
|
current = false;
|
|
if (unsubscribe) {
|
|
unsubscribe();
|
|
}
|
|
};
|
|
}, [state.schedule, state.transactionsMode, state.fields]);
|
|
|
|
const selectedInst = useSelected(
|
|
'transactions',
|
|
state.transactions,
|
|
transaction ? [transaction.id] : [],
|
|
);
|
|
|
|
async function onSave() {
|
|
dispatch({ type: 'form-error', error: null });
|
|
if (state.fields.name) {
|
|
const { data: sameName } = await runQuery(
|
|
q('schedules').filter({ name: state.fields.name }).select('id'),
|
|
);
|
|
if (sameName.length > 0 && sameName[0].id !== state.schedule.id) {
|
|
dispatch({
|
|
type: 'form-error',
|
|
error: 'There is already a schedule with this name',
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
const { error, conditions } = updateScheduleConditions(
|
|
state.schedule,
|
|
state.fields,
|
|
);
|
|
|
|
if (error) {
|
|
dispatch({ type: 'form-error', error });
|
|
return;
|
|
}
|
|
|
|
const res = await sendCatch(
|
|
adding ? 'schedule/create' : 'schedule/update',
|
|
{
|
|
schedule: {
|
|
id: state.schedule.id,
|
|
posts_transaction: state.fields.posts_transaction,
|
|
name: state.fields.name,
|
|
},
|
|
conditions,
|
|
},
|
|
);
|
|
|
|
if (res.error) {
|
|
dispatch({
|
|
type: 'form-error',
|
|
error:
|
|
'An error occurred while saving. Please visit https://actualbudget.org/contact/ for support.',
|
|
});
|
|
} else {
|
|
if (adding) {
|
|
await onLinkTransactions([...selectedInst.items], res.data);
|
|
}
|
|
actions.popModal();
|
|
}
|
|
}
|
|
|
|
async function onEditRule(ruleId) {
|
|
const rule = await send('rule-get', { id: ruleId || state.schedule.rule });
|
|
|
|
globalDispatch(
|
|
pushModal('edit-rule', {
|
|
rule,
|
|
onSave: async () => {
|
|
const schedule = await loadSchedule();
|
|
dispatch({ type: 'set-schedule', schedule });
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
|
|
async function onLinkTransactions(ids, scheduleId) {
|
|
await send('transactions-batch-update', {
|
|
updated: ids.map(id => ({
|
|
id,
|
|
schedule: scheduleId || state.schedule.id,
|
|
})),
|
|
});
|
|
selectedInst.dispatch({ type: 'select-none' });
|
|
}
|
|
|
|
async function onUnlinkTransactions(ids) {
|
|
await send('transactions-batch-update', {
|
|
updated: ids.map(id => ({ id, schedule: null })),
|
|
});
|
|
selectedInst.dispatch({ type: 'select-none' });
|
|
}
|
|
|
|
if (state.schedule == null) {
|
|
return null;
|
|
}
|
|
|
|
function onSwitchTransactions(mode) {
|
|
dispatch({ type: 'switch-transactions', mode });
|
|
selectedInst.dispatch({ type: 'select-none' });
|
|
}
|
|
|
|
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 (
|
|
<Modal
|
|
title={payee ? `Schedule: ${payee.name}` : 'Schedule'}
|
|
size="medium"
|
|
{...modalProps}
|
|
>
|
|
<Stack direction="row" style={{ marginTop: 10 }}>
|
|
<FormField style={{ flex: 1 }}>
|
|
<FormLabel title="Schedule Name" htmlFor="name-field" />
|
|
<GenericInput
|
|
field="string"
|
|
type="string"
|
|
value={state.fields.name}
|
|
multi={false}
|
|
onChange={e => {
|
|
dispatch({ type: 'set-field', field: 'name', value: e });
|
|
}}
|
|
/>
|
|
</FormField>
|
|
</Stack>
|
|
<Stack direction="row" style={{ marginTop: 20 }}>
|
|
<FormField style={{ flex: 1 }}>
|
|
<FormLabel title="Payee" id="payee-label" htmlFor="payee-field" />
|
|
<PayeeAutocomplete
|
|
value={state.fields.payee}
|
|
labelProps={{ id: 'payee-label' }}
|
|
inputProps={{ id: 'payee-field', placeholder: '(none)' }}
|
|
onSelect={id =>
|
|
dispatch({ type: 'set-field', field: 'payee', value: id })
|
|
}
|
|
isCreatable
|
|
/>
|
|
</FormField>
|
|
|
|
<FormField style={{ flex: 1 }}>
|
|
<FormLabel
|
|
title="Account"
|
|
id="account-label"
|
|
htmlFor="account-field"
|
|
/>
|
|
<AccountAutocomplete
|
|
includeClosedAccounts={false}
|
|
value={state.fields.account}
|
|
labelProps={{ id: 'account-label' }}
|
|
inputProps={{ id: 'account-field', placeholder: '(none)' }}
|
|
onSelect={id =>
|
|
dispatch({ type: 'set-field', field: 'account', value: id })
|
|
}
|
|
/>
|
|
</FormField>
|
|
|
|
<FormField style={{ flex: 1 }}>
|
|
<Stack direction="row" align="center" style={{ marginBottom: 3 }}>
|
|
<FormLabel
|
|
title="Amount"
|
|
htmlFor="amount-field"
|
|
style={{ margin: 0, flex: 1 }}
|
|
/>
|
|
<OpSelect
|
|
ops={['isapprox', 'is', 'isbetween']}
|
|
value={state.fields.amountOp}
|
|
formatOp={op => {
|
|
switch (op) {
|
|
case 'is':
|
|
return 'is exactly';
|
|
case 'isapprox':
|
|
return 'is approximately';
|
|
case 'isbetween':
|
|
return 'is between';
|
|
default:
|
|
throw new Error('Invalid op for select: ' + op);
|
|
}
|
|
}}
|
|
style={{
|
|
padding: '0 10px',
|
|
color: theme.pageTextLight,
|
|
fontSize: 12,
|
|
}}
|
|
onChange={(_, op) =>
|
|
dispatch({ type: 'set-field', field: 'amountOp', value: op })
|
|
}
|
|
/>
|
|
</Stack>
|
|
{state.fields.amountOp === 'isbetween' ? (
|
|
<BetweenAmountInput
|
|
defaultValue={state.fields.amount}
|
|
onChange={value =>
|
|
dispatch({
|
|
type: 'set-field',
|
|
field: 'amount',
|
|
value,
|
|
})
|
|
}
|
|
/>
|
|
) : (
|
|
<AmountInput
|
|
id="amount-field"
|
|
value={state.fields.amount}
|
|
onUpdate={value =>
|
|
dispatch({
|
|
type: 'set-field',
|
|
field: 'amount',
|
|
value,
|
|
})
|
|
}
|
|
/>
|
|
)}
|
|
</FormField>
|
|
</Stack>
|
|
|
|
<View style={{ marginTop: 20 }}>
|
|
<FormLabel title="Date" />
|
|
</View>
|
|
|
|
<Stack direction="row" align="flex-start" justify="space-between">
|
|
<View style={{ width: '13.44rem' }}>
|
|
{repeats ? (
|
|
<RecurringSchedulePicker
|
|
value={state.fields.date}
|
|
onChange={value =>
|
|
dispatch({ type: 'set-field', field: 'date', value })
|
|
}
|
|
/>
|
|
) : (
|
|
<DateSelect
|
|
value={state.fields.date}
|
|
onSelect={date =>
|
|
dispatch({ type: 'set-field', field: 'date', value: date })
|
|
}
|
|
dateFormat={dateFormat}
|
|
/>
|
|
)}
|
|
|
|
{state.upcomingDates && (
|
|
<View style={{ fontSize: 13, marginTop: 20 }}>
|
|
<Text style={{ color: theme.pageTextLight, fontWeight: 600 }}>
|
|
Upcoming dates
|
|
</Text>
|
|
<Stack
|
|
direction="column"
|
|
spacing={1}
|
|
style={{ marginTop: 10, color: theme.pageTextLight }}
|
|
>
|
|
{state.upcomingDates.map(date => (
|
|
<View key={date}>
|
|
{monthUtils.format(date, `${dateFormat} EEEE`)}
|
|
</View>
|
|
))}
|
|
</Stack>
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
<View
|
|
style={{
|
|
marginTop: 5,
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
userSelect: 'none',
|
|
}}
|
|
>
|
|
<Checkbox
|
|
id="form_repeats"
|
|
checked={repeats}
|
|
onChange={e => {
|
|
dispatch({ type: 'set-repeats', repeats: e.target.checked });
|
|
}}
|
|
/>
|
|
<label htmlFor="form_repeats" style={{ userSelect: 'none' }}>
|
|
Repeats
|
|
</label>
|
|
</View>
|
|
|
|
<Stack align="flex-end">
|
|
<View
|
|
style={{
|
|
marginTop: 5,
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
userSelect: 'none',
|
|
justifyContent: 'flex-end',
|
|
}}
|
|
>
|
|
<Checkbox
|
|
id="form_posts_transaction"
|
|
checked={state.fields.posts_transaction}
|
|
onChange={e => {
|
|
dispatch({
|
|
type: 'set-field',
|
|
field: 'posts_transaction',
|
|
value: e.target.checked,
|
|
});
|
|
}}
|
|
/>
|
|
<label
|
|
htmlFor="form_posts_transaction"
|
|
style={{ userSelect: 'none' }}
|
|
>
|
|
Automatically add transaction
|
|
</label>
|
|
</View>
|
|
|
|
<Text
|
|
style={{
|
|
width: 350,
|
|
textAlign: 'right',
|
|
color: theme.pageTextLight,
|
|
marginTop: 10,
|
|
fontSize: 13,
|
|
lineHeight: '1.4em',
|
|
}}
|
|
>
|
|
If checked, the schedule will automatically create transactions for
|
|
you in the specified account
|
|
</Text>
|
|
|
|
{!adding && state.schedule.rule && (
|
|
<Stack direction="row" align="center" style={{ marginTop: 20 }}>
|
|
{state.isCustom && (
|
|
<Text
|
|
style={{
|
|
color: theme.pageTextLight,
|
|
fontSize: 13,
|
|
textAlign: 'right',
|
|
width: 350,
|
|
}}
|
|
>
|
|
This schedule has custom conditions and actions
|
|
</Text>
|
|
)}
|
|
<Button onClick={() => onEditRule()} disabled={adding}>
|
|
Edit as rule
|
|
</Button>
|
|
</Stack>
|
|
)}
|
|
</Stack>
|
|
</Stack>
|
|
|
|
<View style={{ marginTop: 30, flex: 1 }}>
|
|
<SelectedProvider instance={selectedInst}>
|
|
{adding ? (
|
|
<View style={{ flexDirection: 'row', padding: '5px 0' }}>
|
|
<Text style={{ color: theme.pageTextLight }}>
|
|
These transactions match this schedule:
|
|
</Text>
|
|
<View style={{ flex: 1 }} />
|
|
<Text style={{ color: theme.pageTextLight }}>
|
|
Select transactions to link on save
|
|
</Text>
|
|
</View>
|
|
) : (
|
|
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
|
<Button
|
|
type="bare"
|
|
style={{
|
|
color:
|
|
state.transactionsMode === 'linked'
|
|
? theme.pageTextLink
|
|
: theme.pageTextSubdued,
|
|
marginRight: 10,
|
|
fontSize: 14,
|
|
}}
|
|
onClick={() => onSwitchTransactions('linked')}
|
|
>
|
|
Linked transactions
|
|
</Button>{' '}
|
|
<Button
|
|
type="bare"
|
|
style={{
|
|
color:
|
|
state.transactionsMode === 'matched'
|
|
? theme.pageTextLink
|
|
: theme.pageTextSubdued,
|
|
fontSize: 14,
|
|
}}
|
|
onClick={() => onSwitchTransactions('matched')}
|
|
>
|
|
Find matching transactions
|
|
</Button>
|
|
<View style={{ flex: 1 }} />
|
|
<SelectedItemsButton
|
|
name="transactions"
|
|
items={
|
|
state.transactionsMode === 'linked'
|
|
? [{ name: 'unlink', text: 'Unlink from schedule' }]
|
|
: [{ name: 'link', text: 'Link to schedule' }]
|
|
}
|
|
onSelect={(name, ids) => {
|
|
switch (name) {
|
|
case 'link':
|
|
onLinkTransactions(ids);
|
|
break;
|
|
case 'unlink':
|
|
onUnlinkTransactions(ids);
|
|
break;
|
|
default:
|
|
}
|
|
}}
|
|
/>
|
|
</View>
|
|
)}
|
|
|
|
<SimpleTransactionsTable
|
|
renderEmpty={
|
|
<NoTransactionsMessage
|
|
error={state.error}
|
|
transactionsMode={state.transactionsMode}
|
|
/>
|
|
}
|
|
transactions={state.transactions}
|
|
fields={['date', 'payee', 'amount']}
|
|
style={{
|
|
border: '1px solid ' + theme.tableBorder,
|
|
borderRadius: 4,
|
|
overflow: 'hidden',
|
|
marginTop: 5,
|
|
maxHeight: 200,
|
|
}}
|
|
/>
|
|
</SelectedProvider>
|
|
</View>
|
|
|
|
<Stack
|
|
direction="row"
|
|
justify="flex-end"
|
|
align="center"
|
|
style={{ marginTop: 20 }}
|
|
>
|
|
{state.error && (
|
|
<Text style={{ color: theme.errorText }}>{state.error}</Text>
|
|
)}
|
|
<Button style={{ marginRight: 10 }} onClick={actions.popModal}>
|
|
Cancel
|
|
</Button>
|
|
<Button type="primary" onClick={onSave}>
|
|
{adding ? 'Add' : 'Save'}
|
|
</Button>
|
|
</Stack>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
function NoTransactionsMessage(props) {
|
|
return (
|
|
<View
|
|
style={{
|
|
padding: 20,
|
|
color: theme.pageTextLight,
|
|
textAlign: 'center',
|
|
}}
|
|
>
|
|
{props.error ? (
|
|
<Text style={{ color: theme.errorText }}>
|
|
Could not search: {props.error}
|
|
</Text>
|
|
) : props.transactionsMode === 'matched' ? (
|
|
'No matching transactions'
|
|
) : (
|
|
'No linked transactions'
|
|
)}
|
|
</View>
|
|
);
|
|
}
|