Compare commits
2 Commits
115a79a5c0
...
c0542900be
| Author | SHA1 | Date |
|---|---|---|
|
|
c0542900be | |
|
|
6280ab7cfb |
|
|
@ -4,6 +4,7 @@
|
|||
],
|
||||
"copyright": "Copyright © 2024, Andrew Lalis",
|
||||
"dependencies": {
|
||||
"cronexp": ">=0.1.0-beta3 <0.2.0-0",
|
||||
"d2sqlite3": "~>1.0",
|
||||
"handy-http-starter": "~>1.6",
|
||||
"jwt4d": "~>0.0.2",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"fileVersion": 1,
|
||||
"versions": {
|
||||
"asdf": "0.8.0",
|
||||
"cronexp": "0.1.0-beta3",
|
||||
"d2sqlite3": "1.0.0",
|
||||
"dxml": "0.4.5",
|
||||
"handy-http-data": "1.3.2",
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ interface ProfileDataSource {
|
|||
TransactionTagRepository getTransactionTagRepository();
|
||||
TransactionRepository getTransactionRepository();
|
||||
TransactionDraftRepository getTransactionDraftRepository();
|
||||
RecurringTransactionRepository getRecurringTransactionRepository();
|
||||
|
||||
AnalyticsRepository getAnalyticsRepository();
|
||||
|
||||
|
|
@ -97,6 +98,9 @@ version(unittest) {
|
|||
TransactionDraftRepository getTransactionDraftRepository() {
|
||||
throw new Exception("Not implemented");
|
||||
}
|
||||
RecurringTransactionRepository getRecurringTransactionRepository() {
|
||||
throw new Exception("Not implemented");
|
||||
}
|
||||
AnalyticsRepository getAnalyticsRepository() {
|
||||
throw new Exception("Not implemented");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -216,6 +216,7 @@ class SqliteProfileDataSource : ProfileDataSource {
|
|||
TransactionTagRepository transactionTagRepo;
|
||||
TransactionRepository transactionRepo;
|
||||
TransactionDraftRepository transactionDraftRepo;
|
||||
RecurringTransactionRepository recurringTransactionRepo;
|
||||
AnalyticsRepository analyticsRepo;
|
||||
|
||||
this(string path) {
|
||||
|
|
@ -305,6 +306,13 @@ class SqliteProfileDataSource : ProfileDataSource {
|
|||
return transactionDraftRepo;
|
||||
}
|
||||
|
||||
RecurringTransactionRepository getRecurringTransactionRepository() {
|
||||
if (recurringTransactionRepo is null) {
|
||||
recurringTransactionRepo = new SqliteRecurringTransactionRepository(db);
|
||||
}
|
||||
return recurringTransactionRepo;
|
||||
}
|
||||
|
||||
AnalyticsRepository getAnalyticsRepository() {
|
||||
if (analyticsRepo is null) {
|
||||
analyticsRepo = new SqliteAnalyticsRepository(db);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ void startScheduledJobs() {
|
|||
);
|
||||
|
||||
JobScheduler jobScheduler = new TaskPoolScheduler();
|
||||
|
||||
jobScheduler.addJob(() {
|
||||
// Clear old analytics data from profiles.
|
||||
import profile.data;
|
||||
|
|
@ -38,5 +39,32 @@ void startScheduledJobs() {
|
|||
);
|
||||
|
||||
}, analyticsSchedule);
|
||||
|
||||
// Add a scheduled job to regularly check for and create recurring transaction drafts.
|
||||
jobScheduler.addJob(() {
|
||||
import profile.data;
|
||||
import profile.data_impl_sqlite;
|
||||
import profile.model;
|
||||
import transaction.dto;
|
||||
import transaction.data;
|
||||
import util.pagination;
|
||||
import std.stdio;
|
||||
import std.datetime;
|
||||
import cronexp;
|
||||
FileSystemProfileRepository.doForAllUserProfiles((Profile profile, ProfileRepository profileRepo) {
|
||||
writefln!"Recurring transaction check: %s / %s"(profile.username, profile.name);
|
||||
ProfileDataSource ds = profileRepo.getDataSource(profile);
|
||||
RecurringTransactionRepository rtRepo = ds.getRecurringTransactionRepository();
|
||||
Page!(RecurringTransactionResponse) result = rtRepo.findAll(PageRequest.unpaged());
|
||||
foreach (rt; result.items) {
|
||||
writeln(rt.scheduleExpr);
|
||||
DateTime now = cast(DateTime) Clock.currTime();
|
||||
auto cron = CronExpr(rt.scheduleExpr);
|
||||
writefln!"scheduleExpr = %s -> %s"(rt.scheduleExpr, cron.getNext(now));
|
||||
// TODO: Figure out how to actually create the transactions!
|
||||
}
|
||||
});
|
||||
}, new FixedIntervalSchedule(minutes(1), Clock.currTime(UTC())));
|
||||
|
||||
jobScheduler.start();
|
||||
}
|
||||
|
|
@ -216,7 +216,7 @@ private ulong getCategoryId(in ServerHttpRequest request) {
|
|||
return getPathParamOrThrow!ulong(request, "categoryId");
|
||||
}
|
||||
|
||||
// Drafts & Templates
|
||||
// Drafts & Templates & Recurring Transactions
|
||||
|
||||
immutable DEFAULT_DRAFT_PAGE = PageRequest(1, 10, [Sort("draft.id", SortDir.DESC)]);
|
||||
|
||||
|
|
@ -264,6 +264,46 @@ void handleDeleteDraft(ref ServerHttpRequest request, ref ServerHttpResponse res
|
|||
deleteDraft(ds, draftId);
|
||||
}
|
||||
|
||||
@GetMapping(PROFILE_PATH ~ "/transaction-drafts/:draftId:ulong/recurring-transactions")
|
||||
void handleGetDraftRecurringTransactions(
|
||||
ref ServerHttpRequest request,
|
||||
ref ServerHttpResponse response
|
||||
) {
|
||||
ProfileDataSource ds = getProfileDataSource(request);
|
||||
ulong draftId = getDraftId(request);
|
||||
RecurringTransactionResponse[] results = getRecurringTransactionsForDraft(ds, draftId);
|
||||
writeJsonBody(response, results);
|
||||
}
|
||||
|
||||
private ulong getDraftId(in ServerHttpRequest request) {
|
||||
return getPathParamOrThrow!ulong(request, "draftId");
|
||||
}
|
||||
|
||||
immutable DEFAULT_RECURRING_TRANSACTIONS_PAGE = PageRequest(1, 10, [Sort("id", SortDir.DESC)]);
|
||||
|
||||
@GetMapping(PROFILE_PATH ~ "/recurring-transactions")
|
||||
void handleGetRecurringTransactions(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
ProfileDataSource ds = getProfileDataSource(request);
|
||||
PageRequest pr = PageRequest.parse(request, DEFAULT_RECURRING_TRANSACTIONS_PAGE);
|
||||
Page!RecurringTransactionResponse page = getRecurringTransactions(ds, pr);
|
||||
writeJsonBody(response, page);
|
||||
}
|
||||
|
||||
@PostMapping(PROFILE_PATH ~ "/recurring-transactions")
|
||||
void handlePostRecurringTransaction(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
RecurringTransactionPayload payload = readJsonBodyAs!RecurringTransactionPayload(request);
|
||||
ProfileDataSource ds = getProfileDataSource(request);
|
||||
RecurringTransactionResponse result = createRecurringTransaction(ds, payload);
|
||||
writeJsonBody(response, result);
|
||||
}
|
||||
|
||||
@DeleteMapping(PROFILE_PATH ~ "/recurring-transactions/:recurringTransactionId:ulong")
|
||||
void handleDeleteRecurringTransaction(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
ProfileDataSource ds = getProfileDataSource(request);
|
||||
ulong id = getRecurringTransactionId(request);
|
||||
deleteRecurringTransaction(ds, id);
|
||||
}
|
||||
|
||||
private ulong getRecurringTransactionId(in ServerHttpRequest request) {
|
||||
return getPathParamOrThrow!ulong(request, "recurringTransactionId");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,3 +67,11 @@ interface TransactionDraftRepository {
|
|||
void updateTags(ulong draftId, in string[] tags);
|
||||
string[] findAllTags();
|
||||
}
|
||||
|
||||
interface RecurringTransactionRepository {
|
||||
Page!RecurringTransactionResponse findAll(in PageRequest pr);
|
||||
RecurringTransactionResponse[] findAllByDraftId(ulong draftId);
|
||||
Optional!RecurringTransactionResponse findById(ulong id);
|
||||
RecurringTransactionResponse insert(in RecurringTransactionPayload data);
|
||||
void deleteById(ulong id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -913,3 +913,55 @@ class SqliteTransactionDraftRepository : TransactionDraftRepository {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SqliteRecurringTransactionRepository : RecurringTransactionRepository {
|
||||
private Database db;
|
||||
this(Database db) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
Page!RecurringTransactionResponse findAll(in PageRequest pr) {
|
||||
string query = "SELECT * FROM recurring_transaction " ~ pr.toSql();
|
||||
RecurringTransactionResponse[] results = util.sqlite.findAll(db, query, &parseRecurringTransaction);
|
||||
ulong totalCount = util.sqlite.count(db, "SELECT COUNT(DISTINCT id) FROM recurring_transaction");
|
||||
return Page!(RecurringTransactionResponse).of(results, pr, totalCount);
|
||||
}
|
||||
|
||||
RecurringTransactionResponse[] findAllByDraftId(ulong draftId) {
|
||||
string query = "SELECT * FROM recurring_transaction WHERE draft_id = ? ORDER BY id";
|
||||
return util.sqlite.findAll(db, query, &parseRecurringTransaction, draftId);
|
||||
}
|
||||
|
||||
Optional!RecurringTransactionResponse findById(ulong id) {
|
||||
return util.sqlite.findOne(
|
||||
db,
|
||||
"SELECT * FROM recurring_transaction WHERE id = ?",
|
||||
&parseRecurringTransaction,
|
||||
id
|
||||
);
|
||||
}
|
||||
|
||||
RecurringTransactionResponse insert(in RecurringTransactionPayload data) {
|
||||
util.sqlite.update(
|
||||
db,
|
||||
"INSERT INTO recurring_transaction " ~
|
||||
"(draft_id, schedule_expr) " ~
|
||||
"VALUES (?, ?)",
|
||||
data.draftId,
|
||||
data.scheduleExpr
|
||||
);
|
||||
return findById(db.lastInsertRowid()).orElseThrow();
|
||||
}
|
||||
|
||||
void deleteById(ulong id) {
|
||||
util.sqlite.deleteById(db, "recurring_transaction", id);
|
||||
}
|
||||
|
||||
private static RecurringTransactionResponse parseRecurringTransaction(Row row) {
|
||||
return RecurringTransactionResponse(
|
||||
row.peek!ulong(0),
|
||||
row.peek!ulong(1),
|
||||
row.peek!string(2)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -205,3 +205,16 @@ struct TransactionDraftPayload {
|
|||
Optional!ulong categoryId;
|
||||
}
|
||||
}
|
||||
|
||||
// Recurring Transaction stuff:
|
||||
|
||||
struct RecurringTransactionPayload {
|
||||
ulong draftId;
|
||||
string scheduleExpr;
|
||||
}
|
||||
|
||||
struct RecurringTransactionResponse {
|
||||
ulong id;
|
||||
ulong draftId;
|
||||
string scheduleExpr;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -530,3 +530,24 @@ private void updateDraftAttachments(
|
|||
attachmentRepo.remove(idToRemove);
|
||||
}
|
||||
}
|
||||
|
||||
// Recurring Transactions:
|
||||
|
||||
Page!RecurringTransactionResponse getRecurringTransactions(ProfileDataSource ds, PageRequest pr) {
|
||||
return ds.getRecurringTransactionRepository().findAll(pr);
|
||||
}
|
||||
|
||||
RecurringTransactionResponse createRecurringTransaction(ProfileDataSource ds, in RecurringTransactionPayload payload) {
|
||||
return ds.getRecurringTransactionRepository()
|
||||
.insert(payload);
|
||||
}
|
||||
|
||||
void deleteRecurringTransaction(ProfileDataSource ds, ulong id) {
|
||||
ds.getRecurringTransactionRepository()
|
||||
.deleteById(id);
|
||||
}
|
||||
|
||||
RecurringTransactionResponse[] getRecurringTransactionsForDraft(ProfileDataSource ds, ulong draftId) {
|
||||
return ds.getRecurringTransactionRepository()
|
||||
.findAllByDraftId(draftId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -225,6 +225,8 @@ CREATE TABLE history_item_linked_journal_entry (
|
|||
);
|
||||
|
||||
-- Drafts / Templates / Recurring Transactions
|
||||
-- Generally, draft tables are copies of their normal transaction counterparts,
|
||||
-- with looser nullability constraints in some cases.
|
||||
|
||||
CREATE TABLE transaction_draft (
|
||||
id INTEGER PRIMARY KEY,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
"@idle-observer/vue3": "^0.2.0",
|
||||
"chart.js": "^4.5.1",
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
"cron-parser": "^5.6.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"pinia": "^3.0.4",
|
||||
|
|
|
|||
|
|
@ -199,6 +199,17 @@ export interface TransactionDraftPayload {
|
|||
attachmentIdsToRemove: number[]
|
||||
}
|
||||
|
||||
export interface RecurringTransactionPayload {
|
||||
draftId: number
|
||||
scheduleExpr: string
|
||||
}
|
||||
|
||||
export interface RecurringTransactionResponse {
|
||||
id: number
|
||||
draftId: number
|
||||
scheduleExpr: string
|
||||
}
|
||||
|
||||
export class TransactionApiClient extends ApiClient {
|
||||
readonly path: string
|
||||
|
||||
|
|
@ -385,4 +396,24 @@ export class TransactionApiClient extends ApiClient {
|
|||
deleteDraft(id: number): Promise<void> {
|
||||
return super.delete(this.path + '/transaction-drafts/' + id)
|
||||
}
|
||||
|
||||
getRecurringTransactionsForDraft(draftId: number): Promise<RecurringTransactionResponse[]> {
|
||||
return super.getJson(`${this.path}/transaction-drafts/${draftId}/recurring-transactions`)
|
||||
}
|
||||
|
||||
getRecurringTransactions(
|
||||
paginationOptions: PageRequest | undefined = undefined,
|
||||
): Promise<Page<RecurringTransactionResponse>> {
|
||||
return super.getJsonPage(this.path + '/recurring-transactions', paginationOptions)
|
||||
}
|
||||
|
||||
createRecurringTransaction(
|
||||
data: RecurringTransactionPayload,
|
||||
): Promise<RecurringTransactionResponse> {
|
||||
return super.postJson(this.path + '/recurring-transactions', data)
|
||||
}
|
||||
|
||||
deleteRecurringTransaction(id: number): Promise<void> {
|
||||
return super.delete(`${this.path}/recurring-transactions/${id}`)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
<script setup lang="ts">
|
||||
import {
|
||||
TransactionApiClient,
|
||||
type RecurringTransactionPayload,
|
||||
type RecurringTransactionResponse,
|
||||
} from '@/api/transaction'
|
||||
import ModalWrapper from './common/ModalWrapper.vue'
|
||||
import { ref, useTemplateRef, type Ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import AppForm from './common/form/AppForm.vue'
|
||||
import FormGroup from './common/form/FormGroup.vue'
|
||||
import FormControl from './common/form/FormControl.vue'
|
||||
import AppButton from './common/AppButton.vue'
|
||||
import { getSelectedProfile } from '@/api/profile.ts'
|
||||
|
||||
const route = useRoute()
|
||||
const props = defineProps<{ draftId: number }>()
|
||||
const modal = useTemplateRef('modal')
|
||||
const savedTxn: Ref<RecurringTransactionResponse | undefined> = ref()
|
||||
|
||||
// Form Data:
|
||||
const dayOfMonth: Ref<number> = ref(1)
|
||||
|
||||
async function show(): Promise<RecurringTransactionResponse | undefined> {
|
||||
if (!modal.value) return undefined
|
||||
savedTxn.value = undefined
|
||||
const result = await modal.value.show()
|
||||
if (result === 'saved') {
|
||||
return savedTxn.value
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
async function addRecurringTransaction() {
|
||||
const payload: RecurringTransactionPayload = {
|
||||
draftId: props.draftId,
|
||||
scheduleExpr: `0 0 0 ${dayOfMonth.value} * *`,
|
||||
}
|
||||
const api = new TransactionApiClient(getSelectedProfile(route))
|
||||
try {
|
||||
savedTxn.value = await api.createRecurringTransaction(payload)
|
||||
modal.value?.close('saved')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
savedTxn.value = undefined
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ show })
|
||||
</script>
|
||||
<template>
|
||||
<ModalWrapper ref="modal">
|
||||
<template v-slot:default>
|
||||
<h2>Add Recurring Transaction</h2>
|
||||
<p>
|
||||
Use this template to add a <em>recurring transaction</em>. According to the interval you
|
||||
specify below, Finnow will create a new draft using this template each time, which you can
|
||||
then review and submit.
|
||||
</p>
|
||||
<p>
|
||||
Each time a new draft is created from this template for recurring transactions, the new
|
||||
draft's timestamp will be set to the current date and time.
|
||||
</p>
|
||||
<AppForm>
|
||||
<h3>Monthly Schedule</h3>
|
||||
<FormGroup>
|
||||
<FormControl
|
||||
label="Day of Month"
|
||||
hint="Specify on which day of the month this transaction occurs."
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
max="31"
|
||||
v-model="dayOfMonth"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
</AppForm>
|
||||
</template>
|
||||
<template v-slot:buttons>
|
||||
<AppButton @click="addRecurringTransaction()">Add</AppButton>
|
||||
<AppButton
|
||||
button-style="secondary"
|
||||
@click="modal?.close()"
|
||||
>Cancel</AppButton
|
||||
>
|
||||
</template>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
|
|
@ -2,25 +2,34 @@
|
|||
import { ApiError } from '@/api/base'
|
||||
import { formatMoney } from '@/api/data'
|
||||
import { getSelectedProfile } from '@/api/profile'
|
||||
import { TransactionApiClient, type TransactionDraftResponse } from '@/api/transaction'
|
||||
import {
|
||||
TransactionApiClient,
|
||||
type RecurringTransactionResponse,
|
||||
type TransactionDraftResponse,
|
||||
} from '@/api/transaction'
|
||||
import AppButton from '@/components/common/AppButton.vue'
|
||||
import AppPage from '@/components/common/AppPage.vue'
|
||||
import CategoryLabel from '@/components/CategoryLabel.vue'
|
||||
import PropertiesTable from '@/components/PropertiesTable.vue'
|
||||
import TagLabel from '@/components/TagLabel.vue'
|
||||
import { showAlert, showConfirm } from '@/util/alert'
|
||||
import { computed, onMounted, ref, type Ref } from 'vue'
|
||||
import { computed, onMounted, ref, useTemplateRef, type Ref } from 'vue'
|
||||
import AttachmentRow from '@/components/common/AttachmentRow.vue'
|
||||
import LineItemCard from '@/components/LineItemCard.vue'
|
||||
import AppBadge from '@/components/common/AppBadge.vue'
|
||||
import ButtonBar from '@/components/common/ButtonBar.vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import AddRecurringTransactionModal from '@/components/AddRecurringTransactionModal.vue'
|
||||
import { CronExpressionParser } from 'cron-parser'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const transactionApi = new TransactionApiClient(getSelectedProfile(route))
|
||||
const addRecurringTransactionModal = useTemplateRef('addRecurringTransactionModal')
|
||||
|
||||
const draft: Ref<TransactionDraftResponse | undefined> = ref()
|
||||
const recurringTransactions: Ref<RecurringTransactionResponse[]> = ref([])
|
||||
|
||||
const pageTitle = computed(() => {
|
||||
if (draft.value === undefined) return 'Transaction Draft'
|
||||
if (draft.value.templateName !== null && draft.value.templateName.length > 0) {
|
||||
|
|
@ -31,8 +40,10 @@ const pageTitle = computed(() => {
|
|||
|
||||
onMounted(async () => {
|
||||
const draftId = parseInt(route.params.id as string)
|
||||
recurringTransactions.value = []
|
||||
try {
|
||||
draft.value = await transactionApi.getDraft(draftId)
|
||||
recurringTransactions.value = await transactionApi.getRecurringTransactionsForDraft(draftId)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
await router.replace('/')
|
||||
|
|
@ -61,6 +72,37 @@ async function onVendorClicked() {
|
|||
await router.push(`/profiles/${getSelectedProfile(route)}/vendors/${draft.value.vendor.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function addRecurringTransaction() {
|
||||
const result = await addRecurringTransactionModal.value?.show()
|
||||
if (result !== undefined && draft.value) {
|
||||
recurringTransactions.value = await transactionApi.getRecurringTransactionsForDraft(
|
||||
draft.value.id,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRecurringTransaction(rt: RecurringTransactionResponse) {
|
||||
if (!draft.value) return
|
||||
await transactionApi.deleteRecurringTransaction(rt.id)
|
||||
recurringTransactions.value = await transactionApi.getRecurringTransactionsForDraft(
|
||||
draft.value.id,
|
||||
)
|
||||
}
|
||||
|
||||
function formatRecurringTransactionScheduleExpr(rt: RecurringTransactionResponse) {
|
||||
const interval = CronExpressionParser.parse(rt.scheduleExpr)
|
||||
const dayOfMonth = interval.fields.dayOfMonth.serialize().values[0] as number
|
||||
return `The ${dayOfMonth}${getNumberAdjectiveSuffix(dayOfMonth)} day of every month`
|
||||
}
|
||||
|
||||
function getNumberAdjectiveSuffix(n: number) {
|
||||
const digit = n % 10
|
||||
if (digit === 1) return 'st'
|
||||
if (digit === 2) return 'nd'
|
||||
if (digit === 3) return 'rd'
|
||||
return 'th'
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<AppPage
|
||||
|
|
@ -184,5 +226,35 @@ async function onVendorClicked() {
|
|||
>Delete</AppButton
|
||||
>
|
||||
</ButtonBar>
|
||||
<ButtonBar>
|
||||
<AppButton
|
||||
icon="repeat"
|
||||
@click="addRecurringTransaction()"
|
||||
>
|
||||
Add Recurring Transaction
|
||||
</AppButton>
|
||||
</ButtonBar>
|
||||
|
||||
<div v-if="recurringTransactions.length > 0">
|
||||
<h3>Recurring Transactions</h3>
|
||||
<p>This template will be used to create new drafts using the following schedules:</p>
|
||||
<ul>
|
||||
<li
|
||||
v-for="rt in recurringTransactions"
|
||||
:key="rt.id"
|
||||
>
|
||||
{{ formatRecurringTransactionScheduleExpr(rt) }}
|
||||
<AppButton
|
||||
icon="trash"
|
||||
@click="deleteRecurringTransaction(rt)"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<AddRecurringTransactionModal
|
||||
ref="addRecurringTransactionModal"
|
||||
:draft-id="draft.id"
|
||||
/>
|
||||
</AppPage>
|
||||
</template>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue