Compare commits
22 Commits
v0.1.0-alp
...
v0.1.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
773cd0da71 | ||
|
|
5da34c4609 | ||
|
|
957584b69c | ||
|
|
d0a15b8a98 | ||
|
|
9956a9540e | ||
|
|
8c1a7af37f | ||
|
|
c5704ffd45 | ||
|
|
8372e26864 | ||
|
|
6477c0f766 | ||
|
|
2a8bb57c9c | ||
|
|
2f432ec0c3 | ||
|
|
e3269e8981 | ||
|
|
8f891b8d8c | ||
|
|
775921092c | ||
|
|
83e2bfceb8 | ||
|
|
87a40aafeb | ||
|
|
a681e73fea | ||
|
|
d3f9be15f1 | ||
|
|
115f792198 | ||
|
|
e4ac5c87e4 | ||
|
|
a4fef176e8 | ||
|
|
ee5fc2be38 |
165
Gemfile.lock
165
Gemfile.lock
@@ -1,38 +1,38 @@
|
||||
GIT
|
||||
remote: https://github.com/maybe-finance/lucide-rails.git
|
||||
revision: 6170b3a0eceb43a8af6552638e9526673c356d0d
|
||||
revision: 79d989593ee4ac6c50106ec5e4d2bd4ec8f5af87
|
||||
specs:
|
||||
lucide-rails (0.2.0)
|
||||
railties (>= 4.1.0)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/rails/rails.git
|
||||
revision: c1f1b14adce5cd373ed63611486eb7a7db73c78c
|
||||
revision: f9c847fac102039d9174106f44b59144da267751
|
||||
branch: 7-2-stable
|
||||
specs:
|
||||
actioncable (7.2.0.alpha)
|
||||
actionpack (= 7.2.0.alpha)
|
||||
activesupport (= 7.2.0.alpha)
|
||||
actioncable (7.2.0.beta2)
|
||||
actionpack (= 7.2.0.beta2)
|
||||
activesupport (= 7.2.0.beta2)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
zeitwerk (~> 2.6)
|
||||
actionmailbox (7.2.0.alpha)
|
||||
actionpack (= 7.2.0.alpha)
|
||||
activejob (= 7.2.0.alpha)
|
||||
activerecord (= 7.2.0.alpha)
|
||||
activestorage (= 7.2.0.alpha)
|
||||
activesupport (= 7.2.0.alpha)
|
||||
actionmailbox (7.2.0.beta2)
|
||||
actionpack (= 7.2.0.beta2)
|
||||
activejob (= 7.2.0.beta2)
|
||||
activerecord (= 7.2.0.beta2)
|
||||
activestorage (= 7.2.0.beta2)
|
||||
activesupport (= 7.2.0.beta2)
|
||||
mail (>= 2.8.0)
|
||||
actionmailer (7.2.0.alpha)
|
||||
actionpack (= 7.2.0.alpha)
|
||||
actionview (= 7.2.0.alpha)
|
||||
activejob (= 7.2.0.alpha)
|
||||
activesupport (= 7.2.0.alpha)
|
||||
actionmailer (7.2.0.beta2)
|
||||
actionpack (= 7.2.0.beta2)
|
||||
actionview (= 7.2.0.beta2)
|
||||
activejob (= 7.2.0.beta2)
|
||||
activesupport (= 7.2.0.beta2)
|
||||
mail (>= 2.8.0)
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (7.2.0.alpha)
|
||||
actionview (= 7.2.0.alpha)
|
||||
activesupport (= 7.2.0.alpha)
|
||||
actionpack (7.2.0.beta2)
|
||||
actionview (= 7.2.0.beta2)
|
||||
activesupport (= 7.2.0.beta2)
|
||||
nokogiri (>= 1.8.5)
|
||||
racc
|
||||
rack (>= 2.2.4)
|
||||
@@ -41,60 +41,61 @@ GIT
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
useragent (~> 0.16)
|
||||
actiontext (7.2.0.alpha)
|
||||
actionpack (= 7.2.0.alpha)
|
||||
activerecord (= 7.2.0.alpha)
|
||||
activestorage (= 7.2.0.alpha)
|
||||
activesupport (= 7.2.0.alpha)
|
||||
actiontext (7.2.0.beta2)
|
||||
actionpack (= 7.2.0.beta2)
|
||||
activerecord (= 7.2.0.beta2)
|
||||
activestorage (= 7.2.0.beta2)
|
||||
activesupport (= 7.2.0.beta2)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (7.2.0.alpha)
|
||||
activesupport (= 7.2.0.alpha)
|
||||
actionview (7.2.0.beta2)
|
||||
activesupport (= 7.2.0.beta2)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
activejob (7.2.0.alpha)
|
||||
activesupport (= 7.2.0.alpha)
|
||||
activejob (7.2.0.beta2)
|
||||
activesupport (= 7.2.0.beta2)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.2.0.alpha)
|
||||
activesupport (= 7.2.0.alpha)
|
||||
activerecord (7.2.0.alpha)
|
||||
activemodel (= 7.2.0.alpha)
|
||||
activesupport (= 7.2.0.alpha)
|
||||
activemodel (7.2.0.beta2)
|
||||
activesupport (= 7.2.0.beta2)
|
||||
activerecord (7.2.0.beta2)
|
||||
activemodel (= 7.2.0.beta2)
|
||||
activesupport (= 7.2.0.beta2)
|
||||
timeout (>= 0.4.0)
|
||||
activestorage (7.2.0.alpha)
|
||||
actionpack (= 7.2.0.alpha)
|
||||
activejob (= 7.2.0.alpha)
|
||||
activerecord (= 7.2.0.alpha)
|
||||
activesupport (= 7.2.0.alpha)
|
||||
activestorage (7.2.0.beta2)
|
||||
actionpack (= 7.2.0.beta2)
|
||||
activejob (= 7.2.0.beta2)
|
||||
activerecord (= 7.2.0.beta2)
|
||||
activesupport (= 7.2.0.beta2)
|
||||
marcel (~> 1.0)
|
||||
activesupport (7.2.0.alpha)
|
||||
activesupport (7.2.0.beta2)
|
||||
base64
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||
connection_pool (>= 2.2.5)
|
||||
drb
|
||||
i18n (>= 1.6, < 2)
|
||||
logger
|
||||
minitest (>= 5.1)
|
||||
tzinfo (~> 2.0, >= 2.0.5)
|
||||
rails (7.2.0.alpha)
|
||||
actioncable (= 7.2.0.alpha)
|
||||
actionmailbox (= 7.2.0.alpha)
|
||||
actionmailer (= 7.2.0.alpha)
|
||||
actionpack (= 7.2.0.alpha)
|
||||
actiontext (= 7.2.0.alpha)
|
||||
actionview (= 7.2.0.alpha)
|
||||
activejob (= 7.2.0.alpha)
|
||||
activemodel (= 7.2.0.alpha)
|
||||
activerecord (= 7.2.0.alpha)
|
||||
activestorage (= 7.2.0.alpha)
|
||||
activesupport (= 7.2.0.alpha)
|
||||
rails (7.2.0.beta2)
|
||||
actioncable (= 7.2.0.beta2)
|
||||
actionmailbox (= 7.2.0.beta2)
|
||||
actionmailer (= 7.2.0.beta2)
|
||||
actionpack (= 7.2.0.beta2)
|
||||
actiontext (= 7.2.0.beta2)
|
||||
actionview (= 7.2.0.beta2)
|
||||
activejob (= 7.2.0.beta2)
|
||||
activemodel (= 7.2.0.beta2)
|
||||
activerecord (= 7.2.0.beta2)
|
||||
activestorage (= 7.2.0.beta2)
|
||||
activesupport (= 7.2.0.beta2)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 7.2.0.alpha)
|
||||
railties (7.2.0.alpha)
|
||||
actionpack (= 7.2.0.alpha)
|
||||
activesupport (= 7.2.0.alpha)
|
||||
railties (= 7.2.0.beta2)
|
||||
railties (7.2.0.beta2)
|
||||
actionpack (= 7.2.0.beta2)
|
||||
activesupport (= 7.2.0.beta2)
|
||||
irb (~> 1.13)
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
@@ -108,17 +109,17 @@ GEM
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
ast (2.4.2)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.930.0)
|
||||
aws-sdk-core (3.196.1)
|
||||
aws-partitions (1.941.0)
|
||||
aws-sdk-core (3.197.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.8)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.81.0)
|
||||
aws-sdk-core (~> 3, >= 3.193.0)
|
||||
aws-sdk-kms (1.83.0)
|
||||
aws-sdk-core (~> 3, >= 3.197.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.151.0)
|
||||
aws-sdk-core (~> 3, >= 3.194.0)
|
||||
aws-sdk-s3 (1.152.0)
|
||||
aws-sdk-core (~> 3, >= 3.197.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.8)
|
||||
aws-sigv4 (1.8.0)
|
||||
@@ -138,7 +139,7 @@ GEM
|
||||
msgpack (~> 1.2)
|
||||
brakeman (6.1.2)
|
||||
racc
|
||||
builder (3.2.4)
|
||||
builder (3.3.0)
|
||||
capybara (3.40.0)
|
||||
addressable
|
||||
matrix
|
||||
@@ -150,7 +151,7 @@ GEM
|
||||
xpath (~> 3.2)
|
||||
childprocess (5.0.0)
|
||||
climate_control (1.2.0)
|
||||
concurrent-ruby (1.2.3)
|
||||
concurrent-ruby (1.3.3)
|
||||
connection_pool (2.4.1)
|
||||
crack (1.0.0)
|
||||
bigdecimal
|
||||
@@ -177,7 +178,7 @@ GEM
|
||||
erubi (1.12.0)
|
||||
et-orbi (1.2.11)
|
||||
tzinfo
|
||||
faraday (2.9.0)
|
||||
faraday (2.9.1)
|
||||
faraday-net_http (>= 2.0, < 3.2)
|
||||
faraday-net_http (3.1.0)
|
||||
net-http
|
||||
@@ -189,7 +190,7 @@ GEM
|
||||
raabro (~> 1.4)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
good_job (3.29.2)
|
||||
good_job (3.29.3)
|
||||
activejob (>= 6.0.0)
|
||||
activerecord (>= 6.0.0)
|
||||
concurrent-ruby (>= 1.0.2)
|
||||
@@ -239,6 +240,7 @@ GEM
|
||||
listen (3.9.0)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
logger (1.6.0)
|
||||
loofah (2.22.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
@@ -257,7 +259,7 @@ GEM
|
||||
msgpack (1.7.2)
|
||||
net-http (0.4.1)
|
||||
uri
|
||||
net-imap (0.4.11)
|
||||
net-imap (0.4.12)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
@@ -283,13 +285,13 @@ GEM
|
||||
base64
|
||||
faraday (>= 1, < 3)
|
||||
sawyer (~> 0.9)
|
||||
pagy (8.4.0)
|
||||
pagy (8.4.4)
|
||||
parallel (1.24.0)
|
||||
parser (3.3.1.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
pg (1.5.6)
|
||||
prism (0.27.0)
|
||||
prism (0.29.0)
|
||||
propshaft (0.9.0)
|
||||
actionpack (>= 7.0.0)
|
||||
activesupport (>= 7.0.0)
|
||||
@@ -331,7 +333,7 @@ GEM
|
||||
rdoc (6.7.0)
|
||||
psych (>= 4.0.0)
|
||||
regexp_parser (2.9.2)
|
||||
reline (0.5.7)
|
||||
reline (0.5.8)
|
||||
io-console (~> 0.5)
|
||||
rexml (3.2.8)
|
||||
strscan (>= 3.0.9)
|
||||
@@ -364,13 +366,12 @@ GEM
|
||||
rubocop-minitest
|
||||
rubocop-performance
|
||||
rubocop-rails
|
||||
ruby-lsp (0.16.6)
|
||||
ruby-lsp (0.17.1)
|
||||
language_server-protocol (~> 3.17.0)
|
||||
prism (>= 0.23.0, < 0.28)
|
||||
prism (>= 0.29.0, < 0.30)
|
||||
sorbet-runtime (>= 0.5.10782)
|
||||
ruby-lsp-rails (0.3.6)
|
||||
ruby-lsp (>= 0.16.5, < 0.17.0)
|
||||
sorbet-runtime (>= 0.5.9897)
|
||||
ruby-lsp-rails (0.3.7)
|
||||
ruby-lsp (>= 0.17.0, < 0.18.0)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-vips (2.2.1)
|
||||
ffi (~> 1.12)
|
||||
@@ -397,23 +398,23 @@ GEM
|
||||
simplecov-html (0.12.3)
|
||||
simplecov_json_formatter (0.1.4)
|
||||
smart_properties (1.17.0)
|
||||
sorbet-runtime (0.5.11383)
|
||||
sorbet-runtime (0.5.11406)
|
||||
stackprof (0.2.26)
|
||||
stimulus-rails (1.3.3)
|
||||
railties (>= 6.0.0)
|
||||
stringio (3.1.0)
|
||||
strscan (3.1.0)
|
||||
tailwindcss-rails (2.6.0)
|
||||
tailwindcss-rails (2.6.1)
|
||||
railties (>= 7.0.0)
|
||||
tailwindcss-rails (2.6.0-aarch64-linux)
|
||||
tailwindcss-rails (2.6.1-aarch64-linux)
|
||||
railties (>= 7.0.0)
|
||||
tailwindcss-rails (2.6.0-arm-linux)
|
||||
tailwindcss-rails (2.6.1-arm-linux)
|
||||
railties (>= 7.0.0)
|
||||
tailwindcss-rails (2.6.0-arm64-darwin)
|
||||
tailwindcss-rails (2.6.1-arm64-darwin)
|
||||
railties (>= 7.0.0)
|
||||
tailwindcss-rails (2.6.0-x86_64-darwin)
|
||||
tailwindcss-rails (2.6.1-x86_64-darwin)
|
||||
railties (>= 7.0.0)
|
||||
tailwindcss-rails (2.6.0-x86_64-linux)
|
||||
tailwindcss-rails (2.6.1-x86_64-linux)
|
||||
railties (>= 7.0.0)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
|
||||
@@ -35,6 +35,11 @@ There are 3 primary ways to use the Maybe app:
|
||||
|
||||
## Local Development Setup
|
||||
|
||||
**If you are trying to _self-host_ the Maybe app, stop here. You
|
||||
should [read this guide to get started](docs/hosting/docker.md).**
|
||||
|
||||
The instructions below are for developers to get started with contributing to the app.
|
||||
|
||||
### Requirements
|
||||
|
||||
- Ruby 3.3.1
|
||||
|
||||
@@ -10,40 +10,18 @@
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.prose {
|
||||
table {
|
||||
@apply divide-y divide-gray-300;
|
||||
}
|
||||
|
||||
tr {
|
||||
@apply divide-x divide-gray-100;
|
||||
}
|
||||
|
||||
th {
|
||||
@apply whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-gray-900;
|
||||
}
|
||||
|
||||
tbody {
|
||||
@apply divide-y divide-gray-200;
|
||||
}
|
||||
|
||||
td {
|
||||
@apply px-2 py-2 text-sm text-gray-500 whitespace-nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.form-field {
|
||||
@apply relative border border-alpha-black-100 bg-white rounded-md shadow-xs;
|
||||
@apply focus-within:shadow-none focus-within:border-gray-900 focus-within:ring-4 focus-within:ring-gray-100;
|
||||
@apply relative rounded-md border bg-white border-alpha-black-100 shadow-xs;
|
||||
@apply focus-within:border-gray-900 focus-within:shadow-none focus-within:ring-4 focus-within:ring-gray-100;
|
||||
}
|
||||
|
||||
.form-field__label {
|
||||
@apply px-3 pt-2 pb-0 block text-xs text-gray-500;
|
||||
@apply block px-3 pt-2 pb-0 text-xs text-gray-500;
|
||||
}
|
||||
|
||||
.form-field__input {
|
||||
@apply px-3 pb-2 pt-1 text-sm w-full bg-transparent border-none opacity-100;
|
||||
@apply focus:outline-none focus:ring-0 focus:opacity-100;
|
||||
@apply w-full border-none bg-transparent px-3 pt-1 pb-2 text-sm opacity-100;
|
||||
@apply focus:opacity-100 focus:outline-none focus:ring-0;
|
||||
@apply placeholder-shown:opacity-50;
|
||||
@apply disabled:opacity-50;
|
||||
}
|
||||
@@ -53,12 +31,48 @@
|
||||
}
|
||||
|
||||
.form-field__submit {
|
||||
@apply w-full p-3 text-center text-white bg-black rounded-lg cursor-pointer hover:bg-gray-700;
|
||||
@apply w-full cursor-pointer rounded-lg bg-black p-3 text-center text-white hover:bg-gray-700;
|
||||
}
|
||||
|
||||
input:checked + label + .toggle-switch-dot {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
[type='checkbox'].maybe-checkbox {
|
||||
@apply rounded-sm;
|
||||
}
|
||||
|
||||
[type='checkbox'].maybe-checkbox--light {
|
||||
@apply border-alpha-black-200 checked:bg-gray-900 checked:ring-gray-900 focus:ring-gray-900 focus-visible:ring-gray-900 checked:hover:bg-gray-500;
|
||||
}
|
||||
|
||||
[type='checkbox'].maybe-checkbox--dark {
|
||||
@apply ring-gray-900 checked:text-white;
|
||||
}
|
||||
|
||||
[type='checkbox'].maybe-checkbox--dark:checked {
|
||||
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='111827' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
.maybe-switch {
|
||||
@apply block bg-gray-100 w-9 h-5 rounded-full cursor-pointer;
|
||||
@apply after:content-[''] after:block after:absolute after:top-0.5 after:left-0.5 after:bg-white after:w-4 after:h-4 after:rounded-full after:transition-transform after:duration-300 after:ease-in-out;
|
||||
@apply peer-checked:bg-green-600 peer-checked:after:translate-x-4;
|
||||
}
|
||||
|
||||
.prose--github-release-notes {
|
||||
.octicon {
|
||||
@apply inline-block overflow-visible align-text-bottom fill-current;
|
||||
}
|
||||
|
||||
.dropdown-caret {
|
||||
@apply content-none border-4 border-b-0 border-transparent border-t-gray-500 size-0 inline-block;
|
||||
}
|
||||
|
||||
.user-mention {
|
||||
@apply font-bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Small, single purpose classes that should take precedence over other styles */
|
||||
|
||||
@@ -2,10 +2,12 @@ class AccountsController < ApplicationController
|
||||
layout "with_sidebar"
|
||||
|
||||
include Filterable
|
||||
before_action :set_account, only: %i[ show update destroy sync ]
|
||||
before_action :set_account, only: %i[ edit show destroy sync update ]
|
||||
after_action :sync_account, only: :create
|
||||
|
||||
def index
|
||||
@accounts = Current.family.accounts
|
||||
@institutions = Current.family.institutions
|
||||
@accounts = Current.family.accounts.ungrouped.alphabetically
|
||||
end
|
||||
|
||||
def summary
|
||||
@@ -25,6 +27,10 @@ class AccountsController < ApplicationController
|
||||
balance: nil,
|
||||
accountable: Accountable.from_type(params[:type])&.new
|
||||
)
|
||||
|
||||
if params[:institution_id]
|
||||
@account.institution = Current.family.institutions.find_by(id: params[:institution_id])
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
@@ -36,36 +42,19 @@ class AccountsController < ApplicationController
|
||||
end
|
||||
|
||||
def update
|
||||
if @account.update(account_params.except(:accountable_type))
|
||||
|
||||
@account.sync_later if account_params[:is_active] == "1" && @account.can_sync?
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to accounts_path, notice: t(".success") }
|
||||
format.turbo_stream do
|
||||
render turbo_stream: [
|
||||
turbo_stream.append("notification-tray", partial: "shared/notification", locals: { type: "success", content: { body: t(".success") } }),
|
||||
turbo_stream.replace("account_#{@account.id}", partial: "accounts/account", locals: { account: @account })
|
||||
]
|
||||
end
|
||||
end
|
||||
else
|
||||
render "show", status: :unprocessable_entity
|
||||
end
|
||||
@account.update! account_params.except(:accountable_type)
|
||||
redirect_back_or_to account_path(@account), notice: t(".success")
|
||||
end
|
||||
|
||||
def create
|
||||
@account = Current.family.accounts.build(account_params.except(:accountable_type, :start_date))
|
||||
@account.accountable = Accountable.from_type(account_params[:accountable_type])&.new
|
||||
@account = Current.family
|
||||
.accounts
|
||||
.create_with_optional_start_balance! \
|
||||
attributes: account_params.except(:start_date, :start_balance),
|
||||
start_date: account_params[:start_date],
|
||||
start_balance: account_params[:start_balance]
|
||||
|
||||
if @account.save
|
||||
@valuation = @account.valuations.new(date: account_params[:start_date] || Date.today, value: @account.balance, currency: @account.currency)
|
||||
@valuation.save!
|
||||
|
||||
redirect_to accounts_path, notice: t(".success")
|
||||
else
|
||||
render "new", status: :unprocessable_entity
|
||||
end
|
||||
redirect_back_or_to account_path(@account), notice: t(".success")
|
||||
end
|
||||
|
||||
def destroy
|
||||
@@ -74,22 +63,11 @@ class AccountsController < ApplicationController
|
||||
end
|
||||
|
||||
def sync
|
||||
if @account.can_sync?
|
||||
unless @account.syncing?
|
||||
@account.sync_later
|
||||
respond_to do |format|
|
||||
format.html { redirect_to account_path(@account), notice: t(".success") }
|
||||
format.turbo_stream do
|
||||
render turbo_stream: turbo_stream.append("notification-tray", partial: "shared/notification", locals: { type: "success", content: { body: t(".success") } })
|
||||
end
|
||||
end
|
||||
else
|
||||
respond_to do |format|
|
||||
format.html { redirect_to account_path(@account), notice: t(".cannot_sync") }
|
||||
format.turbo_stream do
|
||||
render turbo_stream: turbo_stream.append("notification-tray", partial: "shared/notification", locals: { type: "error", content: { body: t(".cannot_sync") } })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
redirect_to account_path(@account), notice: t(".success")
|
||||
end
|
||||
|
||||
private
|
||||
@@ -99,6 +77,10 @@ class AccountsController < ApplicationController
|
||||
end
|
||||
|
||||
def account_params
|
||||
params.require(:account).permit(:name, :accountable_type, :balance, :start_date, :currency, :subtype, :is_active)
|
||||
params.require(:account).permit(:name, :accountable_type, :balance, :start_date, :start_balance, :currency, :subtype, :is_active, :institution_id)
|
||||
end
|
||||
|
||||
def sync_account
|
||||
@account.sync_later
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,20 +2,8 @@ class ApplicationController < ActionController::Base
|
||||
include Authentication, Invitable, SelfHostable
|
||||
include Pagy::Backend
|
||||
|
||||
before_action :sync_accounts
|
||||
|
||||
default_form_builder ApplicationFormBuilder
|
||||
|
||||
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
|
||||
allow_browser versions: :modern
|
||||
|
||||
private
|
||||
|
||||
def sync_accounts
|
||||
return if Current.user.blank?
|
||||
|
||||
if Current.user.last_login_at.nil? || Current.user.last_login_at.before?(Date.current.beginning_of_day)
|
||||
Current.family.sync_accounts
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,7 +9,8 @@ class ImportsController < ApplicationController
|
||||
end
|
||||
|
||||
def new
|
||||
@import = Import.new
|
||||
account = Current.family.accounts.find_by(id: params[:account_id])
|
||||
@import = Import.new account: account
|
||||
end
|
||||
|
||||
def edit
|
||||
|
||||
35
app/controllers/institutions_controller.rb
Normal file
35
app/controllers/institutions_controller.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
class InstitutionsController < ApplicationController
|
||||
before_action :set_institution, except: %i[ new create ]
|
||||
|
||||
def new
|
||||
@institution = Institution.new
|
||||
end
|
||||
|
||||
def create
|
||||
Current.family.institutions.create!(institution_params)
|
||||
redirect_to accounts_path, notice: t(".success")
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
@institution.update!(institution_params)
|
||||
redirect_to accounts_path, notice: t(".success")
|
||||
end
|
||||
|
||||
def destroy
|
||||
@institution.destroy!
|
||||
redirect_to accounts_path, notice: t(".success")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def institution_params
|
||||
params.require(:institution).permit(:name, :logo)
|
||||
end
|
||||
|
||||
def set_institution
|
||||
@institution = Current.family.institutions.find(params[:id])
|
||||
end
|
||||
end
|
||||
@@ -31,6 +31,7 @@ class PagesController < ApplicationController
|
||||
end
|
||||
|
||||
def changelog
|
||||
@releases_notes = Provider::Github.new.fetch_latest_releases_notes
|
||||
end
|
||||
|
||||
def feedback
|
||||
|
||||
@@ -47,7 +47,7 @@ class Settings::HostingsController < SettingsController
|
||||
end
|
||||
end
|
||||
|
||||
if hosting_params[:upgrades_mode] != "manual" && hosting_params[:render_deploy_hook].blank?
|
||||
if hosting_params[:upgrades_mode] == "auto" && hosting_params[:render_deploy_hook].blank?
|
||||
@errors.add(:render_deploy_hook, t("settings.hostings.update.render_deploy_hook_error"))
|
||||
end
|
||||
|
||||
|
||||
@@ -52,6 +52,24 @@ class TransactionsController < ApplicationController
|
||||
redirect_to transactions_url, notice: t(".success")
|
||||
end
|
||||
|
||||
def bulk_delete
|
||||
destroyed = Current.family.transactions.destroy_by(id: bulk_delete_params[:transaction_ids])
|
||||
redirect_to transactions_url, notice: t(".success", count: destroyed.count)
|
||||
end
|
||||
|
||||
def bulk_edit
|
||||
end
|
||||
|
||||
def bulk_update
|
||||
transactions = Current.family.transactions.where(id: bulk_update_params[:transaction_ids])
|
||||
if transactions.update_all(bulk_update_params.except(:transaction_ids).to_h.compact_blank!)
|
||||
redirect_to transactions_url, notice: t(".success", count: transactions.count)
|
||||
else
|
||||
flash.now[:error] = t(".failure")
|
||||
render :index, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_transaction
|
||||
@@ -70,11 +88,19 @@ class TransactionsController < ApplicationController
|
||||
params[:transaction][:nature].to_s.inquiry
|
||||
end
|
||||
|
||||
def bulk_delete_params
|
||||
params.require(:bulk_delete).permit(transaction_ids: [])
|
||||
end
|
||||
|
||||
def bulk_update_params
|
||||
params.require(:bulk_update).permit(:date, :notes, :excluded, :category_id, :merchant_id, transaction_ids: [])
|
||||
end
|
||||
|
||||
def search_params
|
||||
params.fetch(:q, {}).permit(:start_date, :end_date, :search, accounts: [], account_ids: [], categories: [], merchants: [])
|
||||
end
|
||||
|
||||
def transaction_params
|
||||
params.require(:transaction).permit(:name, :date, :amount, :currency, :notes, :excluded, :category_id, :merchant_id, tag_ids: [], taggings_attributes: [ :id, :tag_id, :_destroy ])
|
||||
params.require(:transaction).permit(:name, :date, :amount, :currency, :notes, :excluded, :category_id, :merchant_id, tag_ids: [])
|
||||
end
|
||||
end
|
||||
|
||||
5
app/helpers/institutions_helper.rb
Normal file
5
app/helpers/institutions_helper.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
module InstitutionsHelper
|
||||
def institution_logo(institution)
|
||||
institution.logo.attached? ? institution.logo : institution.logo_url
|
||||
end
|
||||
end
|
||||
126
app/javascript/controllers/bulk_select_controller.js
Normal file
126
app/javascript/controllers/bulk_select_controller.js
Normal file
@@ -0,0 +1,126 @@
|
||||
import {Controller} from "@hotwired/stimulus"
|
||||
|
||||
// Connects to data-controller="bulk-select"
|
||||
export default class extends Controller {
|
||||
static targets = ["row", "group", "selectionBar", "selectionBarText", "bulkEditDrawerTitle"]
|
||||
static values = {
|
||||
resource: String,
|
||||
selectedIds: {type: Array, default: []}
|
||||
}
|
||||
|
||||
connect() {
|
||||
document.addEventListener("turbo:load", this.#updateView)
|
||||
|
||||
this.#updateView()
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
document.removeEventListener("turbo:load", this.#updateView)
|
||||
}
|
||||
|
||||
bulkEditDrawerTitleTargetConnected(element) {
|
||||
element.innerText = `Edit ${this.selectedIdsValue.length} ${this.#pluralizedResourceName()}`
|
||||
}
|
||||
|
||||
submitBulkRequest(e) {
|
||||
const form = e.target.closest("form");
|
||||
const scope = e.params.scope
|
||||
this.#addHiddenFormInputsForSelectedIds(form, `${scope}[transaction_ids][]`, this.selectedIdsValue)
|
||||
form.requestSubmit()
|
||||
}
|
||||
|
||||
togglePageSelection(e) {
|
||||
if (e.target.checked) {
|
||||
this.#selectAll()
|
||||
} else {
|
||||
this.deselectAll()
|
||||
}
|
||||
}
|
||||
|
||||
toggleGroupSelection(e) {
|
||||
const group = this.groupTargets.find(group => group.contains(e.target))
|
||||
|
||||
this.#rowsForGroup(group).forEach(row => {
|
||||
if (e.target.checked) {
|
||||
this.#addToSelection(row.dataset.id)
|
||||
} else {
|
||||
this.#removeFromSelection(row.dataset.id)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
toggleRowSelection(e) {
|
||||
if (e.target.checked) {
|
||||
this.#addToSelection(e.target.dataset.id)
|
||||
} else {
|
||||
this.#removeFromSelection(e.target.dataset.id)
|
||||
}
|
||||
}
|
||||
|
||||
deselectAll() {
|
||||
this.selectedIdsValue = []
|
||||
}
|
||||
|
||||
selectedIdsValueChanged() {
|
||||
this.#updateView()
|
||||
}
|
||||
|
||||
#addHiddenFormInputsForSelectedIds(form, paramName, transactionIds) {
|
||||
transactionIds.forEach(id => {
|
||||
const input = document.createElement("input");
|
||||
input.type = 'hidden'
|
||||
input.name = paramName
|
||||
input.value = id
|
||||
form.appendChild(input)
|
||||
})
|
||||
}
|
||||
|
||||
#rowsForGroup(group) {
|
||||
return this.rowTargets.filter(row => group.contains(row))
|
||||
}
|
||||
|
||||
#addToSelection(idToAdd) {
|
||||
this.selectedIdsValue = Array.from(
|
||||
new Set([...this.selectedIdsValue, idToAdd])
|
||||
)
|
||||
}
|
||||
|
||||
#removeFromSelection(idToRemove) {
|
||||
this.selectedIdsValue = this.selectedIdsValue.filter(id => id !== idToRemove)
|
||||
}
|
||||
|
||||
#selectAll() {
|
||||
this.selectedIdsValue = this.rowTargets.map(t => t.dataset.id)
|
||||
}
|
||||
|
||||
#updateView = () => {
|
||||
this.#updateSelectionBar()
|
||||
this.#updateGroups()
|
||||
this.#updateRows()
|
||||
}
|
||||
|
||||
#updateSelectionBar() {
|
||||
const count = this.selectedIdsValue.length
|
||||
this.selectionBarTextTarget.innerText = `${count} ${this.#pluralizedResourceName()} selected`
|
||||
this.selectionBarTarget.hidden = count === 0
|
||||
this.selectionBarTarget.querySelector("input[type='checkbox']").checked = count > 0
|
||||
}
|
||||
|
||||
#pluralizedResourceName() {
|
||||
return `${this.resourceValue}${this.selectedIdsValue.length === 1 ? "" : "s"}`
|
||||
}
|
||||
|
||||
#updateGroups() {
|
||||
this.groupTargets.forEach(group => {
|
||||
const rows = this.rowTargets.filter(row => group.contains(row))
|
||||
const groupSelected = rows.every(row => this.selectedIdsValue.includes(row.dataset.id))
|
||||
group.querySelector("input[type='checkbox']").checked = groupSelected
|
||||
})
|
||||
}
|
||||
|
||||
#updateRows() {
|
||||
this.rowTargets.forEach(row => {
|
||||
row.checked = this.selectedIdsValue.includes(row.dataset.id)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ export default class extends Controller {
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
this.imagePreviewTarget.innerHTML = `<img src="${e.target.result}" alt="Preview" class="w-24 h-24 rounded-full object-cover" />`;
|
||||
this.imagePreviewTarget.innerHTML = `<img src="${e.target.result}" alt="Preview" class="w-full h-full rounded-full object-cover" />`;
|
||||
this.templateTarget.classList.add("hidden");
|
||||
this.clearBtnTarget.classList.remove("hidden");
|
||||
};
|
||||
|
||||
@@ -2,10 +2,12 @@ class Account < ApplicationRecord
|
||||
include Syncable
|
||||
include Monetizable
|
||||
|
||||
broadcasts_refreshes
|
||||
|
||||
validates :family, presence: true
|
||||
|
||||
broadcasts_refreshes
|
||||
belongs_to :family
|
||||
belongs_to :institution, optional: true
|
||||
has_many :balances, dependent: :destroy
|
||||
has_many :valuations, dependent: :destroy
|
||||
has_many :transactions, dependent: :destroy
|
||||
@@ -19,6 +21,7 @@ class Account < ApplicationRecord
|
||||
scope :assets, -> { where(classification: "asset") }
|
||||
scope :liabilities, -> { where(classification: "liability") }
|
||||
scope :alphabetically, -> { order(:name) }
|
||||
scope :ungrouped, -> { where(institution_id: nil) }
|
||||
|
||||
delegated_type :accountable, types: Accountable::TYPES, dependent: :destroy
|
||||
|
||||
@@ -80,4 +83,20 @@ class Account < ApplicationRecord
|
||||
|
||||
grouped_accounts
|
||||
end
|
||||
|
||||
def self.create_with_optional_start_balance!(attributes:, start_date: nil, start_balance: nil)
|
||||
account = self.new(attributes.except(:accountable_type))
|
||||
account.accountable = Accountable.from_type(attributes[:accountable_type])&.new
|
||||
|
||||
# Always build the initial valuation
|
||||
account.valuations.build(date: Date.current, value: attributes[:balance], currency: account.currency)
|
||||
|
||||
# Conditionally build the optional start valuation
|
||||
if start_date.present? && start_balance.present?
|
||||
account.valuations.build(date: start_date, value: start_balance, currency: account.currency)
|
||||
end
|
||||
|
||||
account.save!
|
||||
account
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,6 +2,7 @@ class Family < ApplicationRecord
|
||||
has_many :users, dependent: :destroy
|
||||
has_many :tags, dependent: :destroy
|
||||
has_many :accounts, dependent: :destroy
|
||||
has_many :institutions, dependent: :destroy
|
||||
has_many :transactions, through: :accounts
|
||||
has_many :imports, through: :accounts
|
||||
has_many :transaction_categories, dependent: :destroy, class_name: "Transaction::Category"
|
||||
|
||||
@@ -148,15 +148,18 @@ class Import < ApplicationRecord
|
||||
|
||||
name_field = Import::Field.new \
|
||||
key: "name",
|
||||
label: "Name"
|
||||
label: "Name",
|
||||
is_optional: true
|
||||
|
||||
category_field = Import::Field.new \
|
||||
key: "category",
|
||||
label: "Category"
|
||||
label: "Category",
|
||||
is_optional: true
|
||||
|
||||
tags_field = Import::Field.new \
|
||||
key: "tags",
|
||||
label: "Tags"
|
||||
label: "Tags",
|
||||
is_optional: true
|
||||
|
||||
amount_field = Import::Field.new \
|
||||
key: "amount",
|
||||
|
||||
@@ -15,12 +15,17 @@ class Import::Field
|
||||
|
||||
attr_reader :key, :label, :validator
|
||||
|
||||
def initialize(key:, label:, validator: nil)
|
||||
def initialize(key:, label:, is_optional: false, validator: nil)
|
||||
@key = key.to_s
|
||||
@label = label
|
||||
@is_optional = is_optional
|
||||
@validator = validator
|
||||
end
|
||||
|
||||
def optional?
|
||||
@is_optional
|
||||
end
|
||||
|
||||
def define_validator(validator = nil, &block)
|
||||
@validator = validator || block
|
||||
end
|
||||
|
||||
7
app/models/institution.rb
Normal file
7
app/models/institution.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
class Institution < ApplicationRecord
|
||||
belongs_to :family
|
||||
has_many :accounts, dependent: :nullify
|
||||
has_one_attached :logo
|
||||
|
||||
scope :alphabetically, -> { order(name: :asc) }
|
||||
end
|
||||
@@ -40,6 +40,26 @@ class Provider::Github
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_latest_releases_notes
|
||||
begin
|
||||
Rails.cache.fetch("latest_github_releases_notes", expires_in: 2.hours) do
|
||||
releases = Octokit.releases(repo)
|
||||
releases.map do |release|
|
||||
{
|
||||
avatar: release.author.avatar_url,
|
||||
name: release.name,
|
||||
published_at: release.published_at,
|
||||
body: Octokit.markdown(release.body, mode: "gfm", context: repo)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
rescue => e
|
||||
Rails.logger.error "Failed to fetch latest GitHub releases notes: #{e.message}"
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def repo
|
||||
"#{owner}/#{name}"
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
<%= turbo_frame_tag dom_id(account) do %>
|
||||
<div class="p-4 flex items-center justify-between gap-3">
|
||||
<div class="p-4 flex items-center justify-between gap-3 group/account">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 flex items-center justify-center rounded-full text-xs font-medium <%= account.is_active ? "bg-blue-500/10 text-blue-500" : "bg-gray-500/10 text-gray-500" %>">
|
||||
<%= account.name[0].upcase %>
|
||||
</div>
|
||||
<p class="text-sm font-medium <%= account.is_active ? "text-gray-900" : "text-gray-400" %>">
|
||||
<%= account.name %>
|
||||
</p>
|
||||
<%= link_to account.name, account, class: [(account.is_active ? "text-gray-900" : "text-gray-400"), "text-sm font-medium hover:underline"], data: { turbo_frame: "_top" } %>
|
||||
|
||||
<%= link_to edit_account_path(account), data: { turbo_frame: :modal }, class: "group-hover/account:flex hidden hover:opacity-80 items-center justify-center" do %>
|
||||
<%= lucide_icon "pencil-line", class: "w-4 h-4 text-gray-500" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex items-center gap-8">
|
||||
<p class="text-sm font-medium <%= account.is_active ? "text-gray-900" : "text-gray-400" %>">
|
||||
<%= format_money account.balance_money %>
|
||||
</p>
|
||||
<%= form_with model: account, method: :patch, html: { class: "flex items-center", data: { turbo_frame: "_top" } } do |form| %>
|
||||
|
||||
<%= form_with model: account,
|
||||
namespace: account.id,
|
||||
builder: ActionView::Helpers::FormBuilder,
|
||||
data: { controller: "auto-submit-form", turbo_frame: "_top" } do |form| %>
|
||||
<div class="relative inline-block select-none">
|
||||
<%= form.check_box :is_active, class: "sr-only peer", id: "is_active_#{account.id}", onchange: "this.form.requestSubmit();" %>
|
||||
<label for="is_active_<%= account.id %>" class="block bg-gray-100 w-9 h-5 rounded-full cursor-pointer after:content-[''] after:block after:absolute after:top-0.5 after:left-0.5 after:bg-white after:w-4 after:h-4 after:rounded-full after:transition-transform after:duration-300 after:ease-in-out peer-checked:bg-green-600 peer-checked:after:translate-x-4"></label>
|
||||
<%= form.check_box :is_active, { class: "sr-only peer", data: { "auto-submit-form-target": "auto" } } %>
|
||||
<%= form.label :is_active, " ".html_safe, class: "maybe-switch" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<%= link_to new_account_path(step: "method", type: type.class.name.demodulize), class: "flex items-center gap-4 w-full text-center focus:outline-none focus:bg-gray-25 border border-transparent focus:border focus:border-gray-200 block px-2 hover:bg-gray-25 rounded-lg p-2" do %>
|
||||
<%= link_to new_account_path(step: "method", type: type.class.name.demodulize, institution_id: params[:institution_id]), class: "flex items-center gap-4 w-full text-center focus:outline-none focus:bg-gray-25 border border-transparent focus:border focus:border-gray-200 block px-2 hover:bg-gray-25 rounded-lg p-2" do %>
|
||||
<span class="flex w-8 h-8 shrink-0 grow-0 items-center justify-center rounded-lg <%= bg_color %> border border-alpha-black-25">
|
||||
<%= lucide_icon(icon, class: "#{text_color} w-5 h-5") %>
|
||||
</span>
|
||||
|
||||
17
app/views/accounts/_accountable_group.html.erb
Normal file
17
app/views/accounts/_accountable_group.html.erb
Normal file
@@ -0,0 +1,17 @@
|
||||
<%# locals: (accounts:) %>
|
||||
|
||||
<% accounts.group_by(&:accountable_type).each do |group, accounts| %>
|
||||
<div class="bg-gray-25 p-1 rounded-xl">
|
||||
<div class="flex items-center px-4 py-2 text-xs font-medium text-gray-500">
|
||||
<p><%= to_accountable_title(Accountable.from_type(group)) %></p>
|
||||
<span class="text-gray-400 mx-2">·</span>
|
||||
<p><%= accounts.count %></p>
|
||||
<p class="ml-auto"><%= format_money accounts.sum(&:balance_money) %></p>
|
||||
</div>
|
||||
<div class="bg-white">
|
||||
<% accounts.each do |account| %>
|
||||
<%= render account %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
11
app/views/accounts/_empty.html.erb
Normal file
11
app/views/accounts/_empty.html.erb
Normal file
@@ -0,0 +1,11 @@
|
||||
<div class="flex justify-center items-center h-[800px] text-sm">
|
||||
<div class="text-center flex flex-col items-center max-w-[300px]">
|
||||
<%= tag.p t(".no_accounts"), class: "text-gray-900 mb-1 font-medium" %>
|
||||
<%= tag.p t(".empty_message"), class: "text-gray-500 mb-4" %>
|
||||
|
||||
<%= link_to new_account_path, class: "w-fit flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-2 pr-3", data: { turbo_frame: "modal" } do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||
<span><%= t(".new_account") %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -6,7 +6,7 @@
|
||||
<%= text %>
|
||||
</span>
|
||||
<% else %>
|
||||
<%= link_to new_account_path(type: type.class.name.demodulize), class: "flex items-center gap-4 w-full text-center focus:outline-none focus:bg-gray-50 border border-transparent focus:border focus:border-gray-200 px-2 hover:bg-gray-50 rounded-lg p-2" do %>
|
||||
<%= link_to new_account_path(type: type.class.name.demodulize, institution_id: params[:institution_id]), class: "flex items-center gap-4 w-full text-center focus:outline-none focus:bg-gray-50 border border-transparent focus:border focus:border-gray-200 px-2 hover:bg-gray-50 rounded-lg p-2" do %>
|
||||
<span class="flex w-8 h-8 shrink-0 grow-0 items-center justify-center rounded-lg bg-alpha-black-50 shadow-[inset_0_0_0_1px_rgba(0,0,0,0.02)]">
|
||||
<%= lucide_icon(icon, class: "text-gray-500 w-5 h-5") %>
|
||||
</span>
|
||||
|
||||
21
app/views/accounts/_header.html.erb
Normal file
21
app/views/accounts/_header.html.erb
Normal file
@@ -0,0 +1,21 @@
|
||||
<header class="flex justify-between items-center text-gray-900 font-medium">
|
||||
<h1 class="text-xl"><%= t(".accounts") %></h1>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<%= contextual_menu do %>
|
||||
<div class="w-48 p-1 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
||||
<%= link_to accounts_path(return_to: summary_accounts_path),
|
||||
class: "block w-full py-2 px-3 space-x-2 text-gray-900 hover:bg-gray-50 flex items-center rounded-lg font-normal" do %>
|
||||
<%= lucide_icon "settings", class: "w-5 h-5 text-gray-500" %>
|
||||
<span class="text-black"><%= t(".manage") %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% end %>
|
||||
|
||||
<%= link_to new_account_path, class: "rounded-lg bg-gray-900 text-white flex items-center gap-1 justify-center hover:bg-gray-700 px-3 py-2", data: { turbo_frame: :modal } do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||
<p class="text-sm font-medium"><%= t(".new") %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
</header>
|
||||
69
app/views/accounts/_institution_accounts.html.erb
Normal file
69
app/views/accounts/_institution_accounts.html.erb
Normal file
@@ -0,0 +1,69 @@
|
||||
<%# locals: (institution:) %>
|
||||
|
||||
<details open class="group bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl">
|
||||
<summary class="flex items-center gap-2 focus-visible:outline-none">
|
||||
<%= lucide_icon "chevron-right", class: "group-open:transform group-open:rotate-90 text-gray-500 w-5" %>
|
||||
|
||||
<div class="flex items-center justify-center h-8 w-8 bg-blue-600/10 rounded-full bg-black/5">
|
||||
<% if institution_logo(institution) %>
|
||||
<%= image_tag institution_logo(institution), class: "rounded-full h-full w-full" %>
|
||||
<% else %>
|
||||
<div class="flex items-center justify-center">
|
||||
<%= tag.p institution.name.first.upcase, class: "text-blue-600 text-xs font-medium" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= link_to institution.name, edit_institution_path(institution), data: { turbo_frame: :modal }, class: "text-sm font-medium text-gray-900 ml-1 mr-auto hover:underline" %>
|
||||
|
||||
<%= contextual_menu do %>
|
||||
<div class="w-48 p-1 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
||||
<%= link_to new_account_path(institution_id: institution.id),
|
||||
class: "block w-full py-2 px-3 space-x-2 text-gray-900 hover:bg-gray-50 flex items-center rounded-lg",
|
||||
data: { turbo_frame: :modal } do %>
|
||||
<%= lucide_icon "plus", class: "w-5 h-5 text-gray-500" %>
|
||||
|
||||
<span><%= t(".add_account_to_institution") %></span>
|
||||
<% end %>
|
||||
|
||||
<%= link_to edit_institution_path(institution),
|
||||
class: "block w-full py-2 px-3 space-x-2 text-gray-900 hover:bg-gray-50 flex items-center rounded-lg",
|
||||
data: { turbo_frame: :modal } do %>
|
||||
<%= lucide_icon "pencil-line", class: "w-5 h-5 text-gray-500" %>
|
||||
|
||||
<span><%= t(".edit") %></span>
|
||||
<% end %>
|
||||
|
||||
<%= button_to institution_path(institution),
|
||||
method: :delete,
|
||||
class: "block w-full py-2 px-3 space-x-2 text-red-600 hover:bg-red-50 flex items-center rounded-lg",
|
||||
data: {
|
||||
turbo_confirm: {
|
||||
title: t(".confirm_title"),
|
||||
body: t(".confirm_body"),
|
||||
accept: t(".confirm_accept")
|
||||
}
|
||||
} do %>
|
||||
<%= lucide_icon "trash-2", class: "w-5 h-5" %>
|
||||
|
||||
<span><%= t(".delete") %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% end %>
|
||||
</summary>
|
||||
|
||||
<div class="space-y-4 mt-4">
|
||||
<% if institution.accounts.any? %>
|
||||
<%= render "accountable_group", accounts: institution.accounts %>
|
||||
<% else %>
|
||||
<div class="p-4 flex flex-col gap-3 items-center justify-center">
|
||||
<p class="text-gray-500 text-sm">There are no accounts in this financial institution</p>
|
||||
<%= link_to new_account_path(institution_id: institution.id), class: "w-fit flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-1.5 pr-2", data: { turbo_frame: "modal" } do %>
|
||||
<%= lucide_icon("plus", class: "w-4 h-4") %>
|
||||
<span><%= t(".new_account") %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</details>
|
||||
17
app/views/accounts/_institutionless_accounts.html.erb
Normal file
17
app/views/accounts/_institutionless_accounts.html.erb
Normal file
@@ -0,0 +1,17 @@
|
||||
<%# locals: (accounts:) %>
|
||||
|
||||
<details open class="group bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl">
|
||||
<summary class="flex items-center gap-2 focus-visible:outline-none">
|
||||
<%= lucide_icon "chevron-right", class: "group-open:transform group-open:rotate-90 text-gray-500 w-5" %>
|
||||
|
||||
<div class="flex items-center justify-center h-8 w-8 rounded-full bg-black/5">
|
||||
<%= lucide_icon("folder-pen", class: "w-5 h-5 text-gray-500") %>
|
||||
</div>
|
||||
|
||||
<span class="mr-auto text-sm font-medium text-gray-900"><%= t(".other_accounts") %></span>
|
||||
</summary>
|
||||
|
||||
<div class="space-y-4 mt-4">
|
||||
<%= render "accountable_group", accounts: accounts %>
|
||||
</div>
|
||||
</details>
|
||||
@@ -13,7 +13,7 @@
|
||||
<% else %>
|
||||
<div class="space-y-6">
|
||||
<% transactions.group_by(&:date).each do |date, transactions| %>
|
||||
<%= transactions_group(date, transactions) %>
|
||||
<%= transactions_group(date, transactions, "accounts/transactions/transaction") %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
21
app/views/accounts/edit.html.erb
Normal file
21
app/views/accounts/edit.html.erb
Normal file
@@ -0,0 +1,21 @@
|
||||
<%= modal do %>
|
||||
<article class="mx-auto w-full p-4 space-y-4 min-w-[350px]">
|
||||
<header class="flex justify-between">
|
||||
<h2 class="font-medium text-xl"><%= t(".edit", account: @account.name) %></h2>
|
||||
<%= lucide_icon "x", class: "w-5 h-5 text-gray-500", data: { action: "click->modal#close" } %>
|
||||
</header>
|
||||
|
||||
<%= form_with model: @account, data: { turbo_frame: "_top" } do |f| %>
|
||||
<%= f.text_field :name, label: "Name" %>
|
||||
|
||||
<div class="relative">
|
||||
<%= f.collection_select :institution_id, Current.family.institutions.alphabetically, :id, :name, { include_blank: t(".ungrouped"), label: t(".institution") } %>
|
||||
<%= link_to new_institution_path do %>
|
||||
<%= lucide_icon "plus", class: "text-gray-700 hover:text-gray-500 w-4 h-4 absolute right-3 top-2" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= f.submit %>
|
||||
<% end %>
|
||||
</article>
|
||||
<% end %>
|
||||
@@ -1,62 +1,45 @@
|
||||
<% content_for :sidebar do %>
|
||||
<%= render "settings/nav" %>
|
||||
<% end %>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-xl font-medium text-gray-900">Accounts</h1>
|
||||
<%= link_to new_account_path, class: "flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-2 pr-3", data: { turbo_frame: "modal" } do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||
<span><%= t(".new_account") %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if @accounts.empty? %>
|
||||
<div class="flex justify-center items-center h-[800px] text-sm">
|
||||
<div class="text-center flex flex-col items-center max-w-[300px]">
|
||||
<p class="text-gray-900 mb-1 font-medium">No accounts yet</p>
|
||||
<p class="text-gray-500 mb-4">Add an account either via connection, importing or entering manually.</p>
|
||||
<%= link_to new_account_path, class: "w-fit flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-2 pr-3", data: { turbo_frame: "modal" } do %>
|
||||
<header class="flex justify-between items-center text-gray-900 font-medium">
|
||||
<h1 class="text-xl"><%= t(".accounts") %></h1>
|
||||
<div class="flex items-center gap-5">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= contextual_menu do %>
|
||||
<div class="w-48 p-1 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
||||
<%= link_to new_institution_path,
|
||||
class: "block w-full py-2 px-3 space-x-2 text-gray-900 hover:bg-gray-50 flex items-center rounded-lg font-normal",
|
||||
data: { turbo_frame: "modal" } do %>
|
||||
<%= lucide_icon "building-2", class: "w-5 h-5 text-gray-500" %>
|
||||
<span class="text-black"><%= t(".add_institution") %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= link_to new_account_path,
|
||||
data: { turbo_frame: "modal" },
|
||||
class: "rounded-lg bg-gray-900 text-white flex items-center gap-1 justify-center hover:bg-gray-700 px-3 py-2" do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||
<span><%= t(".new_account") %></span>
|
||||
<p class="text-sm font-medium"><%= t(".new_account") %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<% if @accounts.empty? && @institutions.empty? %>
|
||||
<%= render "empty" %>
|
||||
<% else %>
|
||||
<div>
|
||||
<% @accounts.by_provider.each do |item| %>
|
||||
<details open class="bg-white group p-4 border border-alpha-black-25 shadow-xs rounded-xl">
|
||||
<summary class="flex items-center gap-2">
|
||||
<%= lucide_icon("chevron-down", class: "hidden group-open:block w-5 h-5 text-gray-500") %>
|
||||
<%= lucide_icon("chevron-right", class: "group-open:hidden w-5 h-5 text-gray-500") %>
|
||||
<% if item[:name] == "Manual accounts" %>
|
||||
<div class="flex items-center justify-center h-8 w-8 rounded-full bg-black/5">
|
||||
<%= lucide_icon("folder-pen", class: "w-5 h-5 text-gray-500") %>
|
||||
</div>
|
||||
<% end %>
|
||||
<span class="text-sm font-medium text-gray-900">
|
||||
<%= item[:name] %>
|
||||
</span>
|
||||
</summary>
|
||||
<div class="space-y-4 mt-4">
|
||||
<% item[:accounts].each do |group, accounts| %>
|
||||
<div class="bg-gray-25 p-1 rounded-xl">
|
||||
<div class="flex items-center px-4 py-2 text-xs font-medium text-gray-500">
|
||||
<p><%= to_accountable_title(Accountable.from_type(group)) %></p>
|
||||
<span class="text-gray-400 mx-2">·</span>
|
||||
<p><%= accounts.count %></p>
|
||||
<p class="ml-auto"><%= format_money accounts.sum(&:balance_money) %></p>
|
||||
</div>
|
||||
<div class="bg-white">
|
||||
<% accounts.each do |account| %>
|
||||
<%= render account %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</details>
|
||||
<div class="space-y-2">
|
||||
<% @institutions.each do |institution| %>
|
||||
<%= render "institution_accounts", institution: %>
|
||||
<% end %>
|
||||
|
||||
<%= render "institutionless_accounts", accounts: @accounts %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="flex justify-between gap-4">
|
||||
<% if self_hosted? %>
|
||||
<%= previous_setting("Self-Hosting", settings_hosting_path) %>
|
||||
|
||||
@@ -20,14 +20,18 @@
|
||||
<div class="border-t border-alpha-black-25 p-4 text-gray-500 text-sm flex justify-between">
|
||||
<div class="flex space-x-5">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span>Select</span> <kbd class="bg-alpha-black-50 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-5 h-5 shrink-0 grow-0 items-center justify-center"><%= lucide_icon("corner-down-left", class: "inline w-3 h-3") %></kbd>
|
||||
<span>Select</span>
|
||||
<kbd class="bg-alpha-black-50 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-5 h-5 shrink-0 grow-0 items-center justify-center"><%= lucide_icon("corner-down-left", class: "inline w-3 h-3") %></kbd>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span>Navigate</span> <kbd class="bg-alpha-black-50 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-5 h-5 shrink-0 grow-0 items-center justify-center"><%= lucide_icon("arrow-up", class: "inline w-3 h-3") %></kbd> <kbd class="bg-alpha-black-50 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-5 h-5 shrink-0 grow-0 items-center justify-center"><%= lucide_icon("arrow-down", class: "inline w-3 h-3") %></kbd>
|
||||
<span>Navigate</span>
|
||||
<kbd class="bg-alpha-black-50 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-5 h-5 shrink-0 grow-0 items-center justify-center"><%= lucide_icon("arrow-up", class: "inline w-3 h-3") %></kbd>
|
||||
<kbd class="bg-alpha-black-50 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-5 h-5 shrink-0 grow-0 items-center justify-center"><%= lucide_icon("arrow-down", class: "inline w-3 h-3") %></kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button data-action="modal#close">Close</button> <kbd class="bg-alpha-black-50 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-8 h-5 shrink-0 grow-0 items-center justify-center text-xs">ESC</kbd>
|
||||
<button data-action="modal#close">Close</button>
|
||||
<kbd class="bg-alpha-black-50 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-8 h-5 shrink-0 grow-0 items-center justify-center text-xs">ESC</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<% elsif params[:step] == 'method' && @account.accountable.present? %>
|
||||
@@ -47,14 +51,18 @@
|
||||
<div class="border-t border-alpha-black-25 p-4 text-gray-500 text-sm flex justify-between">
|
||||
<div class="flex space-x-5">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span>Select</span> <kbd class="bg-alpha-black-50 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-5 h-5 shrink-0 grow-0 items-center justify-center"><%= lucide_icon("corner-down-left", class: "inline w-3 h-3") %></kbd>
|
||||
<span>Select</span>
|
||||
<kbd class="bg-alpha-black-50 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-5 h-5 shrink-0 grow-0 items-center justify-center"><%= lucide_icon("corner-down-left", class: "inline w-3 h-3") %></kbd>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span>Navigate</span> <kbd class="bg-alpha-black-50 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-5 h-5 shrink-0 grow-0 items-center justify-center"><%= lucide_icon("arrow-up", class: "inline w-3 h-3") %></kbd> <kbd class="bg-alpha-black-50 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-5 h-5 shrink-0 grow-0 items-center justify-center"><%= lucide_icon("arrow-down", class: "inline w-3 h-3") %></kbd>
|
||||
<span>Navigate</span>
|
||||
<kbd class="bg-alpha-black-50 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-5 h-5 shrink-0 grow-0 items-center justify-center"><%= lucide_icon("arrow-up", class: "inline w-3 h-3") %></kbd>
|
||||
<kbd class="bg-alpha-black-50 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-5 h-5 shrink-0 grow-0 items-center justify-center"><%= lucide_icon("arrow-down", class: "inline w-3 h-3") %></kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button data-action="modal#close">Close</button> <kbd class="bg-alpha-black-50 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-8 h-5 shrink-0 grow-0 items-center justify-center text-xs">ESC</kbd>
|
||||
<button data-action="modal#close">Close</button>
|
||||
<kbd class="bg-alpha-black-50 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-8 h-5 shrink-0 grow-0 items-center justify-center text-xs">ESC</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
@@ -68,9 +76,19 @@
|
||||
<div class="space-y-4 grow">
|
||||
<%= f.hidden_field :accountable_type %>
|
||||
<%= f.text_field :name, placeholder: t(".name.placeholder"), required: "required", label: t(".name.label"), autofocus: true %>
|
||||
<%= f.collection_select :institution_id, Current.family.institutions.alphabetically, :id, :name, { include_blank: t(".ungrouped"), label: t(".institution") } %>
|
||||
<%= render "accounts/#{permitted_accountable_partial(@account.accountable_type)}", f: f %>
|
||||
<%= f.money_field :balance_money, label: t(".balance.label"), required: "required" %>
|
||||
<%= f.date_field :start_date, label: t(".start_date.label"), required: true, max: Date.today, value: Date.today %>
|
||||
<%= f.money_field :balance_money, label: t(".balance"), required: "required" %>
|
||||
|
||||
<div>
|
||||
<%= check_box_tag :add_start_values, class: "maybe-checkbox maybe-checkbox--light peer mb-1" %>
|
||||
<%= label_tag :add_start_values, t(".optional_start_balance_message"), class: "pl-1 text-sm text-gray-500" %>
|
||||
|
||||
<div class="hidden peer-checked:flex items-center gap-2 mt-3 mb-6">
|
||||
<div class="w-1/2"><%= f.date_field :start_date, label: t(".start_date"), max: Date.current %></div>
|
||||
<div class="w-1/2"><%= f.number_field :start_balance, label: t(".start_balance") %></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%= f.submit "Add #{@account.accountable.model_name.human.downcase}" %>
|
||||
<% end %>
|
||||
|
||||
@@ -9,33 +9,38 @@
|
||||
<%= button_to sync_account_path(@account), method: :post, class: "flex items-center gap-2", title: "Sync Account" do %>
|
||||
<%= lucide_icon "refresh-cw", class: "w-4 h-4 text-gray-900 hover:text-gray-500" %>
|
||||
<% end %>
|
||||
<div class="relative cursor-not-allowed">
|
||||
<div class="flex items-center gap-2 px-3 py-2">
|
||||
<span class="text-gray-900"><%= @account.balance_money.currency.iso_code %> <%= @account.balance_money.currency.symbol %></span>
|
||||
<%= lucide_icon("chevron-down", class: "w-5 h-5 text-gray-500") %>
|
||||
|
||||
<%= contextual_menu do %>
|
||||
<div class="w-48 p-1 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
||||
<%= link_to edit_account_path(@account),
|
||||
data: { turbo_frame: :modal },
|
||||
class: "block w-full py-2 px-3 space-x-2 text-gray-900 hover:bg-gray-50 flex items-center rounded-lg" do %>
|
||||
<%= lucide_icon "pencil-line", class: "w-5 h-5 text-gray-500" %>
|
||||
|
||||
<span><%= t(".edit") %></span>
|
||||
<% end %>
|
||||
|
||||
<%= link_to new_import_path(account_id: @account.id),
|
||||
class: "block w-full py-2 px-3 space-x-2 text-gray-900 hover:bg-gray-50 flex items-center rounded-lg" do %>
|
||||
<%= lucide_icon "download", class: "w-5 h-5 text-gray-500" %>
|
||||
|
||||
<span><%= t(".import") %></span>
|
||||
<% end %>
|
||||
|
||||
<%= button_to account_path(@account),
|
||||
method: :delete,
|
||||
class: "block w-full py-2 px-3 space-x-2 text-red-600 hover:bg-red-50 flex items-center rounded-lg",
|
||||
data: {
|
||||
turbo_confirm: {
|
||||
title: t(".confirm_title"),
|
||||
body: t(".confirm_body_html"),
|
||||
accept: t(".confirm_accept", name: @account.name)
|
||||
}
|
||||
} do %>
|
||||
<%= lucide_icon("trash-2", class: "w-5 h-5 mr-2") %> Delete account
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative cursor-pointer" data-controller="menu">
|
||||
<button data-menu-target="button" class="flex hover:bg-gray-100 p-2 rounded">
|
||||
<%= lucide_icon("more-horizontal", class: "w-5 h-5 text-gray-500") %>
|
||||
</button>
|
||||
<div data-menu-target="content" class="absolute z-10 top-10 right-0 border border-alpha-black-25 bg-white rounded-lg shadow-xs hidden">
|
||||
<div class="w-48 px-3 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
||||
<%= button_to account_path(@account),
|
||||
method: :delete,
|
||||
class: "block w-full py-2 text-red-600 hover:text-red-800 flex items-center",
|
||||
data: {
|
||||
turbo_confirm: {
|
||||
title: t(".confirm_title"),
|
||||
body: t(".confirm_body_html"),
|
||||
accept: t(".confirm_accept", name: @account.name)
|
||||
}
|
||||
} do %>
|
||||
<%= lucide_icon("trash-2", class: "w-5 h-5 mr-2") %> Delete account
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<%= turbo_frame_tag "sync_message" do %>
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-xl font-medium text-gray-900">Accounts</h1>
|
||||
<%= link_to new_account_path, class: "flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-2 pr-3", data: { turbo_frame: "modal" } do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||
<span><%= t(".new") %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= render "header" %>
|
||||
|
||||
<% if @accounts.empty? %>
|
||||
<%= render "shared/no_account_empty_state" %>
|
||||
|
||||
17
app/views/accounts/transactions/_transaction.html.erb
Normal file
17
app/views/accounts/transactions/_transaction.html.erb
Normal file
@@ -0,0 +1,17 @@
|
||||
<%= turbo_frame_tag dom_id(transaction), class: "grid grid-cols-12 items-center text-gray-900 py-4 text-sm font-medium px-4" do %>
|
||||
<div class="col-span-4">
|
||||
<%= render "transactions/name", transaction: transaction %>
|
||||
</div>
|
||||
|
||||
<div class="col-span-3">
|
||||
<%= render "transactions/categories/badge", category: transaction.category %>
|
||||
</div>
|
||||
|
||||
<%= link_to transaction.account.name,
|
||||
account_path(transaction.account),
|
||||
class: ["col-span-3 hover:underline"] %>
|
||||
|
||||
<div class="col-span-2 ml-auto">
|
||||
<%= render "transactions/amount", transaction: transaction %>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -14,7 +14,8 @@
|
||||
<% @import.expected_fields.each do |field| %>
|
||||
<%= mappings.select field.key,
|
||||
options_for_select(@import.available_headers, @import.get_selected_header_for_field(field)),
|
||||
label: field.label %>
|
||||
label: field.label,
|
||||
include_blank: field.optional? ? t(".optional") : false %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
27
app/views/institutions/_form.html.erb
Normal file
27
app/views/institutions/_form.html.erb
Normal file
@@ -0,0 +1,27 @@
|
||||
<%= form_with model: institution, data: { turbo_frame: "_top", controller: "profile-image-preview" } do |f| %>
|
||||
|
||||
<div class="flex justify-center items-center py-4">
|
||||
<%= f.label :logo do %>
|
||||
<div class="relative cursor-pointer hover:opacity-80 w-16 h-16 rounded-full bg-gray-50">
|
||||
<% persisted_logo = institution_logo(institution) %>
|
||||
|
||||
<% if persisted_logo %>
|
||||
<%= image_tag persisted_logo, class: "absolute inset-0 rounded-full w-full h-full object-cover" %>
|
||||
<% end %>
|
||||
|
||||
<div data-profile-image-preview-target="imagePreview" class="absolute inset-0 h-full w-full flex items-center justify-center">
|
||||
<% unless persisted_logo %>
|
||||
<%= lucide_icon "image-plus", class: "w-5 h-5 text-gray-500 cursor-pointer", data: { profile_image_preview_target: "template" } %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= f.file_field :logo,
|
||||
accept: "image/png, image/jpeg",
|
||||
class: "hidden",
|
||||
data: { profile_image_preview_target: "fileField", action: "profile-image-preview#preview" } %>
|
||||
<%= f.text_field :name, label: t(".name") %>
|
||||
<%= f.submit %>
|
||||
<% end %>
|
||||
10
app/views/institutions/edit.html.erb
Normal file
10
app/views/institutions/edit.html.erb
Normal file
@@ -0,0 +1,10 @@
|
||||
<%= modal do %>
|
||||
<article class="mx-auto w-full p-4 space-y-4 min-w-[350px]">
|
||||
<header class="flex justify-between">
|
||||
<h2 class="font-medium text-xl"><%= t(".edit", institution: @institution.name) %></h2>
|
||||
<%= lucide_icon "x", class: "w-5 h-5 text-gray-500", data: { action: "click->modal#close" } %>
|
||||
</header>
|
||||
|
||||
<%= render "form", institution: @institution %>
|
||||
</article>
|
||||
<% end %>
|
||||
10
app/views/institutions/new.html.erb
Normal file
10
app/views/institutions/new.html.erb
Normal file
@@ -0,0 +1,10 @@
|
||||
<%= modal do %>
|
||||
<article class="mx-auto w-full p-4 space-y-4 min-w-[350px]">
|
||||
<header class="flex justify-between">
|
||||
<h2 class="font-medium text-xl"><%= t(".new_institution") %></h2>
|
||||
<%= lucide_icon "x", class: "w-5 h-5 text-gray-500", data: { action: "click->modal#close" } %>
|
||||
</header>
|
||||
|
||||
<%= render "form", institution: @institution %>
|
||||
</article>
|
||||
<% end %>
|
||||
@@ -2,11 +2,27 @@
|
||||
<%= render "settings/nav" %>
|
||||
<% end %>
|
||||
<div class="space-y-4">
|
||||
<h1 class="text-gray-900 text-xl font-medium mb-4">What's New</h1>
|
||||
<h1 class="text-gray-900 text-xl font-medium mb-4"><%= t(".title") %></h1>
|
||||
<div class="bg-white shadow-xs border border-alpha-black-25 rounded-xl p-4">
|
||||
<div class="flex justify-center items-center py-20">
|
||||
<p class="text-gray-500">Changelog coming soon...</p>
|
||||
</div>
|
||||
<% @releases_notes.each do |release_notes| %>
|
||||
<div class="flex justify-between gap-4 mb-12 last:mb-0">
|
||||
<div class="w-1/3">
|
||||
<div class="px-3 flex items-center gap-3">
|
||||
<div class="text-white shrink-0 w-9 h-9">
|
||||
<%= image_tag release_notes[:avatar], class: "rounded-full w-full h-full object-cover" %>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-gray-900 font-medium text-sm"><%= release_notes[:name] %></div>
|
||||
<div class="text-gray-500 text-sm"><%= release_notes[:published_at].strftime("%B %d, %Y") %></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-2/3 text-gray-500 text-sm prose prose--github-release-notes">
|
||||
<h2 class="mb-5 text-xl text-gray-900"><%= release_notes[:name] %></h2>
|
||||
<%= release_notes[:body].html_safe %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4">
|
||||
<%= previous_setting("Imports", imports_path) %>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="text-gray-900 flex items-center py-4 text-sm font-medium px-4">
|
||||
<div class="grow">
|
||||
<div class="grow max-w-72">
|
||||
<%= render "transactions/name", transaction: transaction %>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="flex items-center gap-1 mb-6">
|
||||
<%= link_to root_path, class: "flex items-center gap-1 text-gray-900 font-medium text-sm" do %>
|
||||
<%= link_to return_to_path(params), class: "flex items-center gap-1 text-gray-900 font-medium text-sm" do %>
|
||||
<%= lucide_icon "chevron-left", class: "w-5 h-5 text-gray-500" %>
|
||||
<span>Back</span>
|
||||
<% end %>
|
||||
|
||||
@@ -5,47 +5,52 @@
|
||||
<h1 class="text-gray-900 text-xl font-medium mb-4"><%= t(".page_title") %></h1>
|
||||
<%= settings_section title: t(".general_settings_title") do %>
|
||||
<%= form_with model: Setting.new, url: settings_hosting_path, method: :patch, local: true, html: { class: "space-y-6", data: { controller: "auto-submit-form", "auto-submit-form-trigger-event-value" => "blur" } } do |form| %>
|
||||
<div>
|
||||
<h2 class="font-medium mb-1"><%= t(".upgrades.title") %></h2>
|
||||
<p class="text-gray-500 text-sm mb-4"><%= t(".upgrades.description") %></p>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<%= form.radio_button :upgrades_mode, "manual", checked: Setting.upgrades_mode == "manual", data: { "auto-submit-form-target" => "auto", "autosubmit-trigger-event": "input" } %>
|
||||
<%= form.label :upgrades_mode_manual, t(".upgrades.manual.title"), class: "text-gray-900 text-sm" do %>
|
||||
<span class="font-medium"><%= t(".upgrades.manual.title") %></span>
|
||||
<br>
|
||||
<span class="text-gray-500">
|
||||
|
||||
<% if ENV["HOSTING_PLATFORM"] == "render" %>
|
||||
<div>
|
||||
<h2 class="font-medium mb-1"><%= t(".upgrades.title") %></h2>
|
||||
<p class="text-gray-500 text-sm mb-4"><%= t(".upgrades.description") %></p>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<%= form.radio_button :upgrades_mode, "manual", checked: Setting.upgrades_mode == "manual", data: { "auto-submit-form-target" => "auto", "autosubmit-trigger-event": "input" } %>
|
||||
<%= form.label :upgrades_mode_manual, t(".upgrades.manual.title"), class: "text-gray-900 text-sm" do %>
|
||||
<span class="font-medium"><%= t(".upgrades.manual.title") %></span>
|
||||
<br>
|
||||
<span class="text-gray-500">
|
||||
<%= t(".upgrades.manual.description") %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<%= form.radio_button :upgrades_mode, "release", checked: Setting.upgrades_mode == "auto" && Setting.upgrades_target == "release", data: { "auto-submit-form-target" => "auto", "autosubmit-trigger-event": "input" } %>
|
||||
<%= form.label :upgrades_mode_release, t(".upgrades.latest_release.title"), class: "text-gray-900 text-sm" do %>
|
||||
<span class="font-medium"><%= t(".upgrades.latest_release.title") %></span>
|
||||
<br>
|
||||
<span class="text-gray-500">
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<%= form.radio_button :upgrades_mode, "release", checked: Setting.upgrades_mode == "auto" && Setting.upgrades_target == "release", data: { "auto-submit-form-target" => "auto", "autosubmit-trigger-event": "input" } %>
|
||||
<%= form.label :upgrades_mode_release, t(".upgrades.latest_release.title"), class: "text-gray-900 text-sm" do %>
|
||||
<span class="font-medium"><%= t(".upgrades.latest_release.title") %></span>
|
||||
<br>
|
||||
<span class="text-gray-500">
|
||||
<%= t(".upgrades.latest_release.description") %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<%= form.radio_button :upgrades_mode, "commit", checked: Setting.upgrades_mode == "auto" && Setting.upgrades_target == "commit", data: { "auto-submit-form-target" => "auto", "autosubmit-trigger-event": "input" } %>
|
||||
<%= form.label :upgrades_mode_commit, t(".upgrades.latest_commit.title"), class: "text-gray-900 text-sm" do %>
|
||||
<span class="font-medium"><%= t(".upgrades.latest_commit.title") %></span>
|
||||
<br>
|
||||
<span class="text-gray-500">
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<%= form.radio_button :upgrades_mode, "commit", checked: Setting.upgrades_mode == "auto" && Setting.upgrades_target == "commit", data: { "auto-submit-form-target" => "auto", "autosubmit-trigger-event": "input" } %>
|
||||
<%= form.label :upgrades_mode_commit, t(".upgrades.latest_commit.title"), class: "text-gray-900 text-sm" do %>
|
||||
<span class="font-medium"><%= t(".upgrades.latest_commit.title") %></span>
|
||||
<br>
|
||||
<span class="text-gray-500">
|
||||
<%= t(".upgrades.latest_commit.description") %>
|
||||
</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="font-medium mb-1"><%= t(".provider_settings.title") %></h2>
|
||||
<p class="text-gray-500 text-sm mb-4"><%= t(".render_deploy_hook_description") %></p>
|
||||
<%= form.url_field :render_deploy_hook, label: t(".render_deploy_hook_label"), placeholder: t(".render_deploy_hook_placeholder"), value: Setting.render_deploy_hook, data: { "auto-submit-form-target" => "auto" } %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-medium mb-1"><%= t(".provider_settings.title") %></h2>
|
||||
<p class="text-gray-500 text-sm mb-4"><%= t(".render_deploy_hook_description") %></p>
|
||||
<%= form.url_field :render_deploy_hook, label: t(".render_deploy_hook_label"), placeholder: t(".render_deploy_hook_placeholder"), value: Setting.render_deploy_hook, data: { "auto-submit-form-target" => "auto" } %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div>
|
||||
<h2 class="font-medium mb-1"><%= t(".smtp_settings.title") %></h2>
|
||||
<p class="text-gray-500 text-sm mb-4"><%= t(".smtp_settings.description") %></p>
|
||||
@@ -69,13 +74,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<%= link_to t(".smtp_settings.send_test_email_button"), send_test_email_settings_hosting_path, data: { turbo_method: :post }, class:"bg-gray-50 text-gray-900 text-sm font-medium rounded-lg px-3 py-2" %>
|
||||
<%= link_to t(".smtp_settings.send_test_email_button"), send_test_email_settings_hosting_path, data: { turbo_method: :post }, class: "bg-gray-50 text-gray-900 text-sm font-medium rounded-lg px-3 py-2" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<div class="flex justify-between gap-4">
|
||||
<%= previous_setting("Billing", settings_billing_path) %>
|
||||
<%= next_setting("Accounts", accounts_path) %>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<%= form_with model: Current.user, url: settings_profile_path, html: {data: { controller: "profile-image-preview" }} do |form| %>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="relative flex justify-center items-center bg-gray-50 w-24 h-24 rounded-full border border-alpha-black-25">
|
||||
<div data-profile-image-preview-target="imagePreview">
|
||||
<div data-profile-image-preview-target="imagePreview" class="h-full w-full flex justify-center items-center">
|
||||
<% profile_image_attached = Current.user.profile_image.attached? %>
|
||||
<% if profile_image_attached %>
|
||||
<div class="h-24 w-24">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<%# locals: (content:, classes:) -%>
|
||||
<%= turbo_frame_tag "modal" do %>
|
||||
<dialog class="bg-white border border-alpha-black-25 rounded-2xl max-h-[648px] max-w-[580px] w-min-content shadow-xs h-fit <%= classes %>" data-controller="modal" data-action="click->modal#clickOutside">
|
||||
<dialog class="bg-white border border-alpha-black-25 rounded-2xl max-w-[580px] w-min-content shadow-xs h-fit <%= classes %>" data-controller="modal" data-action="click->modal#clickOutside">
|
||||
<div class="flex flex-col">
|
||||
<%= content %>
|
||||
</div>
|
||||
|
||||
17
app/views/transactions/_date_group.html.erb
Normal file
17
app/views/transactions/_date_group.html.erb
Normal file
@@ -0,0 +1,17 @@
|
||||
<%# locals: (date:, transactions:) %>
|
||||
<div class="bg-gray-25 rounded-xl p-1 w-full" data-bulk-select-target="group">
|
||||
<div class="py-2 px-4 flex items-center justify-between font-medium text-xs text-gray-500">
|
||||
<div class="flex pl-0.5 items-center gap-4">
|
||||
<%= check_box_tag "#{date}_transactions_selection",
|
||||
class: "maybe-checkbox maybe-checkbox--light",
|
||||
id: "selection_transaction_#{date}",
|
||||
data: { action: "bulk-select#toggleGroupSelection" } %>
|
||||
<%= tag.span "#{date.strftime('%b %d, %Y')} · #{transactions.size}" %>
|
||||
</div>
|
||||
|
||||
<%= tag.span format_money(-transactions.sum(&:amount_money)) %>
|
||||
</div>
|
||||
<div class="bg-white shadow-xs rounded-md border border-alpha-black-25 divide-y divide-alpha-black-50">
|
||||
<%= render transactions %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,9 +1,9 @@
|
||||
<%= content_tag :div, class: ["flex items-center gap-2"] do %>
|
||||
<div class="w-8 h-8 flex items-center justify-center rounded-full bg-gray-600/5 text-gray-600">
|
||||
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-gray-600/5 text-gray-600">
|
||||
<%= transaction.name[0].upcase %>
|
||||
</div>
|
||||
|
||||
<div class="text-gray-900 truncate">
|
||||
<div class="truncate text-gray-900">
|
||||
<% if transaction.new_record? %>
|
||||
<%= content_tag :p, transaction.name %>
|
||||
<% else %>
|
||||
|
||||
24
app/views/transactions/_selection_bar.html.erb
Normal file
24
app/views/transactions/_selection_bar.html.erb
Normal file
@@ -0,0 +1,24 @@
|
||||
<div class="fixed bottom-6 z-10 flex items-center justify-between rounded-xl bg-gray-900 px-4 text-sm text-white w-[420px] py-1.5">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= check_box_tag "transaction_selection", 1, true, class: "maybe-checkbox maybe-checkbox--dark", data: { action: "bulk-select#deselectAll" } %>
|
||||
|
||||
<p data-bulk-select-target="selectionBarText"></p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1 text-gray-500">
|
||||
<%= turbo_frame_tag "bulk_transaction_edit_drawer" %>
|
||||
|
||||
<%= link_to bulk_edit_transactions_path,
|
||||
class: "p-1.5 group hover:bg-gray-700 flex items-center justify-center rounded-md",
|
||||
title: "Edit",
|
||||
data: { turbo_frame: "bulk_transaction_edit_drawer" } do %>
|
||||
<%= lucide_icon "pencil-line", class: "w-5 group-hover:text-white" %>
|
||||
<% end %>
|
||||
|
||||
<%= form_with url: bulk_delete_transactions_path, builder: ActionView::Helpers::FormBuilder, data: { turbo_confirm: true } do %>
|
||||
<button type="button" data-bulk-select-scope-param="bulk_delete" data-action="bulk-select#submitBulkRequest" class="p-1.5 group hover:bg-gray-700 flex items-center justify-center rounded-md" title="Delete">
|
||||
<%= lucide_icon "trash-2", class: "w-5 group-hover:text-white" %>
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,6 +1,12 @@
|
||||
<%= turbo_frame_tag dom_id(transaction), class: "grid grid-cols-12 items-center text-gray-900 py-4 text-sm font-medium px-4" do %>
|
||||
<div class="col-span-4">
|
||||
<%= render "transactions/name", transaction: transaction %>
|
||||
<div class="col-span-4 flex items-center gap-4">
|
||||
<%= check_box_tag dom_id(transaction, "selection"),
|
||||
class: "maybe-checkbox maybe-checkbox--light",
|
||||
data: { id: transaction.id, "bulk-select-target": "row", action: "bulk-select#toggleRowSelection" } %>
|
||||
|
||||
<div class="max-w-full pr-10">
|
||||
<%= render "transactions/name", transaction: transaction %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-3">
|
||||
@@ -9,6 +15,7 @@
|
||||
|
||||
<%= link_to transaction.account.name,
|
||||
account_path(transaction.account),
|
||||
data: { turbo_frame: "_top" },
|
||||
class: ["col-span-3 hover:underline"] %>
|
||||
|
||||
<div class="col-span-2 ml-auto">
|
||||
|
||||
80
app/views/transactions/bulk_edit.html.erb
Normal file
80
app/views/transactions/bulk_edit.html.erb
Normal file
@@ -0,0 +1,80 @@
|
||||
<%= turbo_frame_tag "bulk_transaction_edit_drawer" do %>
|
||||
<dialog data-controller="modal"
|
||||
data-action="click->modal#clickOutside"
|
||||
class="bg-white border border-alpha-black-25 rounded-2xl max-h-[calc(100vh-32px)] max-w-[480px] w-full shadow-xs h-full mt-4 mr-4">
|
||||
<%= form_with url: bulk_update_transactions_path, scope: "bulk_update", html: { class: "h-full" }, data: { turbo_frame: "_top" } do |form| %>
|
||||
<div class="flex h-full flex-col justify-between p-4">
|
||||
<div>
|
||||
<div class="flex h-9 items-center justify-end">
|
||||
<div data-action="click->modal#close" class="cursor-pointer">
|
||||
<%= lucide_icon("x", class: "w-5 h-5 shrink-0") %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col overflow-scroll">
|
||||
<div>
|
||||
<header class="mb-4 space-y-1">
|
||||
<h3 class="text-2xl font-medium" data-bulk-select-target="bulkEditDrawerTitle">
|
||||
Edit transactions
|
||||
</h3>
|
||||
</header>
|
||||
|
||||
<div class="space-y-2">
|
||||
<details class="group space-y-2" open>
|
||||
<summary class="flex list-none items-center justify-between rounded-xl px-3 py-2 text-xs font-medium uppercase text-gray-500 bg-gray-25 focus-visible:outline-none">
|
||||
<h4><%= t(".overview") %></h4>
|
||||
<%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-gray-500 w-5" %>
|
||||
</summary>
|
||||
|
||||
<div class="pb-6 space-y-2">
|
||||
<%= form.date_field :date, label: t(".date"), max: Date.current %>
|
||||
<%= form.collection_select :category_id, Current.family.transaction_categories, :id, :name, { prompt: t(".select_category"), label: t(".category"), class: "text-gray-400" } %>
|
||||
<%= form.collection_select :merchant_id, Current.family.transaction_merchants, :id, :name, { prompt: t(".select_merchant"), label: t(".merchant"), class: "text-gray-400" } %>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="group space-y-2" open>
|
||||
<summary class="flex list-none items-center justify-between rounded-xl px-3 py-2 text-xs font-medium uppercase text-gray-500 bg-gray-25 focus-visible:outline-none">
|
||||
<h4><%= t(".additional") %></h4>
|
||||
<%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-gray-500 w-5" %>
|
||||
</summary>
|
||||
|
||||
<div>
|
||||
<%= form.text_area :notes, label: t(".note"), placeholder: t(".note_placeholder"), rows: 5 %>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="group space-y-2" open>
|
||||
<summary class="flex list-none items-center justify-between rounded-xl px-3 py-2 text-xs font-medium uppercase text-gray-500 bg-gray-25 focus-visible:outline-none">
|
||||
<h4><%= t(".settings") %></h4>
|
||||
<%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-gray-500 w-5" %>
|
||||
</summary>
|
||||
|
||||
<div class="flex cursor-pointer items-center justify-between gap-4 p-3 pb-6">
|
||||
<div class="text-sm space-y-1">
|
||||
<h4 class="text-gray-900"><%= t(".exclude_title") %></h4>
|
||||
<p class="text-gray-500"><%= t(".exclude_subtitle") %></p>
|
||||
</div>
|
||||
|
||||
<div class="relative inline-block select-none">
|
||||
<%= form.check_box :excluded, class: "sr-only peer" %>
|
||||
<label for="bulk_update_excluded" class="maybe-switch"></label>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end items-center gap-2">
|
||||
<%= link_to t(".cancel"), transactions_path, class: "text-sm font-medium text-gray-900 px-3 py-2" %>
|
||||
|
||||
<%= tag.button t(".save"),
|
||||
type: "button",
|
||||
data: { "bulk-select-scope-param": "bulk_update", action: "bulk-select#submitBulkRequest" },
|
||||
class: "px-3 py-2 bg-gray-900 text-white text-sm font-medium rounded-lg" %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</dialog>
|
||||
<% end %>
|
||||
@@ -1,23 +1,32 @@
|
||||
<div class="space-y-4">
|
||||
|
||||
<%= render "header" %>
|
||||
|
||||
<%= render partial: "transactions/summary", locals: { totals: @totals } %>
|
||||
|
||||
<div id="transactions" class="bg-white rounded-xl border border-alpha-black-25 shadow-xs p-4 space-y-4">
|
||||
<div id="transactions" data-controller="bulk-select" data-bulk-select-resource-value="<%= t(".transaction") %>" class="bg-white rounded-xl border border-alpha-black-25 shadow-xs p-4 space-y-4">
|
||||
|
||||
<%= render partial: "transactions/searches/search", locals: { transactions: @transactions } %>
|
||||
|
||||
<% if @transactions.present? %>
|
||||
<div hidden id="transaction-selection-bar" data-bulk-select-target="selectionBar">
|
||||
<%= render "selection_bar" %>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-12 bg-gray-25 rounded-xl px-5 py-3 text-xs uppercase font-medium text-gray-500 items-center mb-4">
|
||||
<p class="col-span-4">transaction</p>
|
||||
<div class="pl-0.5 col-span-4 flex items-center gap-4">
|
||||
<%= check_box_tag "selection_transaction",
|
||||
class: "maybe-checkbox maybe-checkbox--light",
|
||||
data: { action: "bulk-select#togglePageSelection" } %>
|
||||
<p class="col-span-4">transaction</p>
|
||||
</div>
|
||||
|
||||
<p class="col-span-3 pl-4">category</p>
|
||||
<p class="col-span-3">account</p>
|
||||
<p class="col-span-2 justify-self-end">amount</p>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<% @transactions.group_by(&:date).each do |date, transactions| %>
|
||||
<%= transactions_group(date, transactions) %>
|
||||
<%= render partial: "date_group", locals: { date:, transactions: } %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
|
||||
@@ -1,81 +1,105 @@
|
||||
<%= drawer do %>
|
||||
<h3 class="font-medium mb-1">
|
||||
<span class="text-2xl"><%= format_money @transaction.amount_money %></span>
|
||||
<span class="text-lg text-gray-500"><%= @transaction.currency %></span>
|
||||
</h3>
|
||||
<span class="text-sm text-gray-500"><%= @transaction.date.strftime("%A %d %B") %></span>
|
||||
<div>
|
||||
<header class="mb-4 space-y-1">
|
||||
<h3 class="font-medium">
|
||||
<span class="text-2xl"><%= format_money @transaction.amount_money %></span>
|
||||
<span class="text-lg text-gray-500"><%= @transaction.currency %></span>
|
||||
</h3>
|
||||
|
||||
<details class="group" open>
|
||||
<summary class="list-none bg-gray-25 rounded-xl py-1 mt-4 group-open:mb-2">
|
||||
<div class="py-2 px-[11px] flex items-center justify-between font-medium text-xs text-gray-500">
|
||||
Overview
|
||||
<%= lucide_icon("chevron-down", class: "hidden group-open:block text-gray-500 w-5 h-5") %>
|
||||
<%= lucide_icon("chevron-right", class: "group-open:hidden text-gray-500 w-5 h-5") %>
|
||||
</div>
|
||||
</summary>
|
||||
<%= form_with model: @transaction, html: { data: { controller: "auto-submit-form" } } do |f| %>
|
||||
<div class="space-y-2">
|
||||
<%= f.date_field :date, label: "Date", max: Date.today, "data-auto-submit-form-target": "auto" %>
|
||||
<%= f.collection_select :category_id, Current.family.transaction_categories, :id, :name, { prompt: "Select a category", label: "Category", class: "text-gray-400" }, "data-auto-submit-form-target": "auto" %>
|
||||
<%= f.collection_select :account_id, Current.family.accounts, :id, :name, { prompt: "Select an Account", label: "Account", class: "text-gray-500" }, { class: "form-field__input cursor-not-allowed text-gray-400", disabled: "disabled" } %>
|
||||
</div>
|
||||
<% end %>
|
||||
</details>
|
||||
<details class="group" open>
|
||||
<summary class="list-none bg-gray-25 rounded-xl py-1 mt-6 group-open:mb-2">
|
||||
<div class="py-2 px-[11px] flex items-center justify-between font-medium text-xs text-gray-500">
|
||||
Description
|
||||
<%= lucide_icon("chevron-down", class: "hidden group-open:block text-gray-500 w-5 h-5") %>
|
||||
<%= lucide_icon("chevron-right", class: "group-open:hidden text-gray-500 w-5 h-5") %>
|
||||
</div>
|
||||
</summary>
|
||||
<%= form_with model: @transaction, html: { data: { controller: "auto-submit-form" } } do |f| %>
|
||||
<%= f.text_field :name, label: "Name", "data-auto-submit-form-target": "auto" %>
|
||||
<% end %>
|
||||
</details>
|
||||
<details class="group" open>
|
||||
<summary class="list-none bg-gray-25 rounded-xl py-1 mt-6 group-open:mb-2">
|
||||
<div class="py-2 px-[11px] flex items-center justify-between font-medium text-xs text-gray-500">
|
||||
<span>Settings</span>
|
||||
<%= lucide_icon("chevron-down", class: "hidden group-open:block text-gray-500 w-5 h-5") %>
|
||||
<%= lucide_icon("chevron-right", class: "group-open:hidden text-gray-500 w-5 h-5") %>
|
||||
</div>
|
||||
</summary>
|
||||
<%= form_with model: @transaction, html: { data: { controller: "auto-submit-form" } } do |f| %>
|
||||
<label class="flex items-center cursor-pointer justify-between mx-3">
|
||||
<%= f.check_box :excluded, class: "sr-only peer", "data-auto-submit-form-target": "auto" %>
|
||||
<div class="flex flex-col justify-center text-sm w-[340px] py-3">
|
||||
<span class="text-gray-900 mb-1">Exclude from analytics</span>
|
||||
<span class="text-gray-500">This excludes the transaction from any in-app features or analytics.</span>
|
||||
<span class="text-sm text-gray-500"><%= @transaction.date.strftime("%A %d %B") %></span>
|
||||
</header>
|
||||
|
||||
<div class="space-y-2">
|
||||
<details class="group space-y-2" open>
|
||||
<summary class="flex list-none items-center justify-between rounded-xl px-3 py-2 text-xs font-medium uppercase text-gray-500 bg-gray-25 focus-visible:outline-none">
|
||||
<h4><%= t(".overview") %></h4>
|
||||
<%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-gray-500 w-5" %>
|
||||
</summary>
|
||||
|
||||
<div class="pb-6">
|
||||
<%= form_with model: @transaction, html: { data: { controller: "auto-submit-form" } } do |f| %>
|
||||
<div class="space-y-2">
|
||||
<%= f.date_field :date, label: "Date", max: Date.today, "data-auto-submit-form-target": "auto" %>
|
||||
<%= f.collection_select :category_id, Current.family.transaction_categories, :id, :name, { prompt: "Select a category", label: "Category", class: "text-gray-400" }, "data-auto-submit-form-target": "auto" %>
|
||||
<%= f.collection_select :account_id, Current.family.accounts, :id, :name, { prompt: "Select an Account", label: "Account", class: "text-gray-500" }, { class: "form-field__input cursor-not-allowed text-gray-400", disabled: "disabled" } %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="relative w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-100 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
|
||||
</label>
|
||||
<% end %>
|
||||
</details>
|
||||
<details class="group" open>
|
||||
<summary class="list-none bg-gray-25 rounded-xl py-1 mt-6 mb-2">
|
||||
<div class="py-2 px-[11px] flex items-center justify-between font-medium text-xs text-gray-500">
|
||||
<span>Additional</span>
|
||||
<%= lucide_icon("chevron-down", class: "hidden group-open:block text-gray-500 w-5 h-5") %>
|
||||
<%= lucide_icon("chevron-right", class: "group-open:hidden text-gray-500 w-5 h-5") %>
|
||||
</div>
|
||||
</summary>
|
||||
</details>
|
||||
|
||||
<div class="mb-2">
|
||||
<%= form_with model: @transaction, html: { data: { controller: "auto-submit-form" } } do |f| %>
|
||||
<%= f.select :tag_ids,
|
||||
options_for_select(Current.family.tags.alphabetically.pluck(:name, :id), @transaction.tag_ids),
|
||||
{
|
||||
multiple: true,
|
||||
label: t(".select_tags"),
|
||||
class: "placeholder:text-gray-500"
|
||||
},
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
<% end %>
|
||||
<details class="group space-y-2" open>
|
||||
<summary class="flex list-none items-center justify-between rounded-xl px-3 py-2 text-xs font-medium uppercase text-gray-500 bg-gray-25 focus-visible:outline-none">
|
||||
<h4><%= t(".description") %></h4>
|
||||
<%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-gray-500 w-5" %>
|
||||
</summary>
|
||||
|
||||
<div class="pb-6">
|
||||
<%= form_with model: @transaction, html: { data: { controller: "auto-submit-form" } } do |f| %>
|
||||
<%= f.text_field :name, label: "Name", "data-auto-submit-form-target": "auto" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="group space-y-2" open>
|
||||
<summary class="flex list-none items-center justify-between rounded-xl px-3 py-2 text-xs font-medium uppercase text-gray-500 bg-gray-25 focus-visible:outline-none">
|
||||
<h4><%= t(".additional") %></h4>
|
||||
<%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-gray-500 w-5" %>
|
||||
</summary>
|
||||
|
||||
<div class="pb-6 space-y-2">
|
||||
<%= form_with model: @transaction, html: { data: { controller: "auto-submit-form" } } do |f| %>
|
||||
<%= f.select :tag_ids,
|
||||
options_for_select(Current.family.tags.alphabetically.pluck(:name, :id), @transaction.tag_ids),
|
||||
{
|
||||
multiple: true,
|
||||
label: t(".select_tags"),
|
||||
class: "placeholder:text-gray-500"
|
||||
},
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
<% end %>
|
||||
|
||||
<%= form_with model: @transaction, html: { data: { controller: "auto-submit-form" } } do |f| %>
|
||||
<%= f.text_area :notes, label: "Notes", placeholder: "Enter a note", "data-auto-submit-form-target": "auto" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="group space-y-2" open>
|
||||
<summary class="flex list-none items-center justify-between rounded-xl px-3 py-2 text-xs font-medium uppercase text-gray-500 bg-gray-25 focus-visible:outline-none">
|
||||
<h4><%= t(".settings") %></h4>
|
||||
<%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-gray-500 w-5" %>
|
||||
</summary>
|
||||
|
||||
<div class="pb-6">
|
||||
|
||||
<%= form_with model: @transaction, html: { class: "p-3", data: { controller: "auto-submit-form" } } do |f| %>
|
||||
<div class="flex cursor-pointer items-center justify-between">
|
||||
<div class="text-sm space-y-1">
|
||||
<h4 class="text-gray-900"><%= t(".exclude_title") %></h4>
|
||||
<p class="text-gray-500"><%= t(".exclude_subtitle") %></p>
|
||||
</div>
|
||||
|
||||
<div class="relative inline-block select-none">
|
||||
<%= f.check_box :excluded, class: "sr-only peer", "data-auto-submit-form-target": "auto" %>
|
||||
<label for="transaction_excluded" class="maybe-switch"></label>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="flex items-center justify-between gap-2 p-3">
|
||||
<div class="text-sm space-y-1">
|
||||
<h4 class="text-gray-900"><%= t(".delete_title") %></h4>
|
||||
<p class="text-gray-500"><%= t(".delete_subtitle") %></p>
|
||||
</div>
|
||||
|
||||
<%= button_to t(".delete"),
|
||||
transaction_path(@transaction),
|
||||
method: :delete,
|
||||
class: "rounded-lg px-3 py-2 text-red-500 text-sm font-medium border border-alpha-black-200",
|
||||
data: { turbo_confirm: true, turbo_frame: "_top" } %>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<%= form_with model: @transaction, html: { data: { controller: "auto-submit-form" } } do |f| %>
|
||||
<%= f.text_area :notes, label: "Notes", placeholder: "Enter a note", "data-auto-submit-form-target": "auto" %>
|
||||
<% end %>
|
||||
</details>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -10,7 +10,7 @@ module Maybe
|
||||
|
||||
private
|
||||
def semver
|
||||
"0.1.0-alpha.4"
|
||||
"0.1.0-alpha.6"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,21 +12,49 @@ en:
|
||||
success: New account created successfully
|
||||
destroy:
|
||||
success: Account deleted successfully
|
||||
index:
|
||||
edit:
|
||||
edit: Edit %{account}
|
||||
institution: Financial institution
|
||||
ungrouped: "(none)"
|
||||
empty:
|
||||
empty_message: Add an account either via connection, importing or entering manually.
|
||||
new_account: New account
|
||||
no_accounts: No accounts yet
|
||||
header:
|
||||
accounts: Accounts
|
||||
manage: Manage accounts
|
||||
new: New account
|
||||
index:
|
||||
accounts: Accounts
|
||||
add_institution: Add institution
|
||||
new_account: New account
|
||||
institution_accounts:
|
||||
add_account_to_institution: Add new account
|
||||
confirm_accept: Delete institution
|
||||
confirm_body: Don't worry, none of the accounts within this institution will
|
||||
be affected by this deletion. Accounts will be ungrouped and all historical
|
||||
data will remain intact.
|
||||
confirm_title: Delete financial institution?
|
||||
delete: Delete institution
|
||||
edit: Edit institution
|
||||
new_account: Add account
|
||||
institutionless_accounts:
|
||||
other_accounts: Other accounts
|
||||
new:
|
||||
balance:
|
||||
label: Balance
|
||||
balance: Current balance
|
||||
currency:
|
||||
all_others: All Others
|
||||
popular: Popular
|
||||
institution: Financial institution
|
||||
name:
|
||||
label: Account name
|
||||
placeholder: Example account name
|
||||
optional_start_balance_message: Add a start balance for this account
|
||||
select_accountable_type: What would you like to add?
|
||||
start_date:
|
||||
label: Start date
|
||||
start_balance: Start balance (optional)
|
||||
start_date: Start date (optional)
|
||||
title: Add an account
|
||||
ungrouped: "(none)"
|
||||
show:
|
||||
confirm_accept: Delete "%{name}"
|
||||
confirm_body_html: "<p>By deleting this account, you will erase its value history,
|
||||
@@ -35,13 +63,14 @@ en:
|
||||
/> <p>After deletion, there is no way you'll be able to restore the account
|
||||
information because you'll need to add it as a new account.</p>"
|
||||
confirm_title: Delete account?
|
||||
edit: Edit
|
||||
import: Import transactions
|
||||
sync_message_missing_rates: Since exchange rates haven't been synced, balance
|
||||
graphs may not reflect accurate values.
|
||||
sync_message_unknown_error: An error has occurred during the sync.
|
||||
summary:
|
||||
new: New account
|
||||
sync:
|
||||
cannot_sync: Account cannot be synced at the moment
|
||||
success: Account sync started
|
||||
update:
|
||||
success: Account updated successfully
|
||||
success: Account updated
|
||||
@@ -18,6 +18,7 @@ en:
|
||||
confirm_title: Are you sure?
|
||||
invalid_csv: Please load a CSV first
|
||||
next: Next
|
||||
optional: "(optional) No column selected"
|
||||
confirm:
|
||||
confirm_description: Preview your transactions below and check to see if there
|
||||
are any changes that are required.
|
||||
|
||||
15
config/locales/views/institutions/en.yml
Normal file
15
config/locales/views/institutions/en.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
en:
|
||||
institutions:
|
||||
create:
|
||||
success: Institution created
|
||||
destroy:
|
||||
success: Institution deleted
|
||||
edit:
|
||||
edit: Edit %{institution}
|
||||
form:
|
||||
name: Financial institution name
|
||||
new:
|
||||
new_institution: New financial institution
|
||||
update:
|
||||
success: Institution updated
|
||||
@@ -1,6 +1,8 @@
|
||||
---
|
||||
en:
|
||||
pages:
|
||||
changelog:
|
||||
title: What's new
|
||||
dashboard:
|
||||
allocation_chart:
|
||||
assets: Assets
|
||||
|
||||
@@ -1,6 +1,27 @@
|
||||
---
|
||||
en:
|
||||
transactions:
|
||||
bulk_delete:
|
||||
success: "%{count} transactions deleted"
|
||||
bulk_edit:
|
||||
additional: Additional
|
||||
cancel: Cancel
|
||||
category: Category
|
||||
date: Date
|
||||
exclude_subtitle: This excludes the transaction from any in-app features or
|
||||
analytics.
|
||||
exclude_title: Exclude transaction
|
||||
merchant: Merchant
|
||||
note: Notes
|
||||
note_placeholder: Enter a note that will be applied to selected transactions
|
||||
overview: Overview
|
||||
save: Save
|
||||
select_category: Select a category
|
||||
select_merchant: Select a merchant
|
||||
settings: Settings
|
||||
bulk_update:
|
||||
failure: Could not update transactions
|
||||
success: "%{count} transactions updated"
|
||||
categories:
|
||||
create:
|
||||
success: New transaction category created successfully
|
||||
@@ -66,6 +87,8 @@ en:
|
||||
edit_categories: Edit categories
|
||||
edit_imports: Edit imports
|
||||
import: Import
|
||||
index:
|
||||
transaction: transaction
|
||||
merchants:
|
||||
create:
|
||||
success: New merchant created successfully
|
||||
@@ -94,6 +117,17 @@ en:
|
||||
update:
|
||||
success: Merchant updated successfully
|
||||
show:
|
||||
additional: Additional
|
||||
delete: Delete
|
||||
delete_subtitle: This permanently deletes the transaction, affects your historical
|
||||
balances, and cannot be undone.
|
||||
delete_title: Delete transaction
|
||||
description: Description
|
||||
exclude_subtitle: This excludes the transaction from any in-app features or
|
||||
analytics.
|
||||
exclude_title: Exclude transaction
|
||||
overview: Overview
|
||||
select_tags: Select one or more tags
|
||||
settings: Settings
|
||||
update:
|
||||
success: Transaction updated successfully
|
||||
|
||||
@@ -43,7 +43,9 @@ Rails.application.routes.draw do
|
||||
|
||||
resources :transactions do
|
||||
collection do
|
||||
match "search" => "transactions#search", via: %i[ get post ]
|
||||
post "bulk_delete"
|
||||
get "bulk_edit"
|
||||
post "bulk_update"
|
||||
|
||||
scope module: :transactions, as: :transaction do
|
||||
resources :rows, only: %i[ show update ]
|
||||
@@ -69,6 +71,8 @@ Rails.application.routes.draw do
|
||||
resources :valuations
|
||||
end
|
||||
|
||||
resources :institutions, except: %i[ index show ]
|
||||
|
||||
# For managing self-hosted upgrades and release notifications
|
||||
resources :upgrades, only: [] do
|
||||
member do
|
||||
|
||||
@@ -52,6 +52,49 @@ module.exports = {
|
||||
to: { "stroke-dashoffset": 0 },
|
||||
},
|
||||
},
|
||||
typography: {
|
||||
DEFAULT: {
|
||||
css: {
|
||||
maxWidth: "none",
|
||||
a: {
|
||||
color: "inherit",
|
||||
textDecoration: "underline",
|
||||
},
|
||||
h2: {
|
||||
fontSize: "1.125rem",
|
||||
fontWeight: "inherit",
|
||||
lineHeight: "1.75rem",
|
||||
marginBottom: "0.625rem",
|
||||
marginTop: "0.875rem",
|
||||
},
|
||||
p: {
|
||||
marginBottom: "0.625rem",
|
||||
marginTop: "0.875rem",
|
||||
},
|
||||
strong: {
|
||||
color: "inherit",
|
||||
},
|
||||
li: {
|
||||
margin: 0,
|
||||
},
|
||||
details: {
|
||||
borderRadius: "12px",
|
||||
marginBottom: "0.875rem",
|
||||
marginTop: "0.875rem",
|
||||
},
|
||||
summary: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
columnGap: "0.25rem",
|
||||
},
|
||||
video: {
|
||||
margin: 0,
|
||||
borderBottomLeftRadius: "12px",
|
||||
borderBottomRightRadius: "12px",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
|
||||
11
db/migrate/20240612164751_create_institutions.rb
Normal file
11
db/migrate/20240612164751_create_institutions.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
class CreateInstitutions < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
create_table :institutions, id: :uuid do |t|
|
||||
t.string :name, null: false
|
||||
t.string :logo_url
|
||||
t.references :family, null: false, foreign_key: true, type: :uuid
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
||||
5
db/migrate/20240612164944_add_institution_to_accounts.rb
Normal file
5
db/migrate/20240612164944_add_institution_to_accounts.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class AddInstitutionToAccounts < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
add_reference :accounts, :institution, foreign_key: true, type: :uuid
|
||||
end
|
||||
end
|
||||
15
db/schema.rb
generated
15
db/schema.rb
generated
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.2].define(version: 2024_05_24_203959) do
|
||||
ActiveRecord::Schema[7.2].define(version: 2024_06_12_164944) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pgcrypto"
|
||||
enable_extension "plpgsql"
|
||||
@@ -93,8 +93,10 @@ ActiveRecord::Schema[7.2].define(version: 2024_05_24_203959) do
|
||||
t.jsonb "sync_warnings", default: [], null: false
|
||||
t.jsonb "sync_errors", default: [], null: false
|
||||
t.date "last_sync_date"
|
||||
t.uuid "institution_id"
|
||||
t.index ["accountable_type"], name: "index_accounts_on_accountable_type"
|
||||
t.index ["family_id"], name: "index_accounts_on_family_id"
|
||||
t.index ["institution_id"], name: "index_accounts_on_institution_id"
|
||||
end
|
||||
|
||||
create_table "active_storage_attachments", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
@@ -234,6 +236,15 @@ ActiveRecord::Schema[7.2].define(version: 2024_05_24_203959) do
|
||||
t.index ["account_id"], name: "index_imports_on_account_id"
|
||||
end
|
||||
|
||||
create_table "institutions", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.string "logo_url"
|
||||
t.uuid "family_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["family_id"], name: "index_institutions_on_family_id"
|
||||
end
|
||||
|
||||
create_table "invite_codes", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.string "token", null: false
|
||||
t.datetime "created_at", null: false
|
||||
@@ -334,9 +345,11 @@ ActiveRecord::Schema[7.2].define(version: 2024_05_24_203959) do
|
||||
|
||||
add_foreign_key "account_balances", "accounts", on_delete: :cascade
|
||||
add_foreign_key "accounts", "families"
|
||||
add_foreign_key "accounts", "institutions"
|
||||
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
||||
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
|
||||
add_foreign_key "imports", "accounts"
|
||||
add_foreign_key "institutions", "families"
|
||||
add_foreign_key "taggings", "tags"
|
||||
add_foreign_key "tags", "families"
|
||||
add_foreign_key "transaction_categories", "families"
|
||||
|
||||
@@ -1,21 +1,55 @@
|
||||
# ===========================================================================
|
||||
# Example Docker Compose file
|
||||
# ===========================================================================
|
||||
#
|
||||
# Purpose:
|
||||
# --------
|
||||
#
|
||||
# This file is an example Docker Compose configuration for self hosting
|
||||
# Maybe on your local machine or on a cloud VPS.
|
||||
#
|
||||
# The configuration below is a "standard" setup, but may require modification
|
||||
# for your specific environment.
|
||||
#
|
||||
# Setup:
|
||||
# ------
|
||||
#
|
||||
# To run this, you should read the setup guide:
|
||||
#
|
||||
# https://github.com/maybe-finance/maybe/blob/main/docs/hosting/docker.md
|
||||
#
|
||||
# Troubleshooting:
|
||||
# ----------------
|
||||
#
|
||||
# If you run into problems, you should open a Discussion here:
|
||||
#
|
||||
# https://github.com/maybe-finance/maybe/discussions/categories/general
|
||||
#
|
||||
|
||||
services:
|
||||
|
||||
app:
|
||||
image: ghcr.io/maybe-finance/maybe:latest
|
||||
|
||||
volumes:
|
||||
- ./storage:/rails/storage
|
||||
|
||||
ports:
|
||||
- 127.0.0.1:3000:3000
|
||||
- 3000:3000
|
||||
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
environment:
|
||||
SELF_HOSTING_ENABLED: true
|
||||
DB_HOST: postgres
|
||||
RAILS_FORCE_SSL: false
|
||||
RAILS_ASSUME_SSL: false
|
||||
POSTGRES_USER: postgres
|
||||
SELF_HOSTING_ENABLED: "true"
|
||||
RAILS_FORCE_SSL: "false"
|
||||
RAILS_ASSUME_SSL: "false"
|
||||
GOOD_JOB_EXECUTION_MODE: async
|
||||
SECRET_KEY_BASE: ${SECRET_KEY_BASE:?}
|
||||
DB_HOST: postgres
|
||||
POSTGRES_DB: ${POSTGRES_DB:-maybe_production}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-maybe_user}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?}
|
||||
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
@@ -26,11 +60,11 @@ services:
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-maybe_user}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-maybe_production}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?}
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "pg_isready -U $$POSTGRES_USER" ]
|
||||
test: [ "CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB" ]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
@@ -1,102 +1,177 @@
|
||||
# Self Hosting Maybe with Docker
|
||||
|
||||
## Quick Start
|
||||
This guide will help you setup, update, and maintain your self-hosted Maybe application with Docker Compose. Docker Compose is the most popular and recommended way to self-host the Maybe app.
|
||||
|
||||
_The below quickstart assumes you're running on Mac or Linux. Windows will
|
||||
be different._
|
||||
If you want a _less
|
||||
technical_ way to host the Maybe app, you can [host on Render](/docs/hosting/one-click-deploy.md) as an
|
||||
_**alternative** to Docker Compose_.
|
||||
|
||||
Make sure [Docker is installed](https://docs.docker.com/engine/install/) and
|
||||
setup your local environment:
|
||||
## Setup Guide
|
||||
|
||||
Follow the guide below to get your app running.
|
||||
|
||||
### Step 1: Install Docker
|
||||
|
||||
Complete the following steps:
|
||||
|
||||
1. Install Docker Engine by following [the official guide](https://docs.docker.com/engine/install/)
|
||||
2. Start the Docker service on your machine
|
||||
3. Verify that Docker is installed correctly and is running by opening up a terminal and running the following command:
|
||||
|
||||
```bash
|
||||
# If Docker is setup correctly, this command will succeed
|
||||
docker run hello-world
|
||||
```
|
||||
|
||||
### Step 2: Configure your Docker Compose file and environnment
|
||||
|
||||
#### Create a directory for your app to run
|
||||
|
||||
Open your terminal and create a directory where your app will run. Below is an example command with a recommended directory:
|
||||
|
||||
```bash
|
||||
# Create a directory on your computer for Docker files
|
||||
mkdir -p ~/docker-apps/maybe
|
||||
|
||||
# Once created, navigate your current working directory to the new folder
|
||||
cd ~/docker-apps/maybe
|
||||
|
||||
# Download the sample docker-compose.yml file from the Maybe Github repository
|
||||
curl -o docker-compose.yml https://raw.githubusercontent.com/maybe-finance/maybe/main/docker-compose.example.yml
|
||||
|
||||
# Create an .env file (make sure to fill in empty variables manually)
|
||||
cat << EOF > .env
|
||||
# Use "openssl rand -hex 64" to generate this
|
||||
SECRET_KEY_BASE=
|
||||
|
||||
# Can be any value, set to what you'd like
|
||||
POSTGRES_PASSWORD=
|
||||
EOF
|
||||
```
|
||||
|
||||
Make sure to generate your `SECRET_KEY_BASE` value and save the `.env` file.
|
||||
Then you're ready to run the app, which will be available at
|
||||
`http://localhost:3000` in your browser:
|
||||
#### Copy our sample Docker Compose file
|
||||
|
||||
Make sure you are in the directory you just created and run the following command:
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
# Download the sample docker-compose.yml file from the Maybe Github repository
|
||||
curl -o compose.yml https://raw.githubusercontent.com/maybe-finance/maybe/main/docker-compose.example.yml
|
||||
```
|
||||
|
||||
Lastly, go to `http://localhost:3000` in your browser, **create a new
|
||||
account**, and you're ready to start tracking your finances!
|
||||
This command will do the following:
|
||||
|
||||
## Detailed Setup Guide
|
||||
1. Fetch the sample docker compose file from our public Github repository
|
||||
2. Creates a file in your current directory called `compose.yml` with the contents of the example file
|
||||
|
||||
### Prerequisites
|
||||
At this point, the only file in your current working directory should be `compose.yml`.
|
||||
|
||||
- Install Docker Engine by
|
||||
following [the official guide](https://docs.docker.com/engine/install/)
|
||||
- Start the Docker service on your machine
|
||||
#### Create your environment file
|
||||
|
||||
### App Setup
|
||||
In order to configure the app, you will need to create a file called `.env`, which is where Docker will read environment variables from.
|
||||
|
||||
1. Create a new directory on your machine (we suggest something like
|
||||
`$HOME/docker-apps/maybe`)
|
||||
2. Create a `docker-compose.yml` file (we suggest
|
||||
using [our example](/docker-compose.example.yml)
|
||||
if
|
||||
you're new to self-hosting and Docker)
|
||||
3. Create a `.env` file and add the required variables. Currently,
|
||||
`SECRET_KEY_BASE` is the only required variable, but you can take a look
|
||||
at our [.env.example](/.env.example) file to see all available options.
|
||||
To do this, run the following command:
|
||||
|
||||
### Run app with Docker Compose
|
||||
```bash
|
||||
touch .env
|
||||
```
|
||||
|
||||
1. Run `docker-compose up -d` to start the maybe app in detached mode.
|
||||
2. Access the Maybe app by navigating to http://localhost:3000 in your web
|
||||
browser.
|
||||
#### Generate the app secret key
|
||||
|
||||
### Updating the App
|
||||
The app requires an environment variable called `SECRET_KEY_BASE` to run.
|
||||
|
||||
The mechanism that updates your self-hosted Maybe app is the GHCR (Github
|
||||
Container Registry) Docker image that you see in the `docker-compose.yml` file:
|
||||
We will first need to generate this in the terminal. If you have `openssl` installed on your computer, you can generate it with the following command:
|
||||
|
||||
```bash
|
||||
openssl rand -hex 64
|
||||
```
|
||||
|
||||
_Alternatively_, you can generate a key without openssl or any external dependencies by pasting the following bash command in your terminal and running it:
|
||||
|
||||
```bash
|
||||
head -c 64 /dev/urandom | od -An -tx1 | tr -d ' \n' && echo
|
||||
```
|
||||
|
||||
Once you have generated a key, save it and move on to the next step.
|
||||
|
||||
#### Fill in your environment file
|
||||
|
||||
Open the file named `.env` that we created in a prior step using your favorite text editor.
|
||||
|
||||
Fill in this file with the following variables:
|
||||
|
||||
```txt
|
||||
SECRET_KEY_BASE="replacemewiththegeneratedstringfromthepriorstep"
|
||||
POSTGRES_PASSWORD="replacemewithyourdesireddatabasepassword"
|
||||
```
|
||||
|
||||
### Step 3: Test your app
|
||||
|
||||
You are now ready to run the app. Start with the following command to make sure everything is working:
|
||||
|
||||
```bash
|
||||
docker compose up
|
||||
```
|
||||
|
||||
This will pull our official Docker image and start the app. You will see logs in your terminal.
|
||||
|
||||
Open your browser, and navigate to `http://localhost:3000`.
|
||||
|
||||
If everything is working, you will see the Maybe login screen.
|
||||
|
||||
### Step 4: Create your account
|
||||
|
||||
The first time you run the app, you will need to register a new account by hitting "register" on the login page.
|
||||
|
||||
1. Enter your email
|
||||
2. Enter a password
|
||||
|
||||
### Step 5: Run the app in the background
|
||||
|
||||
Most self-hosting users will want the Maybe app to run in the background on their computer so they can access it at all times. To do this, hit `Ctrl+C` to stop the running process, and then run the following command:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
The `-d` flag will run Docker Compose in "detached" mode. To verify it is running, you can run the following command:
|
||||
|
||||
```
|
||||
docker compose ls
|
||||
```
|
||||
|
||||
### Step 6: Enjoy!
|
||||
|
||||
Your app is now set up. You can visit it at `http://localhost:3000` in your browser.
|
||||
|
||||
If you find bugs or have a feature request, be sure to read through our [contributing guide here](https://github.com/maybe-finance/maybe/wiki/How-to-Contribute-Effectively-to-this-Project).
|
||||
|
||||
## How to update your app
|
||||
|
||||
The mechanism that updates your self-hosted Maybe app is the GHCR (Github Container Registry) Docker image that you see in the `docker-compose.yml` file:
|
||||
|
||||
```yml
|
||||
image: ghcr.io/maybe-finance/maybe:latest
|
||||
```
|
||||
|
||||
We recommend using one of the following images, but you can pin your app to
|
||||
whatever version you'd like (
|
||||
see [packages](https://github.com/maybe-finance/maybe/pkgs/container/maybe)):
|
||||
We recommend using one of the following images, but you can pin your app to whatever version you'd like (see [packages](https://github.com/maybe-finance/maybe/pkgs/container/maybe)):
|
||||
|
||||
- `ghcr.io/maybe-finance/maybe:latest` (latest commit)
|
||||
- `ghcr.io/maybe-finance/maybe:stable` (latest release)
|
||||
|
||||
By default, your app _will NOT_ automatically update. To update your
|
||||
self-hosted app, you must run the following commands:
|
||||
By default, your app _will
|
||||
NOT_ automatically update. To update your self-hosted app, run the following commands in your terminal:
|
||||
|
||||
```bash
|
||||
docker-compose pull # This pulls the "latest" published image from GHCR
|
||||
|
||||
docker-compose up -d # Restarts the app
|
||||
cd ~/docker-apps/maybe # Navigate to whatever directory you configured the app in
|
||||
docker compose pull # This pulls the "latest" published image from GHCR
|
||||
docker compose build app # This rebuilds the app with updates
|
||||
docker compose up --no-deps -d app # This restarts the app using the newest version
|
||||
```
|
||||
|
||||
#### Changing the image
|
||||
## How to change which updates your app receives
|
||||
|
||||
If you'd like to pin the app to a specific version or tag, all you need to do is
|
||||
edit the `docker-compose.yml` file:
|
||||
If you'd like to pin the app to a specific version or tag, all you need to do is edit the `docker-compose.yml` file:
|
||||
|
||||
```yml
|
||||
image: ghcr.io/maybe-finance/maybe:stable
|
||||
```
|
||||
|
||||
After doing this, make sure and restart the app:
|
||||
|
||||
```bash
|
||||
docker compose pull # This pulls the "latest" published image from GHCR
|
||||
docker compose build app # This rebuilds the app with updates
|
||||
docker compose up --no-deps -d app # This restarts the app using the newest version
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
This section will provide troubleshooting tips and solutions for common issues
|
||||
|
||||
@@ -6,6 +6,15 @@ class AccountsControllerTest < ActionDispatch::IntegrationTest
|
||||
@account = accounts(:checking)
|
||||
end
|
||||
|
||||
test "gets accounts list" do
|
||||
get accounts_url
|
||||
assert_response :success
|
||||
|
||||
@user.family.accounts.each do |account|
|
||||
assert_dom "#" + dom_id(account), count: 1
|
||||
end
|
||||
end
|
||||
|
||||
test "new" do
|
||||
get new_account_path
|
||||
assert_response :ok
|
||||
@@ -16,20 +25,55 @@ class AccountsControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_response :ok
|
||||
end
|
||||
|
||||
test "should create account" do
|
||||
assert_difference -> { Account.count }, +1 do
|
||||
post accounts_path, params: { account: { accountable_type: "Account::Credit" } }
|
||||
assert_redirected_to accounts_url
|
||||
test "can sync an account" do
|
||||
post sync_account_path(@account)
|
||||
assert_redirected_to account_url(@account)
|
||||
end
|
||||
|
||||
test "should update account" do
|
||||
patch account_url(@account), params: {
|
||||
account: {
|
||||
name: "Updated name",
|
||||
is_active: "0",
|
||||
institution_id: institutions(:chase).id
|
||||
}
|
||||
}
|
||||
|
||||
assert_redirected_to account_url(@account)
|
||||
assert_equal "Account updated", flash[:notice]
|
||||
end
|
||||
|
||||
test "should create an account" do
|
||||
assert_difference [ "Account.count", "Valuation.count" ], 1 do
|
||||
post accounts_path, params: {
|
||||
account: {
|
||||
accountable_type: "Account::Depository",
|
||||
balance: 200,
|
||||
subtype: "checking",
|
||||
institution_id: institutions(:chase).id
|
||||
}
|
||||
}
|
||||
|
||||
assert_equal "New account created successfully", flash[:notice]
|
||||
assert_redirected_to account_url(Account.order(:created_at).last)
|
||||
end
|
||||
end
|
||||
|
||||
test "should create a valuation together with account" do
|
||||
balance = 700
|
||||
start_date = 3.days.ago.to_date
|
||||
post accounts_path, params: { account: { accountable_type: "Account::Credit", balance:, start_date: } }
|
||||
test "can add optional start date and balance to an account on create" do
|
||||
assert_difference -> { Account.count } => 1, -> { Valuation.count } => 2 do
|
||||
post accounts_path, params: {
|
||||
account: {
|
||||
accountable_type: "Account::Depository",
|
||||
balance: 200,
|
||||
subtype: "checking",
|
||||
institution_id: institutions(:chase).id,
|
||||
start_balance: 100,
|
||||
start_date: 10.days.ago
|
||||
}
|
||||
}
|
||||
|
||||
new_valuation = Valuation.order(:created_at).last
|
||||
assert new_valuation.value == balance
|
||||
assert new_valuation.date == start_date
|
||||
assert_equal "New account created successfully", flash[:notice]
|
||||
assert_redirected_to account_url(Account.order(:created_at).last)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
55
test/controllers/institutions_controller_test.rb
Normal file
55
test/controllers/institutions_controller_test.rb
Normal file
@@ -0,0 +1,55 @@
|
||||
require "test_helper"
|
||||
|
||||
class InstitutionsControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
sign_in users(:family_admin)
|
||||
@institution = institutions(:chase)
|
||||
end
|
||||
|
||||
test "should get new" do
|
||||
get new_institution_url
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "can create institution" do
|
||||
assert_difference("Institution.count", 1) do
|
||||
post institutions_url, params: {
|
||||
institution: {
|
||||
name: "New institution"
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
assert_redirected_to accounts_url
|
||||
assert_equal "Institution created", flash[:notice]
|
||||
end
|
||||
|
||||
test "should get edit" do
|
||||
get edit_institution_url(@institution)
|
||||
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "should update institution" do
|
||||
patch institution_url(@institution), params: {
|
||||
institution: {
|
||||
name: "New Institution Name",
|
||||
logo: file_fixture_upload("square-placeholder.png", "image/png", :binary)
|
||||
}
|
||||
}
|
||||
|
||||
assert_redirected_to accounts_url
|
||||
assert_equal "Institution updated", flash[:notice]
|
||||
end
|
||||
|
||||
test "can destroy institution without destroying accounts" do
|
||||
assert @institution.accounts.count > 0
|
||||
|
||||
assert_difference -> { Institution.count } => -1, -> { Account.count } => 0 do
|
||||
delete institution_url(@institution)
|
||||
end
|
||||
|
||||
assert_redirected_to accounts_url
|
||||
assert_equal "Institution deleted", flash[:notice]
|
||||
end
|
||||
end
|
||||
@@ -97,13 +97,16 @@ class TransactionsControllerTest < ActionDispatch::IntegrationTest
|
||||
|
||||
test "incomes are negative" do
|
||||
assert_difference("Transaction.count") do
|
||||
post transactions_url, params: { transaction: {
|
||||
nature: "income",
|
||||
account_id: @transaction.account_id,
|
||||
amount: @transaction.amount,
|
||||
currency: @transaction.currency,
|
||||
date: @transaction.date,
|
||||
name: @transaction.name } }
|
||||
post transactions_url, params: {
|
||||
transaction: {
|
||||
nature: "income",
|
||||
account_id: @transaction.account_id,
|
||||
amount: @transaction.amount,
|
||||
currency: @transaction.currency,
|
||||
date: @transaction.date,
|
||||
name: @transaction.name
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
assert_redirected_to transactions_url
|
||||
@@ -122,7 +125,8 @@ class TransactionsControllerTest < ActionDispatch::IntegrationTest
|
||||
amount: @transaction.amount,
|
||||
currency: @transaction.currency,
|
||||
date: @transaction.date,
|
||||
name: @transaction.name
|
||||
name: @transaction.name,
|
||||
tag_ids: [ Tag.first.id, Tag.second.id ]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,4 +142,48 @@ class TransactionsControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_redirected_to transactions_url
|
||||
assert_enqueued_with(job: AccountSyncJob)
|
||||
end
|
||||
|
||||
test "can destroy many transactions at once" do
|
||||
delete_count = 10
|
||||
assert_difference("Transaction.count", -delete_count) do
|
||||
post bulk_delete_transactions_url, params: { bulk_delete: { transaction_ids: @recent_transactions.first(delete_count).pluck(:id) } }
|
||||
end
|
||||
|
||||
assert_redirected_to transactions_url
|
||||
assert_equal "10 transactions deleted", flash[:notice]
|
||||
end
|
||||
|
||||
test "can update many transactions at once" do
|
||||
transactions = @user.family.transactions.ordered.limit(20)
|
||||
|
||||
transactions.each do |transaction|
|
||||
transaction.update! \
|
||||
excluded: false,
|
||||
category_id: Transaction::Category.first.id,
|
||||
merchant_id: Transaction::Merchant.first.id,
|
||||
notes: "Starting note"
|
||||
end
|
||||
|
||||
post bulk_update_transactions_url, params: {
|
||||
bulk_update: {
|
||||
date: Date.current,
|
||||
transaction_ids: transactions.map(&:id),
|
||||
excluded: true,
|
||||
category_id: Transaction::Category.second.id,
|
||||
merchant_id: Transaction::Merchant.second.id,
|
||||
notes: "Updated note"
|
||||
}
|
||||
}
|
||||
|
||||
assert_redirected_to transactions_url
|
||||
assert_equal "#{transactions.count} transactions updated", flash[:notice]
|
||||
|
||||
transactions.reload.each do |transaction|
|
||||
assert_equal Date.current, transaction.date
|
||||
assert transaction.excluded
|
||||
assert_equal Transaction::Category.second, transaction.category
|
||||
assert_equal Transaction::Merchant.second, transaction.merchant
|
||||
assert_equal "Updated note", transaction.notes
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
5
test/fixtures/accounts.yml
vendored
5
test/fixtures/accounts.yml
vendored
@@ -13,6 +13,7 @@ checking:
|
||||
balance: 5000
|
||||
accountable_type: Account::Depository
|
||||
accountable_id: "123e4567-e89b-12d3-a456-426614174000"
|
||||
institution: chase
|
||||
|
||||
# Account with both transactions and valuations
|
||||
savings_with_valuation_overrides:
|
||||
@@ -21,6 +22,7 @@ savings_with_valuation_overrides:
|
||||
balance: 20000
|
||||
accountable_type: Account::Depository
|
||||
accountable_id: "123e4567-e89b-12d3-a456-426614174001"
|
||||
institution: chase
|
||||
|
||||
# Liability account
|
||||
credit_card:
|
||||
@@ -29,6 +31,7 @@ credit_card:
|
||||
balance: 1000
|
||||
accountable_type: Account::Credit
|
||||
accountable_id: "123e4567-e89b-12d3-a456-426614174003"
|
||||
institution: chase
|
||||
|
||||
eur_checking:
|
||||
family: dylan_family
|
||||
@@ -37,6 +40,7 @@ eur_checking:
|
||||
balance: 12000
|
||||
accountable_type: Account::Depository
|
||||
accountable_id: "123e4567-e89b-12d3-a456-426614174004"
|
||||
institution: revolut
|
||||
|
||||
# Multi-currency account (e.g. Wise, Revolut, etc.)
|
||||
multi_currency:
|
||||
@@ -46,3 +50,4 @@ multi_currency:
|
||||
balance: 10000
|
||||
accountable_type: Account::Depository
|
||||
accountable_id: "123e4567-e89b-12d3-a456-426614174005"
|
||||
institution: revolut
|
||||
|
||||
4
test/fixtures/active_storage/attachments.yml
vendored
Normal file
4
test/fixtures/active_storage/attachments.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
chase_logo_attachment:
|
||||
name: logo
|
||||
record: chase (Institution)
|
||||
blob: square_placeholder_blob
|
||||
1
test/fixtures/active_storage/blobs.yml
vendored
Normal file
1
test/fixtures/active_storage/blobs.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
square_placeholder_blob: <%= ActiveStorage::FixtureSet.blob filename: "square-placeholder.png" %>
|
||||
BIN
test/fixtures/files/square-placeholder.png
vendored
Normal file
BIN
test/fixtures/files/square-placeholder.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.4 KiB |
8
test/fixtures/institutions.yml
vendored
Normal file
8
test/fixtures/institutions.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
chase:
|
||||
name: Chase
|
||||
family: dylan_family
|
||||
|
||||
revolut:
|
||||
name: Revolut
|
||||
family: dylan_family
|
||||
logo_url: <%= "file://" + Rails.root.join('test/fixtures/files/square-placeholder.png').to_s %>
|
||||
@@ -4,6 +4,7 @@ class TransactionsTest < ApplicationSystemTestCase
|
||||
setup do
|
||||
sign_in @user = users(:family_admin)
|
||||
|
||||
@latest_transactions = @user.family.transactions.ordered.limit(20).to_a
|
||||
@test_category = @user.family.transaction_categories.create! name: "System Test Category"
|
||||
@test_merchant = @user.family.transaction_merchants.create! name: "System Test Merchant"
|
||||
@target_txn = @user.family.accounts.first.transactions.create! \
|
||||
@@ -91,4 +92,70 @@ class TransactionsTest < ApplicationSystemTestCase
|
||||
|
||||
assert_selector "#" + dom_id(@user.family.transactions.ordered.first), count: 1
|
||||
end
|
||||
|
||||
test "can select and deselect entire page of transactions" do
|
||||
all_transactions_checkbox.check
|
||||
assert_selection_count(number_of_transactions_on_page)
|
||||
all_transactions_checkbox.uncheck
|
||||
assert_selection_count(0)
|
||||
end
|
||||
|
||||
test "can select and deselect groups of transactions" do
|
||||
date_transactions_checkbox(12.days.ago.to_date).check
|
||||
assert_selection_count(3)
|
||||
date_transactions_checkbox(12.days.ago.to_date).uncheck
|
||||
assert_selection_count(0)
|
||||
end
|
||||
|
||||
test "can select and deselect individual transactions" do
|
||||
transaction_checkbox(@latest_transactions.first).check
|
||||
assert_selection_count(1)
|
||||
transaction_checkbox(@latest_transactions.second).check
|
||||
assert_selection_count(2)
|
||||
transaction_checkbox(@latest_transactions.second).uncheck
|
||||
assert_selection_count(1)
|
||||
end
|
||||
|
||||
test "outermost group always overrides inner selections" do
|
||||
transaction_checkbox(@latest_transactions.first).check
|
||||
assert_selection_count(1)
|
||||
all_transactions_checkbox.check
|
||||
assert_selection_count(number_of_transactions_on_page)
|
||||
transaction_checkbox(@latest_transactions.first).uncheck
|
||||
assert_selection_count(number_of_transactions_on_page - 1)
|
||||
date_transactions_checkbox(12.days.ago.to_date).uncheck
|
||||
assert_selection_count(number_of_transactions_on_page - 4)
|
||||
all_transactions_checkbox.uncheck
|
||||
assert_selection_count(0)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def number_of_transactions_on_page
|
||||
page_size = 50
|
||||
|
||||
[ @user.family.transactions.count, page_size ].min
|
||||
end
|
||||
|
||||
def all_transactions_checkbox
|
||||
find("#selection_transaction")
|
||||
end
|
||||
|
||||
def date_transactions_checkbox(date)
|
||||
find("#selection_transaction_#{date}")
|
||||
end
|
||||
|
||||
def transaction_checkbox(transaction)
|
||||
find("#" + dom_id(transaction, "selection"))
|
||||
end
|
||||
|
||||
def assert_selection_count(count)
|
||||
if count == 0
|
||||
assert_no_selector("#transaction-selection-bar")
|
||||
else
|
||||
within "#transaction-selection-bar" do
|
||||
assert_text "#{count} transaction#{count == 1 ? "" : "s"} selected"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user