diff --git a/app/Http/Controllers/Recurring/EditController.php b/app/Http/Controllers/Recurring/EditController.php
index 82b124f1ea..57bb5b18b5 100644
--- a/app/Http/Controllers/Recurring/EditController.php
+++ b/app/Http/Controllers/Recurring/EditController.php
@@ -150,7 +150,11 @@ final class EditController extends Controller
];
$array['first_date'] = substr((string) $array['first_date'], 0, 10);
$array['repeat_until'] = substr((string) $array['repeat_until'], 0, 10);
- $array['transactions'][0]['tags'] = implode(',', $array['transactions'][0]['tags'] ?? []);
+ $tags = [];
+ foreach($array['transactions'][0]['tags'] as $tag) {
+ $tags[$tag] = $tag;
+ }
+ $array['transactions'][0]['tags'] = $tags;
$array['transactions'][0]['amount'] = round((float) $array['transactions'][0]['amount'], $array['transactions'][0]['currency_decimal_places']);
if (null !== $array['transactions'][0]['foreign_amount'] && '' !== $array['transactions'][0]['foreign_amount']) {
$array['transactions'][0]['foreign_amount'] = round(
diff --git a/resources/assets/v3/js/form/create-autocomplete.js b/resources/assets/v3/js/form/create-autocomplete.js
new file mode 100644
index 0000000000..32fb4230d0
--- /dev/null
+++ b/resources/assets/v3/js/form/create-autocomplete.js
@@ -0,0 +1,44 @@
+/*
+ * create-autocomplete.js
+ * Copyright (c) 2026 james@firefly-iii.org
+ *
+ * This file is part of Firefly III (https://github.com/firefly-iii).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import Autocomplete from "bootstrap5-autocomplete";
+
+
+function createAutocomplete(fieldIdentifier, url) {
+ let token = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
+ Autocomplete.init('#' + fieldIdentifier, {
+
+ server: url + '?_token=' + token,
+ labelField: 'name',
+ valueField: 'name',
+ liveServer: true,
+ fetchOptions: {
+ method: 'GET',
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ 'X-CSRF-TOKEN': token
+ }
+ }
+ });
+}
+
+export {createAutocomplete}
diff --git a/resources/assets/v3/js/form/create-tag-field.js b/resources/assets/v3/js/form/create-tag-field.js
new file mode 100644
index 0000000000..ee27599505
--- /dev/null
+++ b/resources/assets/v3/js/form/create-tag-field.js
@@ -0,0 +1,44 @@
+/*
+ * create-tag-field.js
+ * Copyright (c) 2026 james@firefly-iii.org
+ *
+ * This file is part of Firefly III (https://github.com/firefly-iii).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import Tags from "bootstrap5-tags";
+function createTagField(fieldIdentifier) {
+ let token = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
+ Tags.init('#' + fieldIdentifier,
+ {
+ allowNew: true,
+ allowClear: true,
+ server: './api/v1/autocomplete/tags?_token=' + token,
+ liveServer: true,
+ labelField: 'name',
+ valueField: 'name',
+ fetchOptions: {
+ method: 'GET',
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ 'X-CSRF-TOKEN': token
+ }
+ }
+ }
+ );
+}
+
+export {createTagField};
diff --git a/resources/assets/v3/js/pages/recurring/create.js b/resources/assets/v3/js/pages/recurring/create.js
index fa2096bfe4..eb224dbc73 100644
--- a/resources/assets/v3/js/pages/recurring/create.js
+++ b/resources/assets/v3/js/pages/recurring/create.js
@@ -21,58 +21,56 @@
import '../../boot/bootstrap.js';
import sidebar from '../../pages/shared/sidebar.js';
import dates from '../shared/dates.js';
-import Autocomplete from "bootstrap5-autocomplete";
-import Tags from "bootstrap5-tags";
+
import {Calendar} from 'fullcalendar';
import themePlugin from 'fullcalendar/themes/monarch'; // YOUR THEME
import dayGridPlugin from 'fullcalendar/daygrid';
import timeGridPlugin from 'fullcalendar/timegrid';
import listPlugin from 'fullcalendar/list';
-
+import {createTagField} from '../../form/create-tag-field.js';
+import {createAutocomplete} from '../../form/create-autocomplete.js';
+import {createButtonSwitcher} from './shared/create-button-switcher.js';
+import {respondToFirstDateChange} from './shared/respond-to-first-date-change.js';
+import {switchTransactionType} from './shared/switch-transaction-type.js';
+import {respondToRepetitionEnd} from './shared/respond-to-repetition-end.js';
// stylesheets
import 'fullcalendar/skeleton.css'; // ALWAYS NEED SKELETON
import 'fullcalendar/themes/monarch/theme.css'; // YOUR THEME
import 'fullcalendar/themes/monarch/palettes/purple.css'; // YOUR THEME'S PALETTE
-let calendarEl = document.getElementById('recurring_calendar');
-let calendar = new Calendar(calendarEl, {
- plugins: [themePlugin, dayGridPlugin, timeGridPlugin, listPlugin],
- initialView: 'dayGridMonth',
- headerToolbar: {
- left: 'prev,next today',
- center: 'title',
- right: 'dayGridMonth,timeGridWeek,listWeek'
- }
-});
-calendar.render();
-
let create = function () {
return {
calendar: null,
eventSource: null,
init() {
- console.log('Init recurring create');
- this.createTagField();
- this.createAutocomplete();
- this.createButtonSwitcher();
- this.switchTransactionType('withdrawal');
- this.respondToFirstDateChange();
- this.respondToRepetitionEnd();
- document.getElementById('ffInput_first_date').addEventListener('change', this.respondToFirstDateChange.bind(this));
- document.getElementById('ffInput_repetition_end').addEventListener('change', this.respondToRepetitionEnd.bind(this));
+ createTagField('ffInput_tags');
+ createAutocomplete('ffInput_category', './api/v1/autocomplete/categories');
+ createButtonSwitcher();
+ switchTransactionType('withdrawal');
+ let type = document.getElementById('repetitionType').value;
+ let suggestUrl = document.getElementById('suggestUrl').value;
+ respondToFirstDateChange(type, suggestUrl);
+ respondToRepetitionEnd();
+ document.getElementById('ffInput_first_date').addEventListener('change', respondToFirstDateChange.bind(this));
+ document.getElementById('ffInput_repetition_end').addEventListener('change', respondToRepetitionEnd.bind(this));
- // TODO create calendar
+
+ let calendarEl = document.getElementById('recurring_calendar');
this.calendar = new Calendar(calendarEl, {
- plugins: [dayGridPlugin],
- initialView: 'dayGridMonth'
+ plugins: [themePlugin, dayGridPlugin, timeGridPlugin, listPlugin],
+ initialView: 'dayGridMonth',
+ headerToolbar: {
+ left: 'prev,next today',
+ center: 'title'
+ }
});
-
- document.getElementById('calendar-link').addEventListener('click', this.showRepCalendar);
+ this.calendar.render();
+ document.getElementById('calendar-link').addEventListener('click', this.showRepCalendar.bind(this));
},
showRepCalendar() {
-
+ let eventsUrl = document.getElementById('eventsUrl').value;
// pre-append URL with repetition info:
let newEventsUrl = eventsUrl + '?type=' + document.getElementById('ffInput_repetition_type').value;
newEventsUrl += '&skip=' + document.getElementById('ffInput_skip').value;
@@ -82,199 +80,13 @@ let create = function () {
newEventsUrl += '&first_date=' + document.getElementById('ffInput_first_date').value;
newEventsUrl += '&weekend=' + document.getElementById('ffInput_weekend').value;
- console.log(newEventsUrl);
-
- // remove all event sources from calendar:
let eventSource = new EventSource(newEventsUrl);
- calendar.removeAllEventSources();
- calendar.addEventSource(eventSource);
+ this.calendar.removeAllEventSources();
+ this.calendar.addEventSource(eventSource);
$('#calendarModal').modal('show');
return false;
- },
- respondToRepetitionEnd() {
- var obj = document.getElementById('ffInput_repetition_end');
- var value = obj.value;
- switch (value) {
- case 'forever':
- document.getElementById('repeat_until_holder').style.display = 'none';
- document.getElementById('repetitions_holder').style.display = 'none';
- break;
- case 'until_date':
- document.getElementById('repeat_until_holder').style.display = 'block';
- document.getElementById('repetitions_holder').style.display = 'none';
-
- break;
- case 'times':
- document.getElementById('repeat_until_holder').style.display = 'none';
- document.getElementById('repetitions_holder').style.display = 'block';
- break;
- }
- },
- respondToFirstDateChange() {
- let obj = document.getElementById('ffInput_first_date');
- let select = document.getElementById('ffInput_repetition_type');
- let date = obj.value;
- select.disabled = true;
-
- // preselected value:
- var preSelected = oldRepetitionType;
- if (preSelected === '') {
- preSelected = select.value;
- }
-
- $.getJSON(suggestUrl, {date: date, pre_select: preSelected, past: 'true'}).fail(function () {
- console.error('Could not load repetition suggestions');
- alert('Could not load repetition suggestions. Please enter a valid date.');
- }).done(this.parseRepetitionSuggestions);
- },
- parseRepetitionSuggestions(data) {
- let select = document.getElementById('ffInput_repetition_type');
- select.innerHTML = '';
- let opt;
- for (var k in data) {
- if (data.hasOwnProperty(k)) {
- console.log('label: ' + data[k].label + ', selected: ' + data[k].selected);
- opt = document.createElement('option');
- opt.value = k;
- opt.label = data[k].label;
- opt.text = data[k].label;
- if (data[k].selected) {
- opt.selected = true;
- }
- select.appendChild(opt);
- }
- }
- select.disabled = false;
- },
-
-
- createButtonSwitcher() {
- console.log('Now in initializeButtons()');
- let list = document.getElementsByClassName('switch-button');
- for (let i = 0; i < list.length; i++) {
-
- list[i].addEventListener('click', (event) => {
- let transactionType = event.currentTarget.dataset.value;
- // console.log('Clicked value is ' + transactionType);
- this.switchTransactionType(transactionType);
- return false;
- });
- }
- // $.each($('.switch-button'), function (i, v) {
- // //var btn = $(v);
- //
- // if (btn.data('value') === transactionType) {
- // btn.addClass('btn-info disabled').removeClass('btn-default');
- // $('input[name="transaction_type"]').val(transactionType);
- // } else {
- // btn.removeClass('btn-info disabled').addClass('btn-default');
- // }
- // });
- // updateFormFields();
- },
- switchTransactionType(transactionType) {
- let list = document.getElementsByClassName('switch-button');
- for (let i = 0; i < list.length; i++) {
- let currentType = list[i].dataset.value;
- console.log('Selected is ' + currentType + ', form type is ' + transactionType);
- if (currentType === transactionType) {
- list[i].classList.add('btn-primary');
- list[i].classList.remove('btn-secondary');
- }
- if (currentType !== transactionType) {
- list[i].classList.remove('btn-primary');
- list[i].classList.add('btn-secondary');
- }
- }
- if ('withdrawal' === transactionType) {
- // hide source account name:
- document.getElementById('deposit_source_id_holder').style.display = 'none';
-
- // // show source account ID:
- document.getElementById('source_id_holder').style.display = 'block';
-
- // show destination name:
- document.getElementById('withdrawal_destination_id_holder').style.display = 'block';
-
- // // hide destination ID:
- document.getElementById('destination_id_holder').style.display = 'none';
-
- // show budget + bill
- document.getElementById('budget_id_holder').style.display = 'block';
- document.getElementById('bill_id_holder').style.display = 'block';
-
- // hide piggy bank:
- document.getElementById('piggy_bank_id_holder').style.display = 'none';
- }
-
- if (transactionType === 'deposit') {
- document.getElementById('deposit_source_id_holder').style.display = 'block';
-
- document.getElementById('source_id_holder').style.display = 'none';
-
- document.getElementById('withdrawal_destination_id_holder').style.display = 'none';
-
- document.getElementById('destination_id_holder').style.display = 'block';
- document.getElementById('budget_id_holder').style.display = 'none';
- document.getElementById('bill_id_holder').style.display = 'none';
- document.getElementById('piggy_bank_id_holder').style.display = 'none';
- }
-
- if (transactionType === 'transfer') {
- document.getElementById('deposit_source_id_holder').style.display = 'none';
- document.getElementById('source_id_holder').style.display = 'block';
- document.getElementById('withdrawal_destination_id_holder').style.display = 'none';
- document.getElementById('destination_id_holder').style.display = 'block';
- document.getElementById('budget_id_holder').style.display = 'none';
- document.getElementById('bill_id_holder').style.display = 'none';
- document.getElementById('piggy_bank_id_holder').style.display = 'block';
- }
- },
- createTagField() {
- let token = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
- Tags.init('#ffInput_tags',
- {
- allowNew: true,
- allowClear: true,
- server: './api/v1/autocomplete/tags?_token=' + token,
- liveServer: true,
- labelField: 'name',
- valueField: 'name',
- fetchOptions: {
- method: 'GET',
- credentials: 'include',
- headers: {
- 'Content-Type': 'application/json',
- 'Accept': 'application/json',
- 'X-CSRF-TOKEN': token
- }
- }
- }
- );
- },
- createAutocomplete() {
- this.createCategoryAutocomplete();
- },
- createCategoryAutocomplete() {
- let token = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
- Autocomplete.init('#ffInput_category', {
-
- server: './api/v1/autocomplete/categories?_token=' + token,
- labelField: 'name',
- valueField: 'name',
- liveServer: true,
- fetchOptions: {
- method: 'GET',
- credentials: 'include',
- headers: {
- 'Content-Type': 'application/json',
- 'Accept': 'application/json',
- 'X-CSRF-TOKEN': token
- }
- }
- });
- },
+ }
}
};
diff --git a/resources/assets/v3/js/pages/recurring/edit.js b/resources/assets/v3/js/pages/recurring/edit.js
new file mode 100644
index 0000000000..6127ccd97c
--- /dev/null
+++ b/resources/assets/v3/js/pages/recurring/edit.js
@@ -0,0 +1,117 @@
+/*
+ * edit.js
+ * Copyright (c) 2026 james@firefly-iii.org
+ *
+ * This file is part of Firefly III (https://github.com/firefly-iii).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+
+import '../../boot/bootstrap.js';
+import sidebar from '../../pages/shared/sidebar.js';
+import dates from '../shared/dates.js';
+import {createTagField} from "../../form/create-tag-field.js";
+import {createAutocomplete} from "../../form/create-autocomplete.js";
+import {createButtonSwitcher} from "./shared/create-button-switcher.js";
+import {switchTransactionType} from "./shared/switch-transaction-type.js";
+import {respondToFirstDateChange} from './shared/respond-to-first-date-change.js';
+import {respondToRepetitionEnd} from './shared/respond-to-repetition-end.js';
+import {Calendar} from "fullcalendar";
+import themePlugin from "fullcalendar/themes/monarch";
+import dayGridPlugin from "fullcalendar/daygrid";
+import timeGridPlugin from "fullcalendar/timegrid";
+import listPlugin from "fullcalendar/list";
+
+// stylesheets
+import 'fullcalendar/skeleton.css'; // ALWAYS NEED SKELETON
+import 'fullcalendar/themes/monarch/theme.css'; // YOUR THEME
+import 'fullcalendar/themes/monarch/palettes/purple.css'; // YOUR THEME'S PALETTE
+
+let edit = function () {
+ return {
+ init() {
+ createTagField('ffInput_tags');
+ createAutocomplete('ffInput_category','./api/v1/autocomplete/categories');
+ createButtonSwitcher();
+ switchTransactionType(document.getElementsByName('transaction_type')[0].value);
+
+ let type = document.getElementById('repetitionType').value;
+ let suggestUrl = document.getElementById('suggestUrl').value;
+ respondToFirstDateChange(type, suggestUrl);
+ respondToRepetitionEnd();
+ document.getElementById('ffInput_first_date').addEventListener('change', respondToFirstDateChange.bind(this));
+ document.getElementById('ffInput_repetition_end').addEventListener('change', respondToRepetitionEnd.bind(this));
+
+ let calendarEl = document.getElementById('recurring_calendar');
+ this.calendar = new Calendar(calendarEl, {
+ plugins: [themePlugin, dayGridPlugin, timeGridPlugin, listPlugin],
+ initialView: 'dayGridMonth',
+ headerToolbar: {
+ left: 'prev,next today',
+ center: 'title'
+ }
+ });
+ this.calendar.render();
+ document.getElementById('calendar-link').addEventListener('click', this.showRepCalendar.bind(this));
+ },
+
+ showRepCalendar() {
+ let eventsUrl = document.getElementById('eventsUrl').value;
+ // pre-append URL with repetition info:
+ let newEventsUrl = eventsUrl + '?type=' + document.getElementById('ffInput_repetition_type').value;
+ newEventsUrl += '&skip=' + document.getElementById('ffInput_skip').value;
+ newEventsUrl += '&ends=' + document.getElementById('ffInput_repetition_end').value;
+ newEventsUrl += '&end_date=' + document.getElementById('ffInput_repeat_until').value;
+ newEventsUrl += '&reps=' + document.getElementById('ffInput_repetitions').value;
+ newEventsUrl += '&first_date=' + document.getElementById('ffInput_first_date').value;
+ newEventsUrl += '&weekend=' + document.getElementById('ffInput_weekend').value;
+
+ let eventSource = new EventSource(newEventsUrl);
+ this.calendar.removeAllEventSources();
+ this.calendar.addEventSource(eventSource);
+ $('#calendarModal').modal('show');
+
+ return false;
+ }
+ }
+};
+
+
+const comps = {
+ edit,
+ sidebar,
+ dates
+};
+
+function loadPage(comps) {
+ // console.log('loadPage');
+ Object.keys(comps).forEach(comp => {
+ let data = comps[comp]();
+ Alpine.data(comp, () => data);
+ // console.log(comp);
+ });
+ Alpine.start();
+}
+
+// wait for load until bootstrapped event is received.
+document.addEventListener('firefly-iii-bootstrapped', () => {
+ // console.log('Loaded through event listener.');
+ loadPage(comps);
+});
+// or is bootstrapped before event is triggered.
+if (window.bootstrapped) {
+ // console.log('Loaded through window variable.');
+ loadPage(comps);
+}
diff --git a/resources/assets/v3/js/pages/recurring/shared/create-button-switcher.js b/resources/assets/v3/js/pages/recurring/shared/create-button-switcher.js
new file mode 100644
index 0000000000..052584a16b
--- /dev/null
+++ b/resources/assets/v3/js/pages/recurring/shared/create-button-switcher.js
@@ -0,0 +1,36 @@
+/*
+ * create-button-switcher.js
+ * Copyright (c) 2026 james@firefly-iii.org
+ *
+ * This file is part of Firefly III (https://github.com/firefly-iii).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import {switchTransactionType} from "./switch-transaction-type.js";
+
+function createButtonSwitcher() {
+ let list = document.getElementsByClassName('switch-button');
+ for (let i = 0; i < list.length; i++) {
+
+ list[i].addEventListener('click', (event) => {
+ let transactionType = event.currentTarget.dataset.value;
+ // console.log('Clicked value is ' + transactionType);
+ switchTransactionType(transactionType);
+ return false;
+ });
+ }
+}
+
+export {createButtonSwitcher};
diff --git a/resources/assets/v3/js/pages/recurring/shared/respond-to-first-date-change.js b/resources/assets/v3/js/pages/recurring/shared/respond-to-first-date-change.js
new file mode 100644
index 0000000000..8d4725e1ed
--- /dev/null
+++ b/resources/assets/v3/js/pages/recurring/shared/respond-to-first-date-change.js
@@ -0,0 +1,59 @@
+/*
+ * respond-to-first-date-change.js
+ * Copyright (c) 2026 james@firefly-iii.org
+ *
+ * This file is part of Firefly III (https://github.com/firefly-iii).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+function parseRepetitionSuggestions(data) {
+ let select = document.getElementById('ffInput_repetition_type');
+ select.innerHTML = '';
+ let opt;
+ for (var k in data) {
+ if (data.hasOwnProperty(k)) {
+ console.log('label: ' + data[k].label + ', selected: ' + data[k].selected);
+ opt = document.createElement('option');
+ opt.value = k;
+ opt.label = data[k].label;
+ opt.text = data[k].label;
+ if (data[k].selected) {
+ opt.selected = true;
+ }
+ select.appendChild(opt);
+ }
+ }
+ select.disabled = false;
+}
+
+function respondToFirstDateChange(oldRepetitionType, suggestUrl) {
+ let obj = document.getElementById('ffInput_first_date');
+ let select = document.getElementById('ffInput_repetition_type');
+ let date = obj.value;
+ select.disabled = true;
+
+ // preselected value:
+ var preSelected = oldRepetitionType;
+ if (preSelected === '') {
+ preSelected = select.value;
+ }
+
+ $.getJSON(suggestUrl, {date: date, pre_select: preSelected, past: 'true'}).fail(function () {
+ console.error('Could not load repetition suggestions');
+ alert('Could not load repetition suggestions. Please enter a valid date.');
+ }).done(parseRepetitionSuggestions);
+}
+
+export {respondToFirstDateChange}
diff --git a/resources/assets/v3/js/pages/recurring/shared/respond-to-repetition-end.js b/resources/assets/v3/js/pages/recurring/shared/respond-to-repetition-end.js
new file mode 100644
index 0000000000..0841209c01
--- /dev/null
+++ b/resources/assets/v3/js/pages/recurring/shared/respond-to-repetition-end.js
@@ -0,0 +1,41 @@
+/*
+ * respond-to-repetition-end.js
+ * Copyright (c) 2026 james@firefly-iii.org
+ *
+ * This file is part of Firefly III (https://github.com/firefly-iii).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+function respondToRepetitionEnd() {
+ var obj = document.getElementById('ffInput_repetition_end');
+ var value = obj.value;
+ switch (value) {
+ case 'forever':
+ document.getElementById('repeat_until_holder').style.display = 'none';
+ document.getElementById('repetitions_holder').style.display = 'none';
+ break;
+ case 'until_date':
+ document.getElementById('repeat_until_holder').style.display = 'block';
+ document.getElementById('repetitions_holder').style.display = 'none';
+
+ break;
+ case 'times':
+ document.getElementById('repeat_until_holder').style.display = 'none';
+ document.getElementById('repetitions_holder').style.display = 'block';
+ break;
+ }
+}
+
+export {respondToRepetitionEnd}
diff --git a/resources/assets/v3/js/pages/recurring/shared/switch-transaction-type.js b/resources/assets/v3/js/pages/recurring/shared/switch-transaction-type.js
new file mode 100644
index 0000000000..fe9743c41b
--- /dev/null
+++ b/resources/assets/v3/js/pages/recurring/shared/switch-transaction-type.js
@@ -0,0 +1,78 @@
+/*
+ * switch-transaction-type.js
+ * Copyright (c) 2026 james@firefly-iii.org
+ *
+ * This file is part of Firefly III (https://github.com/firefly-iii).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+function switchTransactionType(transactionType) {
+ let list = document.getElementsByClassName('switch-button');
+ for (let i = 0; i < list.length; i++) {
+ let currentType = list[i].dataset.value;
+ if (currentType === transactionType) {
+ list[i].classList.add('btn-primary');
+ list[i].classList.remove('btn-secondary');
+ }
+ if (currentType !== transactionType) {
+ list[i].classList.remove('btn-primary');
+ list[i].classList.add('btn-secondary');
+ }
+ }
+ if ('withdrawal' === transactionType) {
+ // hide source account name:
+ document.getElementById('deposit_source_id_holder').style.display = 'none';
+
+ // // show source account ID:
+ document.getElementById('source_id_holder').style.display = 'block';
+
+ // show destination name:
+ document.getElementById('withdrawal_destination_id_holder').style.display = 'block';
+
+ // // hide destination ID:
+ document.getElementById('destination_id_holder').style.display = 'none';
+
+ // show budget + bill
+ document.getElementById('budget_id_holder').style.display = 'block';
+ document.getElementById('bill_id_holder').style.display = 'block';
+
+ // hide piggy bank:
+ document.getElementById('piggy_bank_id_holder').style.display = 'none';
+ }
+
+ if (transactionType === 'deposit') {
+ document.getElementById('deposit_source_id_holder').style.display = 'block';
+
+ document.getElementById('source_id_holder').style.display = 'none';
+
+ document.getElementById('withdrawal_destination_id_holder').style.display = 'none';
+
+ document.getElementById('destination_id_holder').style.display = 'block';
+ document.getElementById('budget_id_holder').style.display = 'none';
+ document.getElementById('bill_id_holder').style.display = 'none';
+ document.getElementById('piggy_bank_id_holder').style.display = 'none';
+ }
+
+ if (transactionType === 'transfer') {
+ document.getElementById('deposit_source_id_holder').style.display = 'none';
+ document.getElementById('source_id_holder').style.display = 'block';
+ document.getElementById('withdrawal_destination_id_holder').style.display = 'none';
+ document.getElementById('destination_id_holder').style.display = 'block';
+ document.getElementById('budget_id_holder').style.display = 'none';
+ document.getElementById('bill_id_holder').style.display = 'none';
+ document.getElementById('piggy_bank_id_holder').style.display = 'block';
+ }
+}
+export {switchTransactionType};
diff --git a/resources/assets/v3/vite.config.js b/resources/assets/v3/vite.config.js
index be6571865a..ff4d009ef3 100644
--- a/resources/assets/v3/vite.config.js
+++ b/resources/assets/v3/vite.config.js
@@ -72,6 +72,7 @@ export default defineConfig(({command, mode, isSsrBuild, isPreview}) => {
// recurring transactions
'js/pages/recurring/create.js',
+ 'js/pages/recurring/edit.js',
// subscriptions
diff --git a/resources/views/recurring/create.blade.php b/resources/views/recurring/create.blade.php
index 005760b324..0d69697d46 100644
--- a/resources/views/recurring/create.blade.php
+++ b/resources/views/recurring/create.blade.php
@@ -146,7 +146,7 @@
{{-- TAGS --}}
{{-- ExpandedForm::text('tags') --}}
- {!! ExpandedForm::multiSelect('tags', [],null, ['multiple'=> 'multiple']) !!}
+ {!! ExpandedForm::multiSelect('tags', [], null) !!}
{{-- RELATE THIS TRANSFER TO A PIGGY BANK --}}
{!! PiggyBankForm::piggyBankList('piggy_bank_id') !!}
@@ -208,13 +208,11 @@
+
+
+
@endsection
@section('scripts')
-
@vite(['js/pages/recurring/create.js'])