From 90a7c5dd18b7f3554dc2ba5610307bcc7fc57568 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Fri, 10 Apr 2026 09:23:26 +0200 Subject: [PATCH] started to implement updates on transactions Signed-off-by: Stephan Richter --- .../umbrella/accounting/AccountDb.java | 2 + .../umbrella/accounting/AccountingModule.java | 36 +++++++++++ .../umbrella/accounting/SqliteDb.java | 44 +++++++++---- .../umbrella/core/constants/Path.java | 7 ++- .../umbrella/core/constants/Text.java | 1 + .../umbrella/core/model/Transaction.java | 62 ++++++++++++++++++- frontend/src/Components/LineEditor.svelte | 9 +-- frontend/src/routes/accounting/account.svelte | 27 +------- .../src/routes/accounting/transaction.svelte | 49 +++++++++++++++ frontend/src/routes/contact/Address.svelte | 14 ++--- frontend/src/routes/contact/Email.svelte | 2 +- frontend/src/routes/contact/ExtraField.svelte | 2 +- frontend/src/routes/contact/FN.svelte | 2 +- frontend/src/routes/contact/Name.svelte | 10 +-- frontend/src/routes/contact/Number.svelte | 2 +- frontend/src/routes/contact/Org.svelte | 2 +- frontend/src/routes/contact/URL.svelte | 2 +- frontend/src/routes/stock/Index.svelte | 2 +- frontend/src/routes/stock/ItemProps.svelte | 4 +- frontend/src/routes/task/ListTask.svelte | 2 +- frontend/src/routes/wiki/View.svelte | 2 +- 21 files changed, 215 insertions(+), 68 deletions(-) create mode 100644 frontend/src/routes/accounting/transaction.svelte diff --git a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountDb.java b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountDb.java index 1657ccd9..0c96721f 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountDb.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountDb.java @@ -12,6 +12,8 @@ public interface AccountDb { Account loadAccount(long accountId); + Transaction loadTransaction(long transactionId); + List loadTransactions(Account account); Account save(Account account); diff --git a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java index 356c9624..b8315a55 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java @@ -63,6 +63,30 @@ public class AccountingModule extends BaseHandler implements AccountingService { } } + @Override + public boolean doPatch(Path path, HttpExchange ex) throws IOException { + addCors(ex); + try { + var user = userService().refreshSession(ex); + if (user.isEmpty()) return unauthorized(ex); + var head = path.pop(); + return switch (head) { + case TRANSACTION -> { + try { + var tid = Long.parseLong(path.pop()); + yield patchTransaction(user.get(),tid,ex); + } catch (NumberFormatException ignored) { + yield super.doPatch(path,ex); + } + + } + case null, default -> super.doPatch(path, ex); + }; + } catch (UmbrellaException e){ + return send(ex,e); + } + } + @Override public boolean doPost(Path path, HttpExchange ex) throws IOException { addCors(ex); @@ -131,6 +155,18 @@ public class AccountingModule extends BaseHandler implements AccountingService { } } + private boolean patchTransaction(UmbrellaUser user, long transactionId, HttpExchange ex) throws IOException { + var transaction = accountDb.loadTransaction(transactionId); + LOG.log(WARNING,"Missing permission check in patchTransaction(…)!"); + var json = json(ex); + if (json.has(Field.DATE)){ + var date = LocalDate.parse(json.getString(Field.DATE)); + transaction.date(date); + accountDb.save(transaction); + } + return sendContent(ex,transaction); + } + private boolean postEntry(UmbrellaUser user, HttpExchange ex) throws IOException { var json = json(ex); if (!json.has(Field.ACCOUNT)) throw missingField(Field.ACCOUNT); diff --git a/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java b/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java index 7985b35f..a719f910 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java @@ -6,6 +6,7 @@ import static de.srsoftware.tools.jdbc.Condition.*; import static de.srsoftware.tools.jdbc.Query.*; import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL; import static de.srsoftware.umbrella.accounting.Constants.*; +import static de.srsoftware.umbrella.core.constants.Field.ID; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*; import static de.srsoftware.umbrella.core.model.Translatable.t; import static java.text.MessageFormat.format; @@ -51,7 +52,7 @@ public class SqliteDb extends BaseDb implements AccountDb { );"""; try { - sql = format(sql,TABLE_ACCOUNTS, Field.ID, Field.NAME, Field.OWNER, Field.CURRENCY); + sql = format(sql,TABLE_ACCOUNTS, ID, Field.NAME, Field.OWNER, Field.CURRENCY); var stmt = db.prepareStatement(sql); stmt.execute(); stmt.close(); @@ -68,7 +69,7 @@ public class SqliteDb extends BaseDb implements AccountDb { ); """; try { - sql = format(sql,TABLE_TAGS,Field.ID,Field.TAG); + sql = format(sql,TABLE_TAGS, ID,Field.TAG); var stmt = db.prepareStatement(sql); stmt.execute(); stmt.close(); @@ -107,7 +108,7 @@ public class SqliteDb extends BaseDb implements AccountDb { {6} TEXT );"""; try { - sql = format(sql,TABLE_TRANSACTIONS,Field.ACCOUNT,Field.TIMESTAMP,Field.SOURCE,Field.DESTINATION, Field.AMOUNT,Field.DESCRIPTION, Field.ID); + sql = format(sql,TABLE_TRANSACTIONS,Field.ACCOUNT,Field.TIMESTAMP,Field.SOURCE,Field.DESTINATION, Field.AMOUNT,Field.DESCRIPTION, ID); var stmt = db.prepareStatement(sql); stmt.execute(); stmt.close(); @@ -127,7 +128,7 @@ public class SqliteDb extends BaseDb implements AccountDb { while (rs.next()) accountIds.add(rs.getLong(1)); rs.close(); var accounts = new HashSet(); - rs = select(ALL).from(TABLE_ACCOUNTS).where(Field.ID, Condition.in(accountIds.toArray())).exec(db); + rs = select(ALL).from(TABLE_ACCOUNTS).where(ID, Condition.in(accountIds.toArray())).exec(db); while (rs.next()) accounts.add(Account.of(rs)); rs.close(); return accounts; @@ -139,7 +140,7 @@ public class SqliteDb extends BaseDb implements AccountDb { @Override public Account loadAccount(long accountId) { try { - var rs = select(ALL).from(TABLE_ACCOUNTS).where(Field.ID,equal(accountId)).exec(db); + var rs = select(ALL).from(TABLE_ACCOUNTS).where(ID,equal(accountId)).exec(db); Account account = null; if (rs.next()) account = Account.of(rs); rs.close(); @@ -150,6 +151,20 @@ public class SqliteDb extends BaseDb implements AccountDb { } } + @Override + public Transaction loadTransaction(long transactionId) { + try { + Transaction transaction = null; + var rs = select(ALL).from(TABLE_TRANSACTIONS).where(ID,equal(transactionId)).exec(db); + if (rs.next()) transaction = Transaction.of(rs); + rs.close(); + if (transaction != null) return transaction; + throw failedToLoadObject(Text.TRANSACTION,transactionId); + } catch (SQLException e) { + throw failedToLoadObject(Text.TRANSACTION); + } + } + @Override public List loadTransactions(Account account) { try { @@ -161,7 +176,7 @@ public class SqliteDb extends BaseDb implements AccountDb { } rs.close(); var transactionIds = transactions.keySet().toArray(); - rs = select(ALL).from(TABLE_TAGS_TRANSACTIONS).leftJoin(Field.TAG_ID,TABLE_TAGS,Field.ID).where(Field.TRANSACTION_ID,in(transactionIds)).exec(db); + rs = select(ALL).from(TABLE_TAGS_TRANSACTIONS).leftJoin(Field.TAG_ID,TABLE_TAGS, ID).where(Field.TRANSACTION_ID,in(transactionIds)).exec(db); while (rs.next()) { var transaction = transactions.get(rs.getLong(Field.TRANSACTION_ID)); if (transaction != null) transaction.tags().add(rs.getString(Field.TAG)); @@ -191,9 +206,9 @@ public class SqliteDb extends BaseDb implements AccountDb { @Override public Transaction save(Transaction transaction) { + var timestamp = transaction.date().toEpochSecond(ZoneOffset.UTC); if (transaction.id() == 0) { try { - var timestamp = transaction.date().toEpochSecond(ZoneOffset.UTC); var rs = Query.insertInto(TABLE_TRANSACTIONS, Field.ACCOUNT, Field.TIMESTAMP, Field.SOURCE, Field.DESTINATION, Field.AMOUNT, Field.DESCRIPTION) .values(transaction.accountId(), timestamp, transaction.source().value(), transaction.destination().value(), transaction.amount(), transaction.purpose()) .execute(db).getGeneratedKeys(); @@ -202,8 +217,15 @@ public class SqliteDb extends BaseDb implements AccountDb { } catch (SQLException e) { throw failedToStoreObject(transaction); } - } else { // TODO : implement update - throw UmbrellaException.failedToStoreObject(transaction); + } else { + try { + Query.replaceInto(TABLE_TRANSACTIONS, Field.ID, Field.ACCOUNT, Field.TIMESTAMP, Field.SOURCE, Field.DESTINATION, Field.AMOUNT, Field.DESCRIPTION) + .values(transaction.id(), transaction.accountId(), timestamp, transaction.source().value(), transaction.destination().value(), transaction.amount(), transaction.purpose()) + .execute(db).close(); + return transaction; + } catch (SQLException e) { + throw failedToStoreObject(transaction); + } } return saveTags(transaction); } @@ -213,7 +235,7 @@ public class SqliteDb extends BaseDb implements AccountDb { var existingTags = new HashMap(); try { var rs = select(ALL).from(TABLE_TAGS).where(Field.TAG,in(transaction.tags().toArray())).exec(db); - while (rs.next()) existingTags.put(rs.getString(Field.TAG), rs.getLong(Field.ID)); + while (rs.next()) existingTags.put(rs.getString(Field.TAG), rs.getLong(ID)); rs.close(); } catch (SQLException e){ throw failedToLoadMembers(transaction); @@ -257,7 +279,7 @@ public class SqliteDb extends BaseDb implements AccountDb { public Set searchTagsContaining(String key, long accountId) { try { var tags = new HashSet(); - var rs = select(ALL).from(TABLE_TRANSACTIONS).leftJoin(Field.ID,TABLE_TAGS_TRANSACTIONS,Field.TRANSACTION_ID).leftJoin(Field.TAG_ID,TABLE_TAGS,Field.ID).where(Field.TAG,like(format("%{0}%",key))).exec(db); + var rs = select(ALL).from(TABLE_TRANSACTIONS).leftJoin(ID,TABLE_TAGS_TRANSACTIONS,Field.TRANSACTION_ID).leftJoin(Field.TAG_ID,TABLE_TAGS, ID).where(Field.TAG,like(format("%{0}%",key))).exec(db); while (rs.next()) tags.add(rs.getString(Field.TAG)); rs.close(); return tags; diff --git a/core/src/main/java/de/srsoftware/umbrella/core/constants/Path.java b/core/src/main/java/de/srsoftware/umbrella/core/constants/Path.java index 04af091f..132ed457 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/constants/Path.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/constants/Path.java @@ -53,9 +53,10 @@ public class Path { public static final String STATE = "state"; public static final String STOP = "stop"; - public static final String TAGS = "tags"; - public static final String TAGGED = "tagged"; - public static final String TOKEN = "token"; + public static final String TAGS = "tags"; + public static final String TAGGED = "tagged"; + public static final String TRANSACTION = "transaction"; + public static final String TOKEN = "token"; public static final String USER = "user"; public static final String USES = "uses"; diff --git a/core/src/main/java/de/srsoftware/umbrella/core/constants/Text.java b/core/src/main/java/de/srsoftware/umbrella/core/constants/Text.java index 5665ef6f..8f2be0cd 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/constants/Text.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/constants/Text.java @@ -78,6 +78,7 @@ public class Text { public static final String TASKS = "tasks"; public static final String TIMETRACKING = "timetracking"; public static final String TIME_WITH_ID = "time ({id})"; + public static final String TRANSACTION = "transaction"; public static final String TYPE = "type"; public static final String UNIT = "unit"; diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Transaction.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Transaction.java index 19805128..71a1cd38 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/Transaction.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Transaction.java @@ -5,6 +5,7 @@ import de.srsoftware.tools.Mappable; import de.srsoftware.umbrella.core.constants.Field; import java.sql.ResultSet; import java.sql.SQLException; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.HashSet; @@ -13,8 +14,53 @@ import java.util.Set; import static java.text.MessageFormat.format; -public record Transaction(long id, long accountId, LocalDateTime date, IdOrString source, IdOrString destination, double amount, String purpose, Set tags) implements Mappable { +public class Transaction implements Mappable { + private long accountId, id; + private LocalDateTime date; + private IdOrString source, destination; + private double amount; + private String purpose; + private Set tags; + public Transaction(long id, long accountId, LocalDateTime date, IdOrString source, IdOrString destination, double amount, String purpose, Set tags){ + this.id = id; + this.accountId = accountId; + this.date = date; + this.source = source; + this.destination = destination; + this.amount = amount; + this.purpose = purpose; + this.tags = tags == null ? new HashSet<>() : tags; + } + + public long accountId(){ + return accountId; + } + + public double amount(){ + return amount; + } + + public LocalDateTime date(){ + return date; + } + + public Transaction date(LocalDateTime newVal){ + date = newVal; + return this; + } + + public void date(LocalDate date) { + this.date = this.date.withYear(date.getYear()).withMonth(date.getMonthValue()).withDayOfMonth(date.getDayOfMonth()); + } + + public IdOrString destination(){ + return destination; + } + + public long id(){ + return id; + } public static Transaction of(ResultSet rs) throws SQLException { var accountId = rs.getLong(Field.ACCOUNT); @@ -25,7 +71,19 @@ public record Transaction(long id, long accountId, LocalDateTime date, IdOrStrin var amount = rs.getDouble(Field.AMOUNT); var purpose = rs.getString(Field.DESCRIPTION); var id = rs.getLong(Field.ID); - return new Transaction(id, accountId, date, source, destination, amount, purpose, new HashSet<>()); + return new Transaction(id, accountId, date, source, destination, amount, purpose, null); + } + + public String purpose(){ + return purpose; + } + + public IdOrString source(){ + return source; + } + + public Set tags(){ + return tags; } @Override diff --git a/frontend/src/Components/LineEditor.svelte b/frontend/src/Components/LineEditor.svelte index 16f23da5..21676323 100644 --- a/frontend/src/Components/LineEditor.svelte +++ b/frontend/src/Components/LineEditor.svelte @@ -9,8 +9,9 @@ onclick = evt => { evt.preventDefault(); startEdit(); return false }, onSet = newVal => {return true;}, title = t('click_to_edit'), - type = 'div', - value = $bindable(null) + type = 'text', + value = $bindable(null), + wrapper = 'div' } = $props(); let editing = $state(simple); @@ -110,7 +111,7 @@ {#if editable && editing} - + {:else} -{value} +{value} {/if} diff --git a/frontend/src/routes/accounting/account.svelte b/frontend/src/routes/accounting/account.svelte index 0e29a41e..f79061ce 100644 --- a/frontend/src/routes/accounting/account.svelte +++ b/frontend/src/routes/accounting/account.svelte @@ -5,6 +5,7 @@ import { t } from '../../translations.svelte'; import EntryForm from './add_entry.svelte'; + import Transaction from './transaction.svelte'; let { id } = $props(); let account = $state(null); @@ -67,31 +68,7 @@ {#each transactions as transaction, i} - - {transaction.date} - {#each Object.entries(users) as [id,user]} - - {#if id == transaction.source.id} - {(-transaction.amount).toFixed(2)} {account.currency} - {/if} - {#if id == transaction.destination.id} - {(+transaction.amount).toFixed(2)} {account.currency} - {/if} - - {/each} - - {#if !transaction.source.id} - ← {transaction.source.value} - {/if} - {#if !transaction.destination.id} - → {transaction.destination.value} - {/if} - - {transaction.purpose} - - {transaction.tags.join(', ')} - - + {/each} diff --git a/frontend/src/routes/accounting/transaction.svelte b/frontend/src/routes/accounting/transaction.svelte new file mode 100644 index 00000000..bb165bfc --- /dev/null +++ b/frontend/src/routes/accounting/transaction.svelte @@ -0,0 +1,49 @@ + + + + + + + {#each Object.entries(users) as [id,user]} + + {#if id == transaction.source.id} + {(-transaction.amount).toFixed(2)} {account.currency} + {/if} + {#if id == transaction.destination.id} + {(+transaction.amount).toFixed(2)} {account.currency} + {/if} + + {/each} + + {#if !transaction.source.id} + ← {transaction.source.value} + {/if} + {#if !transaction.destination.id} + → {transaction.destination.value} + {/if} + + {transaction.purpose} + + {transaction.tags.join(', ')} + + diff --git a/frontend/src/routes/contact/Address.svelte b/frontend/src/routes/contact/Address.svelte index 9fa4576b..25e65c49 100644 --- a/frontend/src/routes/contact/Address.svelte +++ b/frontend/src/routes/contact/Address.svelte @@ -36,11 +36,11 @@ - onSet(address.box,newVal)} title={t('post_box')} /> - onSet(address.ext,newVal)} title={t('extended_address')} /> - onSet(address.street,newVal)} title={t('street')} /> - onSet(address.post_code,newVal)} title={t('post_code')} /> - onSet(address.locality,newVal)} title={t('locality')} /> - onSet(address.region,newVal)} title={t('region')} /> - onSet(address.country,newVal)} title={t('country')} /> + onSet(address.box,newVal)} title={t('post_box')} /> + onSet(address.ext,newVal)} title={t('extended_address')} /> + onSet(address.street,newVal)} title={t('street')} /> + onSet(address.post_code,newVal)} title={t('post_code')} /> + onSet(address.locality,newVal)} title={t('locality')} /> + onSet(address.region,newVal)} title={t('region')} /> + onSet(address.country,newVal)} title={t('country')} /> \ No newline at end of file diff --git a/frontend/src/routes/contact/Email.svelte b/frontend/src/routes/contact/Email.svelte index 33d4e798..1a4eee6c 100644 --- a/frontend/src/routes/contact/Email.svelte +++ b/frontend/src/routes/contact/Email.svelte @@ -33,5 +33,5 @@ {#if value} -
+
{/if} \ No newline at end of file diff --git a/frontend/src/routes/contact/ExtraField.svelte b/frontend/src/routes/contact/ExtraField.svelte index 88b9a833..66a9f5f0 100644 --- a/frontend/src/routes/contact/ExtraField.svelte +++ b/frontend/src/routes/contact/ExtraField.svelte @@ -19,7 +19,7 @@ {#if field.value.includes('\\n')} {:else} - + {/if} {/if} \ No newline at end of file diff --git a/frontend/src/routes/contact/FN.svelte b/frontend/src/routes/contact/FN.svelte index 76a9aba1..b29ad55f 100644 --- a/frontend/src/routes/contact/FN.svelte +++ b/frontend/src/routes/contact/FN.svelte @@ -14,5 +14,5 @@ {#if value} - + {/if} \ No newline at end of file diff --git a/frontend/src/routes/contact/Name.svelte b/frontend/src/routes/contact/Name.svelte index 9aa18932..9ef85cb2 100644 --- a/frontend/src/routes/contact/Name.svelte +++ b/frontend/src/routes/contact/Name.svelte @@ -16,18 +16,18 @@
{#if n.prefix} - onSet(n.prefix,newVal)} title={t('name_prefix')} /> + onSet(n.prefix,newVal)} title={t('name_prefix')} /> {/if} {#if n.given} - onSet(n.given,newVal)} title={t('given_name')} /> + onSet(n.given,newVal)} title={t('given_name')} /> {/if} {#if n.additional} - onSet(n.additional,newVal)} title={t('additional_name')} /> + onSet(n.additional,newVal)} title={t('additional_name')} /> {/if} {#if n.family} - onSet(n.family,newVal)} title={t('family_name')} /> + onSet(n.family,newVal)} title={t('family_name')} /> {/if} {#if n.suffix} - onSet(n.suffix,newVal)} title={t('')} /> + onSet(n.suffix,newVal)} title={t('')} /> {/if}
\ No newline at end of file diff --git a/frontend/src/routes/contact/Number.svelte b/frontend/src/routes/contact/Number.svelte index 53114e49..fd86475a 100644 --- a/frontend/src/routes/contact/Number.svelte +++ b/frontend/src/routes/contact/Number.svelte @@ -39,5 +39,5 @@ -
+
{/if} \ No newline at end of file diff --git a/frontend/src/routes/contact/Org.svelte b/frontend/src/routes/contact/Org.svelte index 04b0285f..5b7c52e3 100644 --- a/frontend/src/routes/contact/Org.svelte +++ b/frontend/src/routes/contact/Org.svelte @@ -14,5 +14,5 @@ {#if value} - + {/if} \ No newline at end of file diff --git a/frontend/src/routes/contact/URL.svelte b/frontend/src/routes/contact/URL.svelte index 4c392218..b47cedab 100644 --- a/frontend/src/routes/contact/URL.svelte +++ b/frontend/src/routes/contact/URL.svelte @@ -14,5 +14,5 @@ {#if value} - + {/if} \ No newline at end of file diff --git a/frontend/src/routes/stock/Index.svelte b/frontend/src/routes/stock/Index.svelte index 7ab1c641..754f1bc9 100644 --- a/frontend/src/routes/stock/Index.svelte +++ b/frontend/src/routes/stock/Index.svelte @@ -263,7 +263,7 @@
{#if location}

- patchLocation(location,'name',newName)} /> + patchLocation(location,'name',newName)} /> {#if location.parent_location_id} diff --git a/frontend/src/routes/stock/ItemProps.svelte b/frontend/src/routes/stock/ItemProps.svelte index c936c3ba..2fe1eeb0 100644 --- a/frontend/src/routes/stock/ItemProps.svelte +++ b/frontend/src/routes/stock/ItemProps.svelte @@ -68,7 +68,7 @@ {#if item} - update('name',v)} /> + update('name',v)} />
{@html item.description.rendered} @@ -80,7 +80,7 @@ {t('ID')} - update('code',v)} /> + update('code',v)} /> {#each item.properties.toSorted(byName) as prop} diff --git a/frontend/src/routes/task/ListTask.svelte b/frontend/src/routes/task/ListTask.svelte index 5173f323..cb74453b 100644 --- a/frontend/src/routes/task/ListTask.svelte +++ b/frontend/src/routes/task/ListTask.svelte @@ -156,7 +156,7 @@ {#if !deleted}
  • e.preventDefault()} {ondragstart} class="task {states[task.status]?.toLowerCase()}"> - + {#if task.est_time} ({+task.est_time} h) {/if} diff --git a/frontend/src/routes/wiki/View.svelte b/frontend/src/routes/wiki/View.svelte index c06b385b..8423d8fb 100644 --- a/frontend/src/routes/wiki/View.svelte +++ b/frontend/src/routes/wiki/View.svelte @@ -165,7 +165,7 @@ {/each}
  • - patchTitle(t)} /> + patchTitle(t)} /> {#if page.version != page.versions[0]} {t('not_recent_version')} {/if}