Compare commits

...

22 Commits

Author SHA1 Message Date
Zach Gollwitzer
773cd0da71 Bump to 0.1.0-alpha.6
Signed-off-by: Zach Gollwitzer <zach@maybe.co>
2024-06-14 16:50:08 -04:00
Mattia
5da34c4609 Changelog page that pulls from Github Release notes (#867)
* Changelog page that pulls from Github Release notes

* Review changelog page styles

* Move changelog page title to i18n translations
2024-06-14 16:40:50 -04:00
Zach Gollwitzer
957584b69c Clean up sync logic (#871) 2024-06-13 17:03:38 -04:00
Zach Gollwitzer
d0a15b8a98 Improve self hosting docs and UI (#870) 2024-06-13 16:19:05 -04:00
Zach Gollwitzer
9956a9540e Add institution management and account editing controls (#868)
* Add institution management

* Allow user to select institution on create or edit

* Improve redirect behavior

* Final cleanup

* i18n normalization
2024-06-13 14:37:27 -04:00
Zach Gollwitzer
8c1a7af37f Allow for optional start date on account creation (#866) 2024-06-13 09:16:00 -04:00
Zach Gollwitzer
c5704ffd45 Improve account internal linking and redirect behavior (#864)
* Fix transaction row link and overflow

* Allow user to access imports from account page

* Clean up accounts controller, add link to account page from settings

* Add link to accounts management from accounts summary page

* Cleanup styles
2024-06-11 18:47:38 -04:00
Zach Gollwitzer
8372e26864 Allow optional import fields (#865) 2024-06-11 18:46:44 -04:00
dependabot[bot]
6477c0f766 Bump aws-sdk-s3 from 1.151.0 to 1.152.0 (#854)
Bumps [aws-sdk-s3](https://github.com/aws/aws-sdk-ruby) from 1.151.0 to 1.152.0.
- [Release notes](https://github.com/aws/aws-sdk-ruby/releases)
- [Changelog](https://github.com/aws/aws-sdk-ruby/blob/version-3/gems/aws-sdk-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-ruby/commits)

---
updated-dependencies:
- dependency-name: aws-sdk-s3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-10 12:39:15 -04:00
dependabot[bot]
2a8bb57c9c Bump pagy from 8.4.1 to 8.4.4 (#853)
Bumps [pagy](https://github.com/ddnexus/pagy) from 8.4.1 to 8.4.4.
- [Release notes](https://github.com/ddnexus/pagy/releases)
- [Changelog](https://github.com/ddnexus/pagy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ddnexus/pagy/compare/8.4.1...8.4.4)

---
updated-dependencies:
- dependency-name: pagy
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-10 12:38:44 -04:00
dependabot[bot]
2f432ec0c3 Bump good_job from 3.29.2 to 3.29.3 (#851)
Bumps [good_job](https://github.com/bensheldon/good_job) from 3.29.2 to 3.29.3.
- [Release notes](https://github.com/bensheldon/good_job/releases)
- [Changelog](https://github.com/bensheldon/good_job/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bensheldon/good_job/compare/v3.29.2...v3.29.3)

---
updated-dependencies:
- dependency-name: good_job
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-10 12:38:25 -04:00
dependabot[bot]
e3269e8981 Bump faraday from 2.9.0 to 2.9.1 (#850)
Bumps [faraday](https://github.com/lostisland/faraday) from 2.9.0 to 2.9.1.
- [Release notes](https://github.com/lostisland/faraday/releases)
- [Changelog](https://github.com/lostisland/faraday/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lostisland/faraday/compare/v2.9.0...v2.9.1)

---
updated-dependencies:
- dependency-name: faraday
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-10 12:37:59 -04:00
dependabot[bot]
8f891b8d8c Bump tailwindcss-rails from 2.6.0 to 2.6.1 (#848)
Bumps [tailwindcss-rails](https://github.com/rails/tailwindcss-rails) from 2.6.0 to 2.6.1.
- [Release notes](https://github.com/rails/tailwindcss-rails/releases)
- [Changelog](https://github.com/rails/tailwindcss-rails/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rails/tailwindcss-rails/compare/v2.6.0...v2.6.1)

---
updated-dependencies:
- dependency-name: tailwindcss-rails
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-10 12:37:04 -04:00
dependabot[bot]
775921092c Bump rails from 8e7eb03 to f9c847f (#855)
Bumps [rails](https://github.com/rails/rails) from `8e7eb03` to `f9c847f`.
- [Release notes](https://github.com/rails/rails/releases)
- [Commits](8e7eb03d99...f9c847fac1)

---
updated-dependencies:
- dependency-name: rails
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-10 12:36:08 -04:00
dependabot[bot]
83e2bfceb8 Bump lucide-rails from 6170b3a to 79d9895 (#849)
Bumps [lucide-rails](https://github.com/maybe-finance/lucide-rails) from `6170b3a` to `79d9895`.
- [Commits](6170b3a0ec...79d989593e)

---
updated-dependencies:
- dependency-name: lucide-rails
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-10 12:32:25 -04:00
Zach Gollwitzer
87a40aafeb Bump to v0.1.0-alpha.5
Signed-off-by: Zach Gollwitzer <zach@maybe.co>
2024-06-07 19:29:01 -04:00
Zach Gollwitzer
a681e73fea Enable bulk editing of transactions (#846) 2024-06-07 18:59:46 -04:00
Zach Gollwitzer
d3f9be15f1 Bulk transaction deletion (#845)
* Clean up transaction show view, add delete button

* Clean up tailwind global styles, add switch

* Bulk deletion controller and tests

* Normalize translations

* Add bulk deletion button and form
2024-06-07 16:56:30 -04:00
Zach Gollwitzer
115f792198 Add bulk selection UI controls (#840)
* Add bulk selection UI

* Handle bulk selection with Stimulus controller instead of session

* Update tests

* Remove stale routes

* Remove old system test helper methods
2024-06-07 12:44:06 -04:00
dependabot[bot]
e4ac5c87e4 Bump rails from c1f1b14 to 8e7eb03 (#828)
Bumps [rails](https://github.com/rails/rails) from `c1f1b14` to `8e7eb03`.
- [Release notes](https://github.com/rails/rails/releases)
- [Commits](c1f1b14adc...8e7eb03d99)

---
updated-dependencies:
- dependency-name: rails
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-03 08:41:06 -04:00
dependabot[bot]
a4fef176e8 Bump pagy from 8.4.0 to 8.4.1 (#825)
Bumps [pagy](https://github.com/ddnexus/pagy) from 8.4.0 to 8.4.1.
- [Release notes](https://github.com/ddnexus/pagy/releases)
- [Changelog](https://github.com/ddnexus/pagy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ddnexus/pagy/compare/8.4.0...8.4.1)

---
updated-dependencies:
- dependency-name: pagy
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-03 08:33:46 -04:00
dependabot[bot]
ee5fc2be38 Bump ruby-lsp-rails from 0.3.6 to 0.3.7 (#826)
Bumps [ruby-lsp-rails](https://github.com/Shopify/ruby-lsp-rails) from 0.3.6 to 0.3.7.
- [Release notes](https://github.com/Shopify/ruby-lsp-rails/releases)
- [Commits](https://github.com/Shopify/ruby-lsp-rails/compare/v0.3.6...v0.3.7)

---
updated-dependencies:
- dependency-name: ruby-lsp-rails
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-03 08:33:35 -04:00
73 changed files with 1641 additions and 493 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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 */

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -31,6 +31,7 @@ class PagesController < ApplicationController
end
def changelog
@releases_notes = Provider::Github.new.fetch_latest_releases_notes
end
def feedback

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,5 @@
module InstitutionsHelper
def institution_logo(institution)
institution.logo.attached? ? institution.logo : institution.logo_url
end
end

View 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)
})
}
}

View File

@@ -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");
};

View File

@@ -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

View File

@@ -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"

View File

@@ -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",

View File

@@ -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

View 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

View File

@@ -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}"

View File

@@ -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, "&nbsp;".html_safe, class: "maybe-switch" %>
</div>
<% end %>
</div>

View File

@@ -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>

View 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">&middot;</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 %>

View 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>

View File

@@ -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>

View 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>

View 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>

View 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>

View File

@@ -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 %>

View 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 %>

View File

@@ -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">&middot;</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) %>

View File

@@ -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 %>

View File

@@ -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 %>

View File

@@ -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" %>

View 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 %>

View File

@@ -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>

View 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 %>

View 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 %>

View 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 %>

View File

@@ -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) %>

View File

@@ -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>

View File

@@ -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 %>

View File

@@ -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) %>

View File

@@ -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">

View File

@@ -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>

View 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>

View File

@@ -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 %>

View 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>

View File

@@ -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">

View 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 %>

View File

@@ -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 %>

View File

@@ -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 %>

View File

@@ -10,7 +10,7 @@ module Maybe
private
def semver
"0.1.0-alpha.4"
"0.1.0-alpha.6"
end
end
end

View File

@@ -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

View File

@@ -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.

View 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

View File

@@ -1,6 +1,8 @@
---
en:
pages:
changelog:
title: What's new
dashboard:
allocation_chart:
assets: Assets

View File

@@ -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

View File

@@ -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

View File

@@ -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: [

View 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

View 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
View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,4 @@
chase_logo_attachment:
name: logo
record: chase (Institution)
blob: square_placeholder_blob

View File

@@ -0,0 +1 @@
square_placeholder_blob: <%= ActiveStorage::FixtureSet.blob filename: "square-placeholder.png" %>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

8
test/fixtures/institutions.yml vendored Normal file
View 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 %>

View File

@@ -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