mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 05:09:38 +02:00
Separate exclude and one-time transaction handling (#2400)
* Separate exclude and one-time transaction handling - Split transaction "exclude" and "one-time" toggles into separate controls in transaction detail view - Updated Transaction::Search to show excluded transactions with grayed-out styling instead of filtering them out - Modified IncomeStatement calculations to exclude both excluded and one_time transactions from totals - Added migration to convert existing excluded transactions to also be one_time for backward compatibility - Updated transaction list view to show asterisk for one_time transactions and gray out excluded ones - Added controller support for kind parameter in transaction updates 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix linting issues - Remove trailing whitespace from migration - Fix ERB formatting throughout templates 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c003e8c6ed
commit
63d8114b05
28 changed files with 147 additions and 115 deletions
|
@ -133,7 +133,7 @@ class TransactionsController < ApplicationController
|
||||||
def entry_params
|
def entry_params
|
||||||
entry_params = params.require(:entry).permit(
|
entry_params = params.require(:entry).permit(
|
||||||
:name, :date, :amount, :currency, :excluded, :notes, :nature, :entryable_type,
|
:name, :date, :amount, :currency, :excluded, :notes, :nature, :entryable_type,
|
||||||
entryable_attributes: [ :id, :category_id, :merchant_id, { tag_ids: [] } ]
|
entryable_attributes: [ :id, :category_id, :merchant_id, :kind, { tag_ids: [] } ]
|
||||||
)
|
)
|
||||||
|
|
||||||
nature = entry_params.delete(:nature)
|
nature = entry_params.delete(:nature)
|
||||||
|
@ -150,7 +150,7 @@ class TransactionsController < ApplicationController
|
||||||
cleaned_params = params.fetch(:q, {})
|
cleaned_params = params.fetch(:q, {})
|
||||||
.permit(
|
.permit(
|
||||||
:start_date, :end_date, :search, :amount,
|
:start_date, :end_date, :search, :amount,
|
||||||
:amount_operator, :active_accounts_only, :excluded_transactions,
|
:amount_operator, :active_accounts_only,
|
||||||
accounts: [], account_ids: [],
|
accounts: [], account_ids: [],
|
||||||
categories: [], merchants: [], types: [], tags: []
|
categories: [], merchants: [], types: [], tags: []
|
||||||
)
|
)
|
||||||
|
|
|
@ -48,6 +48,7 @@ class IncomeStatement::CategoryStats
|
||||||
)
|
)
|
||||||
WHERE a.family_id = :family_id
|
WHERE a.family_id = :family_id
|
||||||
AND t.kind NOT IN ('funds_movement', 'one_time', 'cc_payment')
|
AND t.kind NOT IN ('funds_movement', 'one_time', 'cc_payment')
|
||||||
|
AND ae.excluded = false
|
||||||
GROUP BY c.id, period, CASE WHEN ae.amount < 0 THEN 'income' ELSE 'expense' END
|
GROUP BY c.id, period, CASE WHEN ae.amount < 0 THEN 'income' ELSE 'expense' END
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
|
|
|
@ -45,6 +45,7 @@ class IncomeStatement::FamilyStats
|
||||||
)
|
)
|
||||||
WHERE a.family_id = :family_id
|
WHERE a.family_id = :family_id
|
||||||
AND t.kind NOT IN ('funds_movement', 'one_time', 'cc_payment')
|
AND t.kind NOT IN ('funds_movement', 'one_time', 'cc_payment')
|
||||||
|
AND ae.excluded = false
|
||||||
GROUP BY period, CASE WHEN ae.amount < 0 THEN 'income' ELSE 'expense' END
|
GROUP BY period, CASE WHEN ae.amount < 0 THEN 'income' ELSE 'expense' END
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
|
|
|
@ -45,6 +45,7 @@ class IncomeStatement::Totals
|
||||||
er.to_currency = :target_currency
|
er.to_currency = :target_currency
|
||||||
)
|
)
|
||||||
WHERE at.kind NOT IN ('funds_movement', 'one_time', 'cc_payment')
|
WHERE at.kind NOT IN ('funds_movement', 'one_time', 'cc_payment')
|
||||||
|
AND ae.excluded = false
|
||||||
GROUP BY c.id, c.parent_id, CASE WHEN ae.amount < 0 THEN 'income' ELSE 'expense' END;
|
GROUP BY c.id, c.parent_id, CASE WHEN ae.amount < 0 THEN 'income' ELSE 'expense' END;
|
||||||
SQL
|
SQL
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,7 +14,6 @@ class Transaction::Search
|
||||||
attribute :merchants, array: true
|
attribute :merchants, array: true
|
||||||
attribute :tags, array: true
|
attribute :tags, array: true
|
||||||
attribute :active_accounts_only, :boolean, default: true
|
attribute :active_accounts_only, :boolean, default: true
|
||||||
attribute :excluded_transactions, :boolean, default: false
|
|
||||||
|
|
||||||
attr_reader :family
|
attr_reader :family
|
||||||
|
|
||||||
|
@ -29,7 +28,6 @@ class Transaction::Search
|
||||||
query = family.transactions
|
query = family.transactions
|
||||||
|
|
||||||
query = apply_active_accounts_filter(query, active_accounts_only)
|
query = apply_active_accounts_filter(query, active_accounts_only)
|
||||||
query = apply_excluded_transactions_filter(query, excluded_transactions)
|
|
||||||
query = apply_category_filter(query, categories)
|
query = apply_category_filter(query, categories)
|
||||||
query = apply_type_filter(query, types)
|
query = apply_type_filter(query, types)
|
||||||
query = apply_merchant_filter(query, merchants)
|
query = apply_merchant_filter(query, merchants)
|
||||||
|
@ -89,13 +87,6 @@ class Transaction::Search
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def apply_excluded_transactions_filter(query, excluded_transactions_filter)
|
|
||||||
unless excluded_transactions_filter
|
|
||||||
query.where(entries: { excluded: false })
|
|
||||||
else
|
|
||||||
query
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def apply_category_filter(query, categories)
|
def apply_category_filter(query, categories)
|
||||||
return query unless categories.present?
|
return query unless categories.present?
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<%- submit_btn_css ||= 'btn btn-link' %>
|
<%- submit_btn_css ||= "btn btn-link" %>
|
||||||
<%= form_tag oauth_application_path(application), method: :delete do %>
|
<%= form_tag oauth_application_path(application), method: :delete do %>
|
||||||
<%= submit_tag t('doorkeeper.applications.buttons.destroy'),
|
<%= submit_tag t("doorkeeper.applications.buttons.destroy"),
|
||||||
onclick: "return confirm('#{ t('doorkeeper.applications.confirmations.destroy') }')",
|
onclick: "return confirm('#{ t('doorkeeper.applications.confirmations.destroy') }')",
|
||||||
class: submit_btn_css %>
|
class: submit_btn_css %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<%= form_for application, url: doorkeeper_submit_path(application), as: :doorkeeper_application, html: { role: 'form' } do |f| %>
|
<%= form_for application, url: doorkeeper_submit_path(application), as: :doorkeeper_application, html: { role: "form" } do |f| %>
|
||||||
<% if application.errors.any? %>
|
<% if application.errors.any? %>
|
||||||
<div class="alert alert-danger" data-alert><p><%= t('doorkeeper.applications.form.error') %></p></div>
|
<div class="alert alert-danger" data-alert><p><%= t("doorkeeper.applications.form.error") %></p></div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<%= f.label :name, class: 'col-sm-2 col-form-label font-weight-bold' %>
|
<%= f.label :name, class: "col-sm-2 col-form-label font-weight-bold" %>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<%= f.text_field :name, class: "form-control #{ 'is-invalid' if application.errors[:name].present? }", required: true %>
|
<%= f.text_field :name, class: "form-control #{ 'is-invalid' if application.errors[:name].present? }", required: true %>
|
||||||
<%= doorkeeper_errors_for application, :name %>
|
<%= doorkeeper_errors_for application, :name %>
|
||||||
|
@ -12,48 +12,48 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<%= f.label :redirect_uri, class: 'col-sm-2 col-form-label font-weight-bold' %>
|
<%= f.label :redirect_uri, class: "col-sm-2 col-form-label font-weight-bold" %>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<%= f.text_area :redirect_uri, class: "form-control #{ 'is-invalid' if application.errors[:redirect_uri].present? }" %>
|
<%= f.text_area :redirect_uri, class: "form-control #{ 'is-invalid' if application.errors[:redirect_uri].present? }" %>
|
||||||
<%= doorkeeper_errors_for application, :redirect_uri %>
|
<%= doorkeeper_errors_for application, :redirect_uri %>
|
||||||
<span class="form-text text-secondary">
|
<span class="form-text text-secondary">
|
||||||
<%= t('doorkeeper.applications.help.redirect_uri') %>
|
<%= t("doorkeeper.applications.help.redirect_uri") %>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<% if Doorkeeper.configuration.allow_blank_redirect_uri?(application) %>
|
<% if Doorkeeper.configuration.allow_blank_redirect_uri?(application) %>
|
||||||
<span class="form-text text-secondary">
|
<span class="form-text text-secondary">
|
||||||
<%= t('doorkeeper.applications.help.blank_redirect_uri') %>
|
<%= t("doorkeeper.applications.help.blank_redirect_uri") %>
|
||||||
</span>
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<%= f.label :confidential, class: 'col-sm-2 form-check-label font-weight-bold' %>
|
<%= f.label :confidential, class: "col-sm-2 form-check-label font-weight-bold" %>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<%= f.check_box :confidential, class: "checkbox #{ 'is-invalid' if application.errors[:confidential].present? }" %>
|
<%= f.check_box :confidential, class: "checkbox #{ 'is-invalid' if application.errors[:confidential].present? }" %>
|
||||||
<%= doorkeeper_errors_for application, :confidential %>
|
<%= doorkeeper_errors_for application, :confidential %>
|
||||||
<span class="form-text text-secondary">
|
<span class="form-text text-secondary">
|
||||||
<%= t('doorkeeper.applications.help.confidential') %>
|
<%= t("doorkeeper.applications.help.confidential") %>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<%= f.label :scopes, class: 'col-sm-2 col-form-label font-weight-bold' %>
|
<%= f.label :scopes, class: "col-sm-2 col-form-label font-weight-bold" %>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<%= f.text_field :scopes, class: "form-control #{ 'has-error' if application.errors[:scopes].present? }" %>
|
<%= f.text_field :scopes, class: "form-control #{ 'has-error' if application.errors[:scopes].present? }" %>
|
||||||
<%= doorkeeper_errors_for application, :scopes %>
|
<%= doorkeeper_errors_for application, :scopes %>
|
||||||
<span class="form-text text-secondary">
|
<span class="form-text text-secondary">
|
||||||
<%= t('doorkeeper.applications.help.scopes') %>
|
<%= t("doorkeeper.applications.help.scopes") %>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-offset-2 col-sm-10">
|
<div class="col-sm-offset-2 col-sm-10">
|
||||||
<%= f.submit t('doorkeeper.applications.buttons.submit'), class: 'btn btn-primary' %>
|
<%= f.submit t("doorkeeper.applications.buttons.submit"), class: "btn btn-primary" %>
|
||||||
<%= link_to t('doorkeeper.applications.buttons.cancel'), oauth_applications_path, class: 'btn btn-secondary' %>
|
<%= link_to t("doorkeeper.applications.buttons.cancel"), oauth_applications_path, class: "btn btn-secondary" %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<div class="border-bottom mb-4">
|
<div class="border-bottom mb-4">
|
||||||
<h1><%= t('.title') %></h1>
|
<h1><%= t(".title") %></h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= render 'form', application: @application %>
|
<%= render "form", application: @application %>
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
<div class="border-bottom mb-4">
|
<div class="border-bottom mb-4">
|
||||||
<h1><%= t('.title') %></h1>
|
<h1><%= t(".title") %></h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p><%= link_to t('.new'), new_oauth_application_path, class: 'btn btn-success' %></p>
|
<p><%= link_to t(".new"), new_oauth_application_path, class: "btn btn-success" %></p>
|
||||||
|
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th><%= t('.name') %></th>
|
<th><%= t(".name") %></th>
|
||||||
<th><%= t('.callback_url') %></th>
|
<th><%= t(".callback_url") %></th>
|
||||||
<th><%= t('.confidential') %></th>
|
<th><%= t(".confidential") %></th>
|
||||||
<th><%= t('.actions') %></th>
|
<th><%= t(".actions") %></th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -24,13 +24,13 @@
|
||||||
<%= simple_format(application.redirect_uri) %>
|
<%= simple_format(application.redirect_uri) %>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
<%= application.confidential? ? t('doorkeeper.applications.index.confidentiality.yes') : t('doorkeeper.applications.index.confidentiality.no') %>
|
<%= application.confidential? ? t("doorkeeper.applications.index.confidentiality.yes") : t("doorkeeper.applications.index.confidentiality.no") %>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
<%= link_to t('doorkeeper.applications.buttons.edit'), edit_oauth_application_path(application), class: 'btn btn-link' %>
|
<%= link_to t("doorkeeper.applications.buttons.edit"), edit_oauth_application_path(application), class: "btn btn-link" %>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
<%= render 'delete_form', application: application %>
|
<%= render "delete_form", application: application %>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<div class="border-bottom mb-4">
|
<div class="border-bottom mb-4">
|
||||||
<h1><%= t('.title') %></h1>
|
<h1><%= t(".title") %></h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= render 'form', application: @application %>
|
<%= render "form", application: @application %>
|
||||||
|
|
|
@ -1,39 +1,39 @@
|
||||||
<div class="border-bottom mb-4">
|
<div class="border-bottom mb-4">
|
||||||
<h1><%= t('.title', name: @application.name) %></h1>
|
<h1><%= t(".title", name: @application.name) %></h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<h4><%= t('.application_id') %>:</h4>
|
<h4><%= t(".application_id") %>:</h4>
|
||||||
<p><code class="bg-light" id="application_id"><%= @application.uid %></code></p>
|
<p><code class="bg-light" id="application_id"><%= @application.uid %></code></p>
|
||||||
|
|
||||||
<h4><%= t('.secret') %>:</h4>
|
<h4><%= t(".secret") %>:</h4>
|
||||||
<p>
|
<p>
|
||||||
<code class="bg-light" id="secret">
|
<code class="bg-light" id="secret">
|
||||||
<% secret = flash[:application_secret].presence || @application.plaintext_secret %>
|
<% secret = flash[:application_secret].presence || @application.plaintext_secret %>
|
||||||
<% if secret.blank? && Doorkeeper.config.application_secret_hashed? %>
|
<% if secret.blank? && Doorkeeper.config.application_secret_hashed? %>
|
||||||
<span class="bg-light font-italic text-uppercase text-muted"><%= t('.secret_hashed') %></span>
|
<span class="bg-light font-italic text-uppercase text-muted"><%= t(".secret_hashed") %></span>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= secret %>
|
<%= secret %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</code>
|
</code>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h4><%= t('.scopes') %>:</h4>
|
<h4><%= t(".scopes") %>:</h4>
|
||||||
<p>
|
<p>
|
||||||
<code class="bg-light" id="scopes">
|
<code class="bg-light" id="scopes">
|
||||||
<% if @application.scopes.present? %>
|
<% if @application.scopes.present? %>
|
||||||
<%= @application.scopes %>
|
<%= @application.scopes %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<span class="bg-light font-italic text-uppercase text-muted"><%= t('.not_defined') %></span>
|
<span class="bg-light font-italic text-uppercase text-muted"><%= t(".not_defined") %></span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</code>
|
</code>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h4><%= t('.confidential') %>:</h4>
|
<h4><%= t(".confidential") %>:</h4>
|
||||||
<p><code class="bg-light" id="confidential"><%= @application.confidential? %></code></p>
|
<p><code class="bg-light" id="confidential"><%= @application.confidential? %></code></p>
|
||||||
|
|
||||||
<h4><%= t('.callback_urls') %>:</h4>
|
<h4><%= t(".callback_urls") %>:</h4>
|
||||||
|
|
||||||
<% if @application.redirect_uri.present? %>
|
<% if @application.redirect_uri.present? %>
|
||||||
<table>
|
<table>
|
||||||
|
@ -43,21 +43,21 @@
|
||||||
<code class="bg-light"><%= uri %></code>
|
<code class="bg-light"><%= uri %></code>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<%= link_to t('doorkeeper.applications.buttons.authorize'), oauth_authorization_path(client_id: @application.uid, redirect_uri: uri, response_type: 'code', scope: @application.scopes), class: 'btn btn-success', target: '_blank' %>
|
<%= link_to t("doorkeeper.applications.buttons.authorize"), oauth_authorization_path(client_id: @application.uid, redirect_uri: uri, response_type: "code", scope: @application.scopes), class: "btn btn-success", target: "_blank" %>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% end %>
|
<% end %>
|
||||||
</table>
|
</table>
|
||||||
<% else %>
|
<% else %>
|
||||||
<span class="bg-light font-italic text-uppercase text-muted"><%= t('.not_defined') %></span>
|
<span class="bg-light font-italic text-uppercase text-muted"><%= t(".not_defined") %></span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<h3><%= t('.actions') %></h3>
|
<h3><%= t(".actions") %></h3>
|
||||||
|
|
||||||
<p><%= link_to t('doorkeeper.applications.buttons.edit'), edit_oauth_application_path(@application), class: 'btn btn-primary' %></p>
|
<p><%= link_to t("doorkeeper.applications.buttons.edit"), edit_oauth_application_path(@application), class: "btn btn-primary" %></p>
|
||||||
|
|
||||||
<p><%= render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger' %></p>
|
<p><%= render "delete_form", application: @application, submit_btn_css: "btn btn-danger" %></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<div class="mx-auto w-12 h-12 rounded-full bg-destructive-surface flex items-center justify-center mb-4">
|
<div class="mx-auto w-12 h-12 rounded-full bg-destructive-surface flex items-center justify-center mb-4">
|
||||||
<%= icon("alert-circle", class: "w-6 h-6 text-destructive") %>
|
<%= icon("alert-circle", class: "w-6 h-6 text-destructive") %>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-2xl font-medium text-primary"><%= t('doorkeeper.authorizations.error.title') %></h1>
|
<h1 class="text-2xl font-medium text-primary"><%= t("doorkeeper.authorizations.error.title") %></h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-surface-inset rounded-lg p-4">
|
<div class="bg-surface-inset rounded-lg p-4">
|
||||||
|
|
|
@ -3,12 +3,12 @@
|
||||||
<div class="mx-auto w-12 h-12 rounded-full bg-surface-inset flex items-center justify-center mb-4">
|
<div class="mx-auto w-12 h-12 rounded-full bg-surface-inset flex items-center justify-center mb-4">
|
||||||
<%= icon("loader-circle", class: "w-6 h-6 text-primary animate-spin") %>
|
<%= icon("loader-circle", class: "w-6 h-6 text-primary animate-spin") %>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-2xl font-medium text-primary"><%= t('.title') %></h1>
|
<h1 class="text-2xl font-medium text-primary"><%= t(".title") %></h1>
|
||||||
<p class="text-sm text-secondary">Redirecting you back to the application...</p>
|
<p class="text-sm text-secondary">Redirecting you back to the application...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% turbo_disabled = @pre_auth.redirect_uri&.start_with?('maybeapp://') || params[:display] == 'mobile' %>
|
<% turbo_disabled = @pre_auth.redirect_uri&.start_with?("maybeapp://") || params[:display] == "mobile" %>
|
||||||
<%= form_tag @pre_auth.redirect_uri, method: :post, name: :redirect_form, authenticity_token: false, data: { turbo: !turbo_disabled } do %>
|
<%= form_tag @pre_auth.redirect_uri, method: :post, name: :redirect_form, authenticity_token: false, data: { turbo: !turbo_disabled } do %>
|
||||||
<% auth.body.compact.each do |key, value| %>
|
<% auth.body.compact.each do |key, value| %>
|
||||||
<%= hidden_field_tag key, value %>
|
<%= hidden_field_tag key, value %>
|
||||||
|
|
|
@ -5,13 +5,13 @@
|
||||||
<div class="bg-container rounded-xl p-6 space-y-6">
|
<div class="bg-container rounded-xl p-6 space-y-6">
|
||||||
<div class="space-y-2 text-center">
|
<div class="space-y-2 text-center">
|
||||||
<p class="text-sm text-secondary">
|
<p class="text-sm text-secondary">
|
||||||
<%= raw t('.prompt', client_name: content_tag(:span, @pre_auth.client.name, class: 'font-medium text-primary')) %>
|
<%= raw t(".prompt", client_name: content_tag(:span, @pre_auth.client.name, class: "font-medium text-primary")) %>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% if @pre_auth.scopes.count > 0 %>
|
<% if @pre_auth.scopes.count > 0 %>
|
||||||
<div class="bg-surface-inset rounded-lg p-4 space-y-3">
|
<div class="bg-surface-inset rounded-lg p-4 space-y-3">
|
||||||
<p class="text-sm font-medium text-primary"><%= t('.able_to') %>:</p>
|
<p class="text-sm font-medium text-primary"><%= t(".able_to") %>:</p>
|
||||||
<ul class="space-y-2">
|
<ul class="space-y-2">
|
||||||
<% @pre_auth.scopes.each do |scope| %>
|
<% @pre_auth.scopes.each do |scope| %>
|
||||||
<li class="flex items-start gap-2 text-sm text-secondary">
|
<li class="flex items-start gap-2 text-sm text-secondary">
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<% turbo_disabled = params[:redirect_uri]&.start_with?('maybeapp://') || params[:display] == 'mobile' %>
|
<% turbo_disabled = params[:redirect_uri]&.start_with?("maybeapp://") || params[:display] == "mobile" %>
|
||||||
<%= form_tag oauth_authorization_path, method: :post, class: "w-full", data: { turbo: !turbo_disabled } do %>
|
<%= form_tag oauth_authorization_path, method: :post, class: "w-full", data: { turbo: !turbo_disabled } do %>
|
||||||
<%= hidden_field_tag :client_id, @pre_auth.client.uid, id: nil %>
|
<%= hidden_field_tag :client_id, @pre_auth.client.uid, id: nil %>
|
||||||
<%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri, id: nil %>
|
<%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri, id: nil %>
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
<%= hidden_field_tag :display, params[:display], id: nil %>
|
<%= hidden_field_tag :display, params[:display], id: nil %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render ButtonComponent.new(
|
<%= render ButtonComponent.new(
|
||||||
text: t('doorkeeper.authorizations.buttons.authorize'),
|
text: t("doorkeeper.authorizations.buttons.authorize"),
|
||||||
variant: :primary,
|
variant: :primary,
|
||||||
size: :lg,
|
size: :lg,
|
||||||
full_width: true,
|
full_width: true,
|
||||||
|
@ -60,7 +60,7 @@
|
||||||
<%= hidden_field_tag :display, params[:display], id: nil %>
|
<%= hidden_field_tag :display, params[:display], id: nil %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render ButtonComponent.new(
|
<%= render ButtonComponent.new(
|
||||||
text: t('doorkeeper.authorizations.buttons.deny'),
|
text: t("doorkeeper.authorizations.buttons.deny"),
|
||||||
variant: :outline,
|
variant: :outline,
|
||||||
size: :lg,
|
size: :lg,
|
||||||
full_width: true,
|
full_width: true,
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<div class="mx-auto w-12 h-12 rounded-full bg-success-surface flex items-center justify-center mb-4">
|
<div class="mx-auto w-12 h-12 rounded-full bg-success-surface flex items-center justify-center mb-4">
|
||||||
<%= icon("check", class: "w-6 h-6 text-success") %>
|
<%= icon("check", class: "w-6 h-6 text-success") %>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-2xl font-medium text-primary"><%= t('.title') %></h1>
|
<h1 class="text-2xl font-medium text-primary"><%= t(".title") %></h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-surface-inset rounded-lg p-4">
|
<div class="bg-surface-inset rounded-lg p-4">
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<%- submit_btn_css ||= 'btn btn-link' %>
|
<%- submit_btn_css ||= "btn btn-link" %>
|
||||||
<%= form_tag oauth_authorized_application_path(application), method: :delete do %>
|
<%= form_tag oauth_authorized_application_path(application), method: :delete do %>
|
||||||
<%= submit_tag t('doorkeeper.authorized_applications.buttons.revoke'), onclick: "return confirm('#{ t('doorkeeper.authorized_applications.confirmations.revoke') }')", class: submit_btn_css %>
|
<%= submit_tag t("doorkeeper.authorized_applications.buttons.revoke"), onclick: "return confirm('#{ t('doorkeeper.authorized_applications.confirmations.revoke') }')", class: submit_btn_css %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
<header class="page-header">
|
<header class="page-header">
|
||||||
<h1><%= t('doorkeeper.authorized_applications.index.title') %></h1>
|
<h1><%= t("doorkeeper.authorized_applications.index.title") %></h1>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main role="main">
|
<main role="main">
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th><%= t('doorkeeper.authorized_applications.index.application') %></th>
|
<th><%= t("doorkeeper.authorized_applications.index.application") %></th>
|
||||||
<th><%= t('doorkeeper.authorized_applications.index.created_at') %></th>
|
<th><%= t("doorkeeper.authorized_applications.index.created_at") %></th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -15,8 +15,8 @@
|
||||||
<% @applications.each do |application| %>
|
<% @applications.each do |application| %>
|
||||||
<tr>
|
<tr>
|
||||||
<td><%= application.name %></td>
|
<td><%= application.name %></td>
|
||||||
<td><%= application.created_at.strftime(t('doorkeeper.authorized_applications.index.date_format')) %></td>
|
<td><%= application.created_at.strftime(t("doorkeeper.authorized_applications.index.date_format")) %></td>
|
||||||
<td><%= render 'delete_form', application: application %></td>
|
<td><%= render "delete_form", application: application %></td>
|
||||||
</tr>
|
</tr>
|
||||||
<% end %>
|
<% end %>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
@ -4,22 +4,22 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title><%= t('doorkeeper.layouts.admin.title') %></title>
|
<title><%= t("doorkeeper.layouts.admin.title") %></title>
|
||||||
<%= stylesheet_link_tag "doorkeeper/admin/application" %>
|
<%= stylesheet_link_tag "doorkeeper/admin/application" %>
|
||||||
<%= csrf_meta_tags %>
|
<%= csrf_meta_tags %>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-5">
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-5">
|
||||||
<%= link_to t('doorkeeper.layouts.admin.nav.oauth2_provider'), oauth_applications_path, class: 'navbar-brand' %>
|
<%= link_to t("doorkeeper.layouts.admin.nav.oauth2_provider"), oauth_applications_path, class: "navbar-brand" %>
|
||||||
|
|
||||||
<div class="collapse navbar-collapse">
|
<div class="collapse navbar-collapse">
|
||||||
<ul class="navbar-nav mr-auto">
|
<ul class="navbar-nav mr-auto">
|
||||||
<li class="nav-item <%= 'active' if request.path == oauth_applications_path %>">
|
<li class="nav-item <%= "active" if request.path == oauth_applications_path %>">
|
||||||
<%= link_to t('doorkeeper.layouts.admin.nav.applications'), oauth_applications_path, class: 'nav-link' %>
|
<%= link_to t("doorkeeper.layouts.admin.nav.applications"), oauth_applications_path, class: "nav-link" %>
|
||||||
</li>
|
</li>
|
||||||
<% if respond_to?(:root_path) %>
|
<% if respond_to?(:root_path) %>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<%= link_to t('doorkeeper.layouts.admin.nav.home'), root_path, class: 'nav-link' %>
|
<%= link_to t("doorkeeper.layouts.admin.nav.home"), root_path, class: "nav-link" %>
|
||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -6,9 +6,10 @@
|
||||||
<%= turbo_frame_tag dom_id(transaction) do %>
|
<%= turbo_frame_tag dom_id(transaction) do %>
|
||||||
<div class="grid grid-cols-12 items-center text-primary text-sm font-medium p-4 lg:p-4
|
<div class="grid grid-cols-12 items-center text-primary text-sm font-medium p-4 lg:p-4
|
||||||
<%= @focused_record == entry || @focused_record == transaction ?
|
<%= @focused_record == entry || @focused_record == transaction ?
|
||||||
"border border-gray-900 rounded-lg" : "" %>">
|
"border border-gray-900 rounded-lg" : "" %>
|
||||||
|
<%= entry.excluded ? "opacity-50 text-gray-400" : "" %>">
|
||||||
|
|
||||||
<div class="pr-4 lg:pr-10 flex items-center gap-3 lg:gap-4 col-span-8 <%= view_ctx == 'global' ? 'lg:col-span-8' : 'lg:col-span-6' %>">
|
<div class="pr-4 lg:pr-10 flex items-center gap-3 lg:gap-4 col-span-8 <%= view_ctx == "global" ? "lg:col-span-8" : "lg:col-span-6" %>">
|
||||||
<%= check_box_tag dom_id(entry, "selection"),
|
<%= check_box_tag dom_id(entry, "selection"),
|
||||||
disabled: transaction.transfer.present?,
|
disabled: transaction.transfer.present?,
|
||||||
class: "checkbox checkbox--light",
|
class: "checkbox checkbox--light",
|
||||||
|
@ -61,7 +62,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-1 flex-shrink-0">
|
<div class="flex items-center gap-1 flex-shrink-0">
|
||||||
<% if entry.excluded %>
|
<% if transaction.one_time? %>
|
||||||
<span class="text-orange-500" title="One-time <%= entry.amount.negative? ? "income" : "expense" %> (excluded from averages)">
|
<span class="text-orange-500" title="One-time <%= entry.amount.negative? ? "income" : "expense" %> (excluded from averages)">
|
||||||
<%= icon "asterisk", size: "sm", color: "current" %>
|
<%= icon "asterisk", size: "sm", color: "current" %>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -106,13 +106,34 @@
|
||||||
data: { controller: "auto-submit-form" } do |f| %>
|
data: { controller: "auto-submit-form" } do |f| %>
|
||||||
<div class="flex cursor-pointer items-center gap-4 justify-between">
|
<div class="flex cursor-pointer items-center gap-4 justify-between">
|
||||||
<div class="text-sm space-y-1">
|
<div class="text-sm space-y-1">
|
||||||
<h4 class="text-primary">One-time <%= @entry.amount.negative? ? "Income" : "Expense" %></h4>
|
<h4 class="text-primary">Exclude</h4>
|
||||||
<p class="text-secondary">One-time transactions will be excluded from certain budgeting calculations and reports to help you see what's really important.</p>
|
<p class="text-secondary">Excluded transactions will be removed from budgeting calculations and reports.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= f.toggle :excluded, { data: { auto_submit_form_target: "auto" } } %>
|
<%= f.toggle :excluded, { data: { auto_submit_form_target: "auto" } } %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pb-4">
|
||||||
|
<%= styled_form_with model: @entry,
|
||||||
|
url: transaction_path(@entry),
|
||||||
|
class: "p-3",
|
||||||
|
data: { controller: "auto-submit-form" } do |f| %>
|
||||||
|
<%= f.fields_for :entryable do |ef| %>
|
||||||
|
<div class="flex cursor-pointer items-center gap-4 justify-between">
|
||||||
|
<div class="text-sm space-y-1">
|
||||||
|
<h4 class="text-primary">One-time <%= @entry.amount.negative? ? "Income" : "Expense" %></h4>
|
||||||
|
<p class="text-secondary">One-time transactions will be excluded from certain budgeting calculations and reports to help you see what's really important.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= ef.toggle :kind, {
|
||||||
|
checked: @entry.transaction.one_time?,
|
||||||
|
data: { auto_submit_form_target: "auto" }
|
||||||
|
}, "one_time", "standard" %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<div class="flex items-center justify-between gap-4 p-3">
|
<div class="flex items-center justify-between gap-4 p-3">
|
||||||
<div class="text-sm space-y-1">
|
<div class="text-sm space-y-1">
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
class UpdateExcludedTransactionsToOneTime < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
reversible do |dir|
|
||||||
|
dir.up do
|
||||||
|
# Update all transactions that have excluded entries to be one_time
|
||||||
|
# They remain excluded as well since users were using excluded as "one time" before
|
||||||
|
execute <<~SQL
|
||||||
|
UPDATE transactions
|
||||||
|
SET kind = 'one_time'
|
||||||
|
FROM entries
|
||||||
|
WHERE entries.entryable_id = transactions.id
|
||||||
|
AND entries.entryable_type = 'Transaction'
|
||||||
|
AND entries.excluded = true
|
||||||
|
AND transactions.kind = 'standard'
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
|
||||||
|
dir.down do
|
||||||
|
# Revert one_time transactions back to standard if their entry is excluded
|
||||||
|
# This assumes these were the ones we migrated in the up method
|
||||||
|
execute <<~SQL
|
||||||
|
UPDATE transactions
|
||||||
|
SET kind = 'standard'
|
||||||
|
FROM entries
|
||||||
|
WHERE entries.entryable_id = transactions.id
|
||||||
|
AND entries.entryable_type = 'Transaction'
|
||||||
|
AND entries.excluded = true
|
||||||
|
AND transactions.kind = 'one_time'
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
2
db/schema.rb
generated
2
db/schema.rb
generated
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.2].define(version: 2025_06_18_120703) do
|
ActiveRecord::Schema[7.2].define(version: 2025_06_20_204550) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pgcrypto"
|
enable_extension "pgcrypto"
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
|
|
@ -204,6 +204,20 @@ class IncomeStatementTest < ActiveSupport::TestCase
|
||||||
assert_equal Money.new(900, @family.currency), totals.expense_money
|
assert_equal Money.new(900, @family.currency), totals.expense_money
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "excludes excluded transactions from income statement calculations" do
|
||||||
|
# Create an excluded transaction
|
||||||
|
excluded_transaction_entry = create_transaction(account: @checking_account, amount: 250, category: @groceries_category)
|
||||||
|
excluded_transaction_entry.update!(excluded: true)
|
||||||
|
|
||||||
|
income_statement = IncomeStatement.new(@family)
|
||||||
|
totals = income_statement.totals
|
||||||
|
|
||||||
|
# Should exclude excluded transactions
|
||||||
|
assert_equal 4, totals.transactions_count # Only original 4 transactions
|
||||||
|
assert_equal Money.new(1000, @family.currency), totals.income_money
|
||||||
|
assert_equal Money.new(900, @family.currency), totals.expense_money
|
||||||
|
end
|
||||||
|
|
||||||
# NEW TESTS: Interval-Based Calculations
|
# NEW TESTS: Interval-Based Calculations
|
||||||
test "different intervals return different statistical results with multi-period data" do
|
test "different intervals return different statistical results with multi-period data" do
|
||||||
# Clear existing transactions
|
# Clear existing transactions
|
||||||
|
|
|
@ -298,35 +298,4 @@ class Transaction::SearchTest < ActiveSupport::TestCase
|
||||||
assert_equal Money.new(0, "USD"), totals.expense_money
|
assert_equal Money.new(0, "USD"), totals.expense_money
|
||||||
assert_equal Money.new(0, "USD"), totals.income_money
|
assert_equal Money.new(0, "USD"), totals.income_money
|
||||||
end
|
end
|
||||||
|
|
||||||
test "totals respects excluded transactions filter from search" do
|
|
||||||
# Create an excluded transaction (should be excluded by default)
|
|
||||||
excluded_entry = create_transaction(
|
|
||||||
account: @checking_account,
|
|
||||||
amount: 100,
|
|
||||||
kind: "standard"
|
|
||||||
)
|
|
||||||
excluded_entry.update!(excluded: true) # Marks it as excluded
|
|
||||||
|
|
||||||
# Create a normal transaction
|
|
||||||
normal_entry = create_transaction(
|
|
||||||
account: @checking_account,
|
|
||||||
amount: 50,
|
|
||||||
kind: "standard"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Default behavior should exclude excluded transactions
|
|
||||||
search = Transaction::Search.new(@family)
|
|
||||||
totals = search.totals
|
|
||||||
|
|
||||||
assert_equal 1, totals.count
|
|
||||||
assert_equal Money.new(50, "USD"), totals.expense_money # Only non-excluded transaction
|
|
||||||
|
|
||||||
# Explicitly include excluded transactions
|
|
||||||
search_with_excluded = Transaction::Search.new(@family, filters: { excluded_transactions: true })
|
|
||||||
totals_with_excluded = search_with_excluded.totals
|
|
||||||
|
|
||||||
assert_equal 2, totals_with_excluded.count
|
|
||||||
assert_equal Money.new(150, "USD"), totals_with_excluded.expense_money # Both transactions
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue