Compare commits
46 Commits
v0.1.0-alp
...
v0.1.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
320954282a | ||
|
|
9e1d8a753b | ||
|
|
3d4def59d6 | ||
|
|
da18c3d850 | ||
|
|
cb3fd34f90 | ||
|
|
593892bc2b | ||
|
|
bbcd3881db | ||
|
|
ee53546c1b | ||
|
|
66c27b8df4 | ||
|
|
03e027e089 | ||
|
|
b7799aaa8e | ||
|
|
094128fef1 | ||
|
|
a5212f0f5e | ||
|
|
62d5df795b | ||
|
|
3cae528dfd | ||
|
|
12380dc8ad | ||
|
|
0bc0d87768 | ||
|
|
e13c3d9271 | ||
|
|
1e0635b31a | ||
|
|
bddaab0192 | ||
|
|
dc3147c101 | ||
|
|
2681dd96b1 | ||
|
|
a947db92b2 | ||
|
|
778098ebb0 | ||
|
|
ca39b26070 | ||
|
|
b462bc8f8c | ||
|
|
73ecf0b912 | ||
|
|
cdaed495b3 | ||
|
|
651028a9f3 | ||
|
|
c4fb9a54a2 | ||
|
|
9af355fc59 | ||
|
|
773cd0da71 | ||
|
|
5da34c4609 | ||
|
|
957584b69c | ||
|
|
d0a15b8a98 | ||
|
|
9956a9540e | ||
|
|
8c1a7af37f | ||
|
|
c5704ffd45 | ||
|
|
8372e26864 | ||
|
|
6477c0f766 | ||
|
|
2a8bb57c9c | ||
|
|
2f432ec0c3 | ||
|
|
e3269e8981 | ||
|
|
8f891b8d8c | ||
|
|
775921092c | ||
|
|
83e2bfceb8 |
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -7,11 +7,6 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Where did this bug occur?**
|
||||
|
||||
- [ ] Local development
|
||||
- [ ] Self hosted app (i.e. Docker)
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
|
||||
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
type=raw,value=stable,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
id: build
|
||||
with:
|
||||
context: .
|
||||
|
||||
188
Gemfile.lock
188
Gemfile.lock
@@ -1,38 +1,38 @@
|
||||
GIT
|
||||
remote: https://github.com/maybe-finance/lucide-rails.git
|
||||
revision: 6170b3a0eceb43a8af6552638e9526673c356d0d
|
||||
revision: 79d989593ee4ac6c50106ec5e4d2bd4ec8f5af87
|
||||
specs:
|
||||
lucide-rails (0.2.0)
|
||||
railties (>= 4.1.0)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/rails/rails.git
|
||||
revision: 8e7eb03d990e0a2a80fe1ea80133ef5d58bbc268
|
||||
revision: 9e370f0243870b39ea9801eeb95498f3a0d7bd83
|
||||
branch: 7-2-stable
|
||||
specs:
|
||||
actioncable (7.2.0.beta1)
|
||||
actionpack (= 7.2.0.beta1)
|
||||
activesupport (= 7.2.0.beta1)
|
||||
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.beta1)
|
||||
actionpack (= 7.2.0.beta1)
|
||||
activejob (= 7.2.0.beta1)
|
||||
activerecord (= 7.2.0.beta1)
|
||||
activestorage (= 7.2.0.beta1)
|
||||
activesupport (= 7.2.0.beta1)
|
||||
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.beta1)
|
||||
actionpack (= 7.2.0.beta1)
|
||||
actionview (= 7.2.0.beta1)
|
||||
activejob (= 7.2.0.beta1)
|
||||
activesupport (= 7.2.0.beta1)
|
||||
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.beta1)
|
||||
actionview (= 7.2.0.beta1)
|
||||
activesupport (= 7.2.0.beta1)
|
||||
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.beta1)
|
||||
actionpack (= 7.2.0.beta1)
|
||||
activerecord (= 7.2.0.beta1)
|
||||
activestorage (= 7.2.0.beta1)
|
||||
activesupport (= 7.2.0.beta1)
|
||||
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.beta1)
|
||||
activesupport (= 7.2.0.beta1)
|
||||
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.beta1)
|
||||
activesupport (= 7.2.0.beta1)
|
||||
activejob (7.2.0.beta2)
|
||||
activesupport (= 7.2.0.beta2)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.2.0.beta1)
|
||||
activesupport (= 7.2.0.beta1)
|
||||
activerecord (7.2.0.beta1)
|
||||
activemodel (= 7.2.0.beta1)
|
||||
activesupport (= 7.2.0.beta1)
|
||||
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.beta1)
|
||||
actionpack (= 7.2.0.beta1)
|
||||
activejob (= 7.2.0.beta1)
|
||||
activerecord (= 7.2.0.beta1)
|
||||
activesupport (= 7.2.0.beta1)
|
||||
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.beta1)
|
||||
activesupport (7.2.0.beta2)
|
||||
base64
|
||||
bigdecimal
|
||||
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.beta1)
|
||||
actioncable (= 7.2.0.beta1)
|
||||
actionmailbox (= 7.2.0.beta1)
|
||||
actionmailer (= 7.2.0.beta1)
|
||||
actionpack (= 7.2.0.beta1)
|
||||
actiontext (= 7.2.0.beta1)
|
||||
actionview (= 7.2.0.beta1)
|
||||
activejob (= 7.2.0.beta1)
|
||||
activemodel (= 7.2.0.beta1)
|
||||
activerecord (= 7.2.0.beta1)
|
||||
activestorage (= 7.2.0.beta1)
|
||||
activesupport (= 7.2.0.beta1)
|
||||
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.beta1)
|
||||
railties (7.2.0.beta1)
|
||||
actionpack (= 7.2.0.beta1)
|
||||
activesupport (= 7.2.0.beta1)
|
||||
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.944.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.84.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.3)
|
||||
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.3.1)
|
||||
concurrent-ruby (1.3.3)
|
||||
connection_pool (2.4.1)
|
||||
crack (1.0.0)
|
||||
bigdecimal
|
||||
@@ -174,10 +175,10 @@ GEM
|
||||
rainbow
|
||||
rubocop
|
||||
smart_properties
|
||||
erubi (1.12.0)
|
||||
erubi (1.13.0)
|
||||
et-orbi (1.2.11)
|
||||
tzinfo
|
||||
faraday (2.9.0)
|
||||
faraday (2.9.2)
|
||||
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.4)
|
||||
activejob (>= 6.0.0)
|
||||
activerecord (>= 6.0.0)
|
||||
concurrent-ruby (>= 1.0.2)
|
||||
@@ -225,7 +226,7 @@ GEM
|
||||
activesupport (>= 3.0)
|
||||
nokogiri (>= 1.6)
|
||||
io-console (0.7.2)
|
||||
irb (1.13.1)
|
||||
irb (1.13.2)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
jmespath (1.6.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)
|
||||
@@ -251,13 +253,13 @@ GEM
|
||||
matrix (0.4.2)
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.5)
|
||||
minitest (5.23.1)
|
||||
mocha (2.3.0)
|
||||
minitest (5.24.0)
|
||||
mocha (2.4.0)
|
||||
ruby2_keywords (>= 0.0.5)
|
||||
msgpack (1.7.2)
|
||||
net-http (0.4.1)
|
||||
uri
|
||||
net-imap (0.4.12)
|
||||
net-imap (0.4.14)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
@@ -267,23 +269,22 @@ GEM
|
||||
net-smtp (0.5.0)
|
||||
net-protocol
|
||||
nio4r (2.7.3)
|
||||
nokogiri (1.16.5-aarch64-linux)
|
||||
nokogiri (1.16.6-aarch64-linux)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.5-arm-linux)
|
||||
nokogiri (1.16.6-arm-linux)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.5-arm64-darwin)
|
||||
nokogiri (1.16.6-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.5-x86-linux)
|
||||
nokogiri (1.16.6-x86-linux)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.5-x86_64-darwin)
|
||||
nokogiri (1.16.6-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.5-x86_64-linux)
|
||||
nokogiri (1.16.6-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
octokit (8.1.0)
|
||||
base64
|
||||
octokit (9.1.0)
|
||||
faraday (>= 1, < 3)
|
||||
sawyer (~> 0.9)
|
||||
pagy (8.4.1)
|
||||
pagy (8.4.5)
|
||||
parallel (1.24.0)
|
||||
parser (3.3.1.0)
|
||||
ast (~> 2.4.1)
|
||||
@@ -297,12 +298,12 @@ GEM
|
||||
railties (>= 7.0.0)
|
||||
psych (5.1.2)
|
||||
stringio
|
||||
public_suffix (5.0.5)
|
||||
public_suffix (5.1.0)
|
||||
puma (6.4.2)
|
||||
nio4r (~> 2.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.0)
|
||||
rack (3.0.11)
|
||||
rack (3.1.4)
|
||||
rack-session (2.0.0)
|
||||
rack (>= 3.0.0)
|
||||
rack-test (2.1.0)
|
||||
@@ -331,10 +332,10 @@ GEM
|
||||
rdoc (6.7.0)
|
||||
psych (>= 4.0.0)
|
||||
regexp_parser (2.9.2)
|
||||
reline (0.5.8)
|
||||
reline (0.5.9)
|
||||
io-console (~> 0.5)
|
||||
rexml (3.2.8)
|
||||
strscan (>= 3.0.9)
|
||||
rexml (3.3.0)
|
||||
strscan
|
||||
rubocop (1.63.5)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
@@ -378,8 +379,9 @@ GEM
|
||||
sawyer (0.9.2)
|
||||
addressable (>= 2.3.5)
|
||||
faraday (>= 0.17.3, < 3)
|
||||
selenium-webdriver (4.21.1)
|
||||
selenium-webdriver (4.22.0)
|
||||
base64 (~> 0.2)
|
||||
logger (~> 1.4)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
rubyzip (>= 1.2.2, < 3.0)
|
||||
websocket (~> 1.0)
|
||||
@@ -400,19 +402,19 @@ GEM
|
||||
stackprof (0.2.26)
|
||||
stimulus-rails (1.3.3)
|
||||
railties (>= 6.0.0)
|
||||
stringio (3.1.0)
|
||||
stringio (3.1.1)
|
||||
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)
|
||||
@@ -444,7 +446,7 @@ GEM
|
||||
websocket-extensions (0.1.5)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
zeitwerk (2.6.15)
|
||||
zeitwerk (2.6.16)
|
||||
|
||||
PLATFORMS
|
||||
aarch64-linux
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,31 +7,13 @@
|
||||
details > summary::-webkit-details-marker {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
details > summary {
|
||||
@apply list-none;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.prose {
|
||||
table {
|
||||
@apply divide-y divide-gray-300;
|
||||
}
|
||||
|
||||
tr {
|
||||
@apply divide-x divide-gray-100;
|
||||
}
|
||||
|
||||
th {
|
||||
@apply whitespace-nowrap px-2 text-left text-sm font-semibold text-gray-900 py-3.5;
|
||||
}
|
||||
|
||||
tbody {
|
||||
@apply divide-y divide-gray-200;
|
||||
}
|
||||
|
||||
td {
|
||||
@apply whitespace-nowrap px-2 py-2 text-sm text-gray-500;
|
||||
}
|
||||
}
|
||||
|
||||
.form-field {
|
||||
@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;
|
||||
@@ -81,6 +63,20 @@
|
||||
@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 */
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
class Accounts::LogosController < ApplicationController
|
||||
class Account::LogosController < ApplicationController
|
||||
def show
|
||||
@account = Current.family.accounts.find(params[:account_id])
|
||||
render_placeholder
|
||||
@@ -1,4 +1,4 @@
|
||||
class Transactions::RowsController < ApplicationController
|
||||
class Account::Transaction::RowsController < ApplicationController
|
||||
before_action :set_transaction, only: %i[ show update ]
|
||||
|
||||
def show
|
||||
@@ -7,7 +7,7 @@ class Transactions::RowsController < ApplicationController
|
||||
def update
|
||||
@transaction.update! transaction_params
|
||||
|
||||
redirect_to transaction_row_path(@transaction)
|
||||
redirect_to account_transaction_row_path(@transaction.account, @transaction)
|
||||
end
|
||||
|
||||
private
|
||||
@@ -17,6 +17,6 @@ class Transactions::RowsController < ApplicationController
|
||||
end
|
||||
|
||||
def set_transaction
|
||||
@transaction = Current.family.transactions.find(params[:id])
|
||||
@transaction = Current.family.accounts.find(params[:account_id]).transactions.find(params[:transaction_id])
|
||||
end
|
||||
end
|
||||
6
app/controllers/account/transaction/rules_controller.rb
Normal file
6
app/controllers/account/transaction/rules_controller.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
class Account::Transaction::RulesController < ApplicationController
|
||||
layout "with_sidebar"
|
||||
|
||||
def index
|
||||
end
|
||||
end
|
||||
47
app/controllers/account/transactions_controller.rb
Normal file
47
app/controllers/account/transactions_controller.rb
Normal file
@@ -0,0 +1,47 @@
|
||||
class Account::TransactionsController < ApplicationController
|
||||
layout "with_sidebar"
|
||||
|
||||
before_action :set_account
|
||||
before_action :set_transaction, only: %i[ show update destroy ]
|
||||
|
||||
def index
|
||||
@transactions = @account.transactions.ordered
|
||||
end
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def update
|
||||
@transaction.update! transaction_params
|
||||
@transaction.sync_account_later
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_back_or_to account_transaction_path(@account, @transaction), notice: t(".success") }
|
||||
format.turbo_stream { render turbo_stream: turbo_stream.replace(@transaction) }
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@transaction.destroy!
|
||||
@transaction.sync_account_later
|
||||
redirect_back_or_to account_url(@transaction.account), notice: t(".success")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Current.family.accounts.find(params[:account_id])
|
||||
end
|
||||
|
||||
def set_transaction
|
||||
@transaction = @account.transactions.find(params[:id])
|
||||
end
|
||||
|
||||
def search_params
|
||||
params.fetch(:q, {}).permit(:start_date, :end_date, :search, accounts: [], account_ids: [], categories: [], merchants: [])
|
||||
end
|
||||
|
||||
def transaction_params
|
||||
params.require(:account_transaction).permit(:name, :date, :amount, :currency, :notes, :excluded, :category_id, :merchant_id, tag_ids: [])
|
||||
end
|
||||
end
|
||||
44
app/controllers/account/transfers_controller.rb
Normal file
44
app/controllers/account/transfers_controller.rb
Normal file
@@ -0,0 +1,44 @@
|
||||
class Account::TransfersController < ApplicationController
|
||||
layout "with_sidebar"
|
||||
|
||||
before_action :set_transfer, only: :destroy
|
||||
|
||||
def new
|
||||
@transfer = Account::Transfer.new
|
||||
end
|
||||
|
||||
def create
|
||||
from_account = Current.family.accounts.find(transfer_params[:from_account_id])
|
||||
to_account = Current.family.accounts.find(transfer_params[:to_account_id])
|
||||
|
||||
@transfer = Account::Transfer.build_from_accounts from_account, to_account, \
|
||||
date: transfer_params[:date],
|
||||
amount: transfer_params[:amount].to_d,
|
||||
currency: transfer_params[:currency],
|
||||
name: transfer_params[:name]
|
||||
|
||||
if @transfer.save
|
||||
redirect_to transactions_path, notice: t(".success")
|
||||
else
|
||||
# TODO: this is not an ideal way to handle errors and should eventually be improved.
|
||||
# See: https://github.com/hotwired/turbo-rails/pull/367
|
||||
flash[:error] = @transfer.errors.full_messages.to_sentence
|
||||
redirect_to transactions_path
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@transfer.destroy_and_remove_marks!
|
||||
redirect_back_or_to transactions_url, notice: t(".success")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_transfer
|
||||
@transfer = Account::Transfer.find(params[:id])
|
||||
end
|
||||
|
||||
def transfer_params
|
||||
params.require(:account_transfer).permit(:from_account_id, :to_account_id, :amount, :currency, :date, :name)
|
||||
end
|
||||
end
|
||||
61
app/controllers/account/valuations_controller.rb
Normal file
61
app/controllers/account/valuations_controller.rb
Normal file
@@ -0,0 +1,61 @@
|
||||
class Account::ValuationsController < ApplicationController
|
||||
before_action :set_account
|
||||
before_action :set_valuation, only: %i[ show edit update destroy ]
|
||||
|
||||
def new
|
||||
@valuation = @account.valuations.new
|
||||
end
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def create
|
||||
@valuation = @account.valuations.build(valuation_params)
|
||||
|
||||
if @valuation.save
|
||||
@valuation.sync_account_later
|
||||
redirect_to account_path(@account), notice: "Valuation created"
|
||||
else
|
||||
# TODO: this is not an ideal way to handle errors and should eventually be improved.
|
||||
# See: https://github.com/hotwired/turbo-rails/pull/367
|
||||
flash[:error] = @valuation.errors.full_messages.to_sentence
|
||||
redirect_to account_path(@account)
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
if @valuation.update(valuation_params)
|
||||
@valuation.sync_account_later
|
||||
redirect_to account_path(@account), notice: t(".success")
|
||||
else
|
||||
# TODO: this is not an ideal way to handle errors and should eventually be improved.
|
||||
# See: https://github.com/hotwired/turbo-rails/pull/367
|
||||
flash[:error] = @valuation.errors.full_messages.to_sentence
|
||||
redirect_to account_path(@account)
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@valuation.destroy!
|
||||
@valuation.sync_account_later
|
||||
|
||||
redirect_to account_path(@account), notice: t(".success")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Current.family.accounts.find(params[:account_id])
|
||||
end
|
||||
|
||||
def set_valuation
|
||||
@valuation = @account.valuations.find(params[:id])
|
||||
end
|
||||
|
||||
def valuation_params
|
||||
params.require(:account_valuation).permit(:date, :value, :currency)
|
||||
end
|
||||
end
|
||||
@@ -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,47 +27,33 @@ 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
|
||||
@balance_series = @account.series(period: @period)
|
||||
@valuation_series = @account.valuations.to_series
|
||||
end
|
||||
|
||||
def edit
|
||||
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 +62,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 +76,10 @@ class AccountsController < ApplicationController
|
||||
end
|
||||
|
||||
def account_params
|
||||
params.require(:account).permit(:name, :accountable_type, :balance, :start_date, :currency, :subtype, :is_active)
|
||||
params.require(:account).permit(:name, :accountable_type, :balance, :start_date, :start_balance, :currency, :subtype, :is_active, :institution_id)
|
||||
end
|
||||
|
||||
def sync_account
|
||||
@account.sync_later
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,20 +2,8 @@ class ApplicationController < ActionController::Base
|
||||
include Authentication, Invitable, SelfHostable
|
||||
include Pagy::Backend
|
||||
|
||||
before_action :sync_accounts
|
||||
|
||||
default_form_builder ApplicationFormBuilder
|
||||
|
||||
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
|
||||
allow_browser versions: :modern
|
||||
|
||||
private
|
||||
|
||||
def sync_accounts
|
||||
return if Current.user.blank?
|
||||
|
||||
if Current.user.last_login_at.nil? || Current.user.last_login_at.before?(Date.current.beginning_of_day)
|
||||
Current.family.sync_accounts
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
class Transactions::CategoriesController < ApplicationController
|
||||
class CategoriesController < ApplicationController
|
||||
layout "with_sidebar"
|
||||
|
||||
before_action :set_category, only: %i[ edit update ]
|
||||
before_action :set_transaction, only: :create
|
||||
|
||||
def index
|
||||
@categories = Current.family.transaction_categories.alphabetically
|
||||
@categories = Current.family.categories.alphabetically
|
||||
end
|
||||
|
||||
def new
|
||||
@category = Current.family.transaction_categories.new color: Transaction::Category::COLORS.sample
|
||||
@category = Current.family.categories.new color: Category::COLORS.sample
|
||||
end
|
||||
|
||||
def create
|
||||
Transaction::Category.transaction do
|
||||
category = Current.family.transaction_categories.create!(category_params)
|
||||
Category.transaction do
|
||||
category = Current.family.categories.create!(category_params)
|
||||
@transaction.update!(category_id: category.id) if @transaction
|
||||
end
|
||||
|
||||
@@ -32,7 +32,7 @@ class Transactions::CategoriesController < ApplicationController
|
||||
|
||||
private
|
||||
def set_category
|
||||
@category = Current.family.transaction_categories.find(params[:id])
|
||||
@category = Current.family.categories.find(params[:id])
|
||||
end
|
||||
|
||||
def set_transaction
|
||||
@@ -42,6 +42,6 @@ class Transactions::CategoriesController < ApplicationController
|
||||
end
|
||||
|
||||
def category_params
|
||||
params.require(:transaction_category).permit(:name, :color)
|
||||
params.require(:category).permit(:name, :color)
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,4 @@
|
||||
class Transactions::Categories::DeletionsController < ApplicationController
|
||||
class Category::DeletionsController < ApplicationController
|
||||
layout "with_sidebar"
|
||||
|
||||
before_action :set_category
|
||||
@@ -15,12 +15,12 @@ class Transactions::Categories::DeletionsController < ApplicationController
|
||||
|
||||
private
|
||||
def set_category
|
||||
@category = Current.family.transaction_categories.find(params[:category_id])
|
||||
@category = Current.family.categories.find(params[:category_id])
|
||||
end
|
||||
|
||||
def set_replacement_category
|
||||
if params[:replacement_category_id].present?
|
||||
@replacement_category = Current.family.transaction_categories.find(params[:replacement_category_id])
|
||||
@replacement_category = Current.family.categories.find(params[:replacement_category_id])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,4 @@
|
||||
class Transactions::Categories::DropdownsController < ApplicationController
|
||||
class Category::DropdownsController < ApplicationController
|
||||
before_action :set_from_params
|
||||
|
||||
def show
|
||||
@@ -17,6 +17,6 @@ class Transactions::Categories::DropdownsController < ApplicationController
|
||||
end
|
||||
|
||||
def categories_scope
|
||||
Current.family.transaction_categories.alphabetically
|
||||
Current.family.categories.alphabetically
|
||||
end
|
||||
end
|
||||
@@ -9,7 +9,8 @@ class ImportsController < ApplicationController
|
||||
end
|
||||
|
||||
def new
|
||||
@import = Import.new
|
||||
account = Current.family.accounts.find_by(id: params[:account_id])
|
||||
@import = Import.new account: account
|
||||
end
|
||||
|
||||
def edit
|
||||
|
||||
35
app/controllers/institutions_controller.rb
Normal file
35
app/controllers/institutions_controller.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
class InstitutionsController < ApplicationController
|
||||
before_action :set_institution, except: %i[ new create ]
|
||||
|
||||
def new
|
||||
@institution = Institution.new
|
||||
end
|
||||
|
||||
def create
|
||||
Current.family.institutions.create!(institution_params)
|
||||
redirect_to accounts_path, notice: t(".success")
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
@institution.update!(institution_params)
|
||||
redirect_to accounts_path, notice: t(".success")
|
||||
end
|
||||
|
||||
def destroy
|
||||
@institution.destroy!
|
||||
redirect_to accounts_path, notice: t(".success")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def institution_params
|
||||
params.require(:institution).permit(:name, :logo)
|
||||
end
|
||||
|
||||
def set_institution
|
||||
@institution = Current.family.institutions.find(params[:id])
|
||||
end
|
||||
end
|
||||
41
app/controllers/merchants_controller.rb
Normal file
41
app/controllers/merchants_controller.rb
Normal file
@@ -0,0 +1,41 @@
|
||||
class MerchantsController < ApplicationController
|
||||
layout "with_sidebar"
|
||||
|
||||
before_action :set_merchant, only: %i[ edit update destroy ]
|
||||
|
||||
def index
|
||||
@merchants = Current.family.merchants.alphabetically
|
||||
end
|
||||
|
||||
def new
|
||||
@merchant = Merchant.new
|
||||
end
|
||||
|
||||
def create
|
||||
Current.family.merchants.create!(merchant_params)
|
||||
redirect_to merchants_path, notice: t(".success")
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
@merchant.update!(merchant_params)
|
||||
redirect_to merchants_path, notice: t(".success")
|
||||
end
|
||||
|
||||
def destroy
|
||||
@merchant.destroy!
|
||||
redirect_to merchants_path, notice: t(".success")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_merchant
|
||||
@merchant = Current.family.merchants.find(params[:id])
|
||||
end
|
||||
|
||||
def merchant_params
|
||||
params.require(:merchant).permit(:name, :color)
|
||||
end
|
||||
end
|
||||
@@ -21,7 +21,7 @@ class PagesController < ApplicationController
|
||||
|
||||
@accounts = Current.family.accounts
|
||||
@account_groups = @accounts.by_group(period: @period, currency: Current.family.currency)
|
||||
@transactions = Current.family.transactions.limit(5).order(date: :desc)
|
||||
@transactions = Current.family.transactions.limit(6).order(date: :desc)
|
||||
|
||||
# TODO: Placeholders for trendlines
|
||||
placeholder_series_data = 10.times.map do |i|
|
||||
@@ -31,6 +31,7 @@ class PagesController < ApplicationController
|
||||
end
|
||||
|
||||
def changelog
|
||||
@releases_notes = Provider::Github.new.fetch_latest_releases_notes
|
||||
end
|
||||
|
||||
def feedback
|
||||
|
||||
@@ -16,7 +16,7 @@ class RegistrationsController < ApplicationController
|
||||
@user.role = :admin
|
||||
|
||||
if @user.save
|
||||
Transaction::Category.create_default_categories(@user.family)
|
||||
Category.create_default_categories(@user.family)
|
||||
login @user
|
||||
flash[:notice] = t(".success")
|
||||
redirect_to root_path
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
class Tags::DeletionsController < ApplicationController
|
||||
class Tag::DeletionsController < ApplicationController
|
||||
layout "with_sidebar"
|
||||
|
||||
before_action :set_tag
|
||||
@@ -1,41 +0,0 @@
|
||||
class Transactions::MerchantsController < ApplicationController
|
||||
layout "with_sidebar"
|
||||
|
||||
before_action :set_merchant, only: %i[ edit update destroy ]
|
||||
|
||||
def index
|
||||
@merchants = Current.family.transaction_merchants.alphabetically
|
||||
end
|
||||
|
||||
def new
|
||||
@merchant = Transaction::Merchant.new
|
||||
end
|
||||
|
||||
def create
|
||||
Current.family.transaction_merchants.create!(merchant_params)
|
||||
redirect_to transaction_merchants_path, notice: t(".success")
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
@merchant.update!(merchant_params)
|
||||
redirect_to transaction_merchants_path, notice: t(".success")
|
||||
end
|
||||
|
||||
def destroy
|
||||
@merchant.destroy!
|
||||
redirect_to transaction_merchants_path, notice: t(".success")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_merchant
|
||||
@merchant = Current.family.transaction_merchants.find(params[:id])
|
||||
end
|
||||
|
||||
def merchant_params
|
||||
params.require(:transaction_merchant).permit(:name, :color)
|
||||
end
|
||||
end
|
||||
@@ -1,6 +0,0 @@
|
||||
class Transactions::RulesController < ApplicationController
|
||||
layout "with_sidebar"
|
||||
|
||||
def index
|
||||
end
|
||||
end
|
||||
@@ -1,60 +1,39 @@
|
||||
class TransactionsController < ApplicationController
|
||||
layout "with_sidebar"
|
||||
|
||||
before_action :set_transaction, only: %i[ show edit update destroy ]
|
||||
|
||||
def index
|
||||
@q = search_params
|
||||
result = Current.family.transactions.search(@q).ordered
|
||||
@pagy, @transactions = pagy(result, items: 50)
|
||||
@pagy, @transactions = pagy(result, items: params[:per_page] || "10")
|
||||
|
||||
@totals = {
|
||||
count: result.count,
|
||||
income: result.inflows.sum(&:amount_money).abs,
|
||||
expense: result.outflows.sum(&:amount_money).abs
|
||||
count: result.select { |t| t.currency == Current.family.currency }.count,
|
||||
income: result.income_total(Current.family.currency).abs,
|
||||
expense: result.expense_total(Current.family.currency)
|
||||
}
|
||||
end
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def new
|
||||
@transaction = Transaction.new.tap do |txn|
|
||||
@transaction = Account::Transaction.new.tap do |txn|
|
||||
if params[:account_id]
|
||||
txn.account = Current.family.accounts.find(params[:account_id])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def create
|
||||
@transaction = Current.family.accounts
|
||||
.find(params[:transaction][:account_id])
|
||||
.transactions.build(transaction_params.merge(amount: amount))
|
||||
.find(params[:transaction][:account_id])
|
||||
.transactions
|
||||
.create!(transaction_params.merge(amount: amount))
|
||||
|
||||
@transaction.save!
|
||||
@transaction.sync_account_later
|
||||
redirect_to transactions_url, notice: t(".success")
|
||||
end
|
||||
|
||||
def update
|
||||
@transaction.update! transaction_params
|
||||
@transaction.sync_account_later
|
||||
|
||||
redirect_to transaction_url(@transaction), notice: t(".success")
|
||||
end
|
||||
|
||||
def destroy
|
||||
@transaction.destroy!
|
||||
@transaction.sync_account_later
|
||||
redirect_to transactions_url, notice: t(".success")
|
||||
redirect_back_or_to account_path(@transaction.account), 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)
|
||||
redirect_back_or_to transactions_url, notice: t(".success", count: destroyed.count)
|
||||
end
|
||||
|
||||
def bulk_edit
|
||||
@@ -63,18 +42,32 @@ class TransactionsController < ApplicationController
|
||||
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)
|
||||
redirect_back_or_to transactions_url, notice: t(".success", count: transactions.count)
|
||||
else
|
||||
flash.now[:error] = t(".failure")
|
||||
render :index, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def mark_transfers
|
||||
Current.family
|
||||
.transactions
|
||||
.where(id: bulk_update_params[:transaction_ids])
|
||||
.mark_transfers!
|
||||
|
||||
def set_transaction
|
||||
@transaction = Current.family.transactions.find(params[:id])
|
||||
end
|
||||
redirect_back_or_to transactions_url, notice: t(".success")
|
||||
end
|
||||
|
||||
def unmark_transfers
|
||||
Current.family
|
||||
.transactions
|
||||
.where(id: bulk_update_params[:transaction_ids])
|
||||
.update_all marked_as_transfer: false
|
||||
|
||||
redirect_back_or_to transactions_url, notice: t(".success")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def amount
|
||||
if nature.income?
|
||||
@@ -101,6 +94,6 @@ class TransactionsController < ApplicationController
|
||||
end
|
||||
|
||||
def transaction_params
|
||||
params.require(:transaction).permit(:name, :date, :amount, :currency, :notes, :excluded, :category_id, :merchant_id, tag_ids: [])
|
||||
params.require(:transaction).permit(:name, :date, :amount, :currency, :category_id, tag_ids: [])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
class ValuationsController < ApplicationController
|
||||
before_action :set_valuation, only: %i[ edit update destroy ]
|
||||
def create
|
||||
@account = Current.family.accounts.find(params[:account_id])
|
||||
|
||||
# TODO: placeholder logic until we have a better abstraction for trends
|
||||
@valuation = @account.valuations.new(valuation_params.merge(currency: @account.currency))
|
||||
if @valuation.save
|
||||
@valuation.account.sync_later(@valuation.date)
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to account_path(@account), notice: "Valuation created" }
|
||||
format.turbo_stream
|
||||
end
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
flash.now[:error] = "Valuation already exists for this date"
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def show
|
||||
@valuation = Current.family.accounts.find(params[:account_id]).valuations.find(params[:id])
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
sync_start_date = [ @valuation.date, Date.parse(valuation_params[:date]) ].compact.min
|
||||
if @valuation.update(valuation_params)
|
||||
@valuation.account.sync_later(sync_start_date)
|
||||
|
||||
redirect_to account_path(@valuation.account), notice: "Valuation updated"
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
flash.now[:error] = "Valuation already exists for this date"
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def destroy
|
||||
@account = @valuation.account
|
||||
sync_start_date = @account.valuations.where("date < ?", @valuation.date).order(date: :desc).first&.date
|
||||
@valuation.destroy!
|
||||
@account.sync_later(sync_start_date)
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to account_path(@account), notice: "Valuation deleted" }
|
||||
format.turbo_stream
|
||||
end
|
||||
end
|
||||
|
||||
def new
|
||||
@account = Current.family.accounts.find(params[:account_id])
|
||||
@valuation = @account.valuations.new
|
||||
end
|
||||
|
||||
private
|
||||
# Use callbacks to share common setup or constraints between actions.
|
||||
def set_valuation
|
||||
@valuation = Valuation.find(params[:id])
|
||||
end
|
||||
|
||||
def valuation_params
|
||||
params.require(:valuation).permit(:date, :value)
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,4 @@
|
||||
module Transactions::SearchesHelper
|
||||
module Account::Transaction::SearchesHelper
|
||||
def transaction_search_filters
|
||||
[
|
||||
{ key: "account_filter", name: "Account", icon: "layers" },
|
||||
24
app/helpers/account/transactions_helper.rb
Normal file
24
app/helpers/account/transactions_helper.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
module Account::TransactionsHelper
|
||||
def unconfirmed_transfer?(transaction)
|
||||
transaction.marked_as_transfer && transaction.transfer.nil?
|
||||
end
|
||||
|
||||
def group_transactions_by_date(transactions)
|
||||
grouped_by_date = {}
|
||||
|
||||
transactions.each do |transaction|
|
||||
if transaction.transfer
|
||||
transfer_date = transaction.transfer.inflow_transaction.date
|
||||
grouped_by_date[transfer_date] ||= { transactions: [], transfers: [] }
|
||||
unless grouped_by_date[transfer_date][:transfers].include?(transaction.transfer)
|
||||
grouped_by_date[transfer_date][:transfers] << transaction.transfer
|
||||
end
|
||||
else
|
||||
grouped_by_date[transaction.date] ||= { transactions: [], transfers: [] }
|
||||
grouped_by_date[transaction.date][:transactions] << transaction
|
||||
end
|
||||
end
|
||||
|
||||
grouped_by_date
|
||||
end
|
||||
end
|
||||
2
app/helpers/account/transfers_helper.rb
Normal file
2
app/helpers/account/transfers_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
module Account::TransfersHelper
|
||||
end
|
||||
23
app/helpers/account/valuations_helper.rb
Normal file
23
app/helpers/account/valuations_helper.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
module Account::ValuationsHelper
|
||||
def valuation_icon(valuation)
|
||||
if valuation.oldest?
|
||||
"keyboard"
|
||||
elsif valuation.trend.direction.up?
|
||||
"arrow-up"
|
||||
elsif valuation.trend.direction.down?
|
||||
"arrow-down"
|
||||
else
|
||||
"minus"
|
||||
end
|
||||
end
|
||||
|
||||
def valuation_style(valuation)
|
||||
color = valuation.oldest? ? "#D444F1" : valuation.trend.color
|
||||
|
||||
<<-STYLE.strip
|
||||
background-color: color-mix(in srgb, #{color} 5%, white);
|
||||
border-color: color-mix(in srgb, #{color} 10%, white);
|
||||
color: #{color};
|
||||
STYLE
|
||||
end
|
||||
end
|
||||
@@ -27,14 +27,14 @@ module AccountsHelper
|
||||
|
||||
def class_mapping(accountable_type)
|
||||
{
|
||||
"Account::Credit" => { text: "text-red-500", bg: "bg-red-500", bg_transparent: "bg-red-500/10", fill: "fill-red-500", hex: "#F13636" },
|
||||
"Account::Loan" => { text: "text-fuchsia-500", bg: "bg-fuchsia-500", bg_transparent: "bg-fuchsia-500/10", fill: "fill-fuchsia-500", hex: "#D444F1" },
|
||||
"Account::OtherLiability" => { text: "text-gray-500", bg: "bg-gray-500", bg_transparent: "bg-gray-500/10", fill: "fill-gray-500", hex: "#737373" },
|
||||
"Account::Depository" => { text: "text-violet-500", bg: "bg-violet-500", bg_transparent: "bg-violet-500/10", fill: "fill-violet-500", hex: "#875BF7" },
|
||||
"Account::Investment" => { text: "text-blue-600", bg: "bg-blue-600", bg_transparent: "bg-blue-600/10", fill: "fill-blue-600", hex: "#1570EF" },
|
||||
"Account::OtherAsset" => { text: "text-green-500", bg: "bg-green-500", bg_transparent: "bg-green-500/10", fill: "fill-green-500", hex: "#12B76A" },
|
||||
"Account::Property" => { text: "text-cyan-500", bg: "bg-cyan-500", bg_transparent: "bg-cyan-500/10", fill: "fill-cyan-500", hex: "#06AED4" },
|
||||
"Account::Vehicle" => { text: "text-pink-500", bg: "bg-pink-500", bg_transparent: "bg-pink-500/10", fill: "fill-pink-500", hex: "#F23E94" }
|
||||
"CreditCard" => { text: "text-red-500", bg: "bg-red-500", bg_transparent: "bg-red-500/10", fill: "fill-red-500", hex: "#F13636" },
|
||||
"Loan" => { text: "text-fuchsia-500", bg: "bg-fuchsia-500", bg_transparent: "bg-fuchsia-500/10", fill: "fill-fuchsia-500", hex: "#D444F1" },
|
||||
"OtherLiability" => { text: "text-gray-500", bg: "bg-gray-500", bg_transparent: "bg-gray-500/10", fill: "fill-gray-500", hex: "#737373" },
|
||||
"Depository" => { text: "text-violet-500", bg: "bg-violet-500", bg_transparent: "bg-violet-500/10", fill: "fill-violet-500", hex: "#875BF7" },
|
||||
"Investment" => { text: "text-blue-600", bg: "bg-blue-600", bg_transparent: "bg-blue-600/10", fill: "fill-blue-600", hex: "#1570EF" },
|
||||
"OtherAsset" => { text: "text-green-500", bg: "bg-green-500", bg_transparent: "bg-green-500/10", fill: "fill-green-500", hex: "#12B76A" },
|
||||
"Property" => { text: "text-cyan-500", bg: "bg-cyan-500", bg_transparent: "bg-cyan-500/10", fill: "fill-cyan-500", hex: "#06AED4" },
|
||||
"Vehicle" => { text: "text-pink-500", bg: "bg-pink-500", bg_transparent: "bg-pink-500/10", fill: "fill-pink-500", hex: "#F23E94" }
|
||||
}.fetch(accountable_type, { text: "text-gray-500", bg: "bg-gray-500", bg_transparent: "bg-gray-500/10", fill: "fill-gray-500", hex: "#737373" })
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,7 +25,7 @@ class ApplicationFormBuilder < ActionView::Helpers::FormBuilder
|
||||
# See `Monetizable` concern, which adds a _money suffix to the attribute name
|
||||
# For a monetized field, the setter will always be the attribute name without the _money suffix
|
||||
def money_field(method, options = {})
|
||||
money = @object.send(method)
|
||||
money = @object && @object.respond_to?(method) ? @object.send(method) : nil
|
||||
raise ArgumentError, "The value of #{method} is not a Money object" unless money.is_a?(Money) || money.nil?
|
||||
|
||||
money_amount_method = method.to_s.chomp("_money").to_sym
|
||||
|
||||
@@ -122,4 +122,11 @@ module ApplicationHelper
|
||||
options.reverse_merge!(money.default_format_options)
|
||||
ActiveSupport::NumberHelper.number_to_delimited(money.amount.round(options[:precision] || 0), { delimiter: options[:delimiter], separator: options[:separator] })
|
||||
end
|
||||
|
||||
def totals_by_currency(collection:, money_method:, separator: " | ", negate: false, options: {})
|
||||
collection.group_by(&:currency)
|
||||
.transform_values { |item| negate ? item.sum(&money_method) * -1 : item.sum(&money_method) }
|
||||
.map { |_currency, money| format_money(money) }
|
||||
.join(separator)
|
||||
end
|
||||
end
|
||||
|
||||
7
app/helpers/categories_helper.rb
Normal file
7
app/helpers/categories_helper.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
module CategoriesHelper
|
||||
def null_category
|
||||
Category.new \
|
||||
name: "Uncategorized",
|
||||
color: Category::UNCATEGORIZED_COLOR
|
||||
end
|
||||
end
|
||||
5
app/helpers/institutions_helper.rb
Normal file
5
app/helpers/institutions_helper.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
module InstitutionsHelper
|
||||
def institution_logo(institution)
|
||||
institution.logo.attached? ? institution.logo : institution.logo_url
|
||||
end
|
||||
end
|
||||
@@ -6,6 +6,23 @@ module MenusHelper
|
||||
end
|
||||
end
|
||||
|
||||
def contextual_menu_modal_action_item(label, url, icon: "pencil-line", turbo_frame: nil)
|
||||
link_to url, class: "flex items-center rounded-lg text-gray-900 hover:bg-gray-50 py-2 px-3 gap-2", data: { turbo_frame: } do
|
||||
concat(lucide_icon(icon, class: "shrink-0 w-5 h-5 text-gray-500"))
|
||||
concat(tag.span(label, class: "text-sm"))
|
||||
end
|
||||
end
|
||||
|
||||
def contextual_menu_destructive_item(label, url, turbo_confirm: true, turbo_frame: nil)
|
||||
button_to url,
|
||||
method: :delete,
|
||||
class: "flex items-center w-full rounded-lg text-red-500 hover:bg-red-500/5 py-2 px-3 gap-2",
|
||||
data: { turbo_confirm: turbo_confirm, turbo_frame: } do
|
||||
concat(lucide_icon("trash-2", class: "shrink-0 w-5 h-5"))
|
||||
concat(tag.span(label, class: "text-sm"))
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def contextual_menu_icon
|
||||
tag.button class: "flex hover:bg-gray-100 p-2 rounded", data: { menu_target: "button" } do
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
module Transactions::CategoriesHelper
|
||||
def null_category
|
||||
Transaction::Category.new \
|
||||
name: "Uncategorized",
|
||||
color: Transaction::Category::UNCATEGORIZED_COLOR
|
||||
end
|
||||
end
|
||||
@@ -1,20 +0,0 @@
|
||||
module TransactionsHelper
|
||||
def transactions_group(date, transactions, transaction_partial_path = "transactions/transaction")
|
||||
header_left = content_tag :span do
|
||||
"#{date.strftime('%b %d, %Y')} · #{transactions.size}".html_safe
|
||||
end
|
||||
|
||||
header_right = content_tag :span do
|
||||
format_money(-transactions.sum(&:amount_money))
|
||||
end
|
||||
|
||||
header = header_left.concat(header_right)
|
||||
|
||||
content = render partial: transaction_partial_path, collection: transactions
|
||||
|
||||
render partial: "shared/list_group", locals: {
|
||||
header: header,
|
||||
content: content
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -1,2 +0,0 @@
|
||||
module ValuationsHelper
|
||||
end
|
||||
@@ -66,6 +66,8 @@ export default class extends Controller {
|
||||
}
|
||||
|
||||
#addHiddenFormInputsForSelectedIds(form, paramName, transactionIds) {
|
||||
this.#resetFormInputs(form, paramName);
|
||||
|
||||
transactionIds.forEach(id => {
|
||||
const input = document.createElement("input");
|
||||
input.type = 'hidden'
|
||||
@@ -75,6 +77,11 @@ export default class extends Controller {
|
||||
})
|
||||
}
|
||||
|
||||
#resetFormInputs(form, paramName) {
|
||||
const existingInputs = form.querySelectorAll(`input[name='${paramName}']`);
|
||||
existingInputs.forEach((input) => input.remove());
|
||||
}
|
||||
|
||||
#rowsForGroup(group) {
|
||||
return this.rowTargets.filter(row => group.contains(row))
|
||||
}
|
||||
@@ -113,7 +120,7 @@ export default class extends Controller {
|
||||
#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))
|
||||
const groupSelected = rows.length > 0 && rows.every(row => this.selectedIdsValue.includes(row.dataset.id))
|
||||
group.querySelector("input[type='checkbox']").checked = groupSelected
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export default class extends Controller {
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
this.imagePreviewTarget.innerHTML = `<img src="${e.target.result}" alt="Preview" class="w-24 h-24 rounded-full object-cover" />`;
|
||||
this.imagePreviewTarget.innerHTML = `<img src="${e.target.result}" alt="Preview" class="w-full h-full rounded-full object-cover" />`;
|
||||
this.templateTarget.classList.add("hidden");
|
||||
this.clearBtnTarget.classList.remove("hidden");
|
||||
};
|
||||
|
||||
@@ -2,10 +2,12 @@ class Account < ApplicationRecord
|
||||
include Syncable
|
||||
include Monetizable
|
||||
|
||||
broadcasts_refreshes
|
||||
|
||||
validates :family, presence: true
|
||||
|
||||
broadcasts_refreshes
|
||||
belongs_to :family
|
||||
belongs_to :institution, optional: true
|
||||
has_many :balances, dependent: :destroy
|
||||
has_many :valuations, dependent: :destroy
|
||||
has_many :transactions, dependent: :destroy
|
||||
@@ -19,6 +21,7 @@ class Account < ApplicationRecord
|
||||
scope :assets, -> { where(classification: "asset") }
|
||||
scope :liabilities, -> { where(classification: "liability") }
|
||||
scope :alphabetically, -> { order(:name) }
|
||||
scope :ungrouped, -> { where(institution_id: nil) }
|
||||
|
||||
delegated_type :accountable, types: Accountable::TYPES, dependent: :destroy
|
||||
|
||||
@@ -26,6 +29,10 @@ class Account < ApplicationRecord
|
||||
balances.where("date <= ?", date).order(date: :desc).first&.balance
|
||||
end
|
||||
|
||||
def favorable_direction
|
||||
classification == "asset" ? "up" : "down"
|
||||
end
|
||||
|
||||
# e.g. Wise, Revolut accounts that have transactions in multiple currencies
|
||||
def multi_currency?
|
||||
currencies = [ valuations.pluck(:currency), transactions.pluck(:currency) ].flatten.uniq
|
||||
@@ -80,4 +87,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
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
class Account::Credit < ApplicationRecord
|
||||
include Accountable
|
||||
end
|
||||
@@ -1,3 +0,0 @@
|
||||
class Account::Crypto < ApplicationRecord
|
||||
include Accountable
|
||||
end
|
||||
@@ -1,3 +0,0 @@
|
||||
class Account::Depository < ApplicationRecord
|
||||
include Accountable
|
||||
end
|
||||
@@ -1,3 +0,0 @@
|
||||
class Account::Loan < ApplicationRecord
|
||||
include Accountable
|
||||
end
|
||||
@@ -1,3 +0,0 @@
|
||||
class Account::OtherAsset < ApplicationRecord
|
||||
include Accountable
|
||||
end
|
||||
@@ -1,3 +0,0 @@
|
||||
class Account::OtherLiability < ApplicationRecord
|
||||
include Accountable
|
||||
end
|
||||
@@ -1,3 +0,0 @@
|
||||
class Account::Property < ApplicationRecord
|
||||
include Accountable
|
||||
end
|
||||
@@ -1,9 +1,10 @@
|
||||
class Transaction < ApplicationRecord
|
||||
class Account::Transaction < ApplicationRecord
|
||||
include Monetizable
|
||||
|
||||
monetize :amount
|
||||
|
||||
belongs_to :account
|
||||
belongs_to :transfer, optional: true, class_name: "Account::Transfer"
|
||||
belongs_to :category, optional: true
|
||||
belongs_to :merchant, optional: true
|
||||
has_many :taggings, as: :taggable, dependent: :destroy
|
||||
@@ -16,22 +17,22 @@ class Transaction < ApplicationRecord
|
||||
scope :active, -> { where(excluded: false) }
|
||||
scope :inflows, -> { where("amount <= 0") }
|
||||
scope :outflows, -> { where("amount > 0") }
|
||||
scope :by_name, ->(name) { where("transactions.name ILIKE ?", "%#{name}%") }
|
||||
scope :with_categories, ->(categories) { joins(:category).where(transaction_categories: { name: categories }) }
|
||||
scope :by_name, ->(name) { where("account_transactions.name ILIKE ?", "%#{name}%") }
|
||||
scope :with_categories, ->(categories) { joins(:category).where(categories: { name: categories }) }
|
||||
scope :with_accounts, ->(accounts) { joins(:account).where(accounts: { name: accounts }) }
|
||||
scope :with_account_ids, ->(account_ids) { joins(:account).where(accounts: { id: account_ids }) }
|
||||
scope :with_merchants, ->(merchants) { joins(:merchant).where(transaction_merchants: { name: merchants }) }
|
||||
scope :on_or_after_date, ->(date) { where("transactions.date >= ?", date) }
|
||||
scope :on_or_before_date, ->(date) { where("transactions.date <= ?", date) }
|
||||
scope :with_merchants, ->(merchants) { joins(:merchant).where(merchants: { name: merchants }) }
|
||||
scope :on_or_after_date, ->(date) { where("account_transactions.date >= ?", date) }
|
||||
scope :on_or_before_date, ->(date) { where("account_transactions.date <= ?", date) }
|
||||
scope :with_converted_amount, ->(currency = Current.family.currency) {
|
||||
# Join with exchange rates to convert the amount to the given currency
|
||||
# If no rate is available, exclude the transaction from the results
|
||||
select(
|
||||
"transactions.*",
|
||||
"transactions.amount * COALESCE(er.rate, 1) AS converted_amount"
|
||||
"account_transactions.*",
|
||||
"account_transactions.amount * COALESCE(er.rate, 1) AS converted_amount"
|
||||
)
|
||||
.joins(sanitize_sql_array([ "LEFT JOIN exchange_rates er ON transactions.date = er.date AND transactions.currency = er.base_currency AND er.converted_currency = ?", currency ]))
|
||||
.where("er.rate IS NOT NULL OR transactions.currency = ?", currency)
|
||||
.joins(sanitize_sql_array([ "LEFT JOIN exchange_rates er ON account_transactions.date = er.date AND account_transactions.currency = er.base_currency AND er.converted_currency = ?", currency ]))
|
||||
.where("er.rate IS NOT NULL OR account_transactions.currency = ?", currency)
|
||||
}
|
||||
|
||||
def inflow?
|
||||
@@ -42,6 +43,10 @@ class Transaction < ApplicationRecord
|
||||
amount > 0
|
||||
end
|
||||
|
||||
def transfer?
|
||||
marked_as_transfer
|
||||
end
|
||||
|
||||
def sync_account_later
|
||||
if destroyed?
|
||||
sync_start_date = previous_transaction_date
|
||||
@@ -53,6 +58,21 @@ class Transaction < ApplicationRecord
|
||||
end
|
||||
|
||||
class << self
|
||||
def income_total(currency = "USD")
|
||||
inflows.reject(&:transfer?).select { |t| t.currency == currency }.sum(&:amount_money)
|
||||
end
|
||||
|
||||
def expense_total(currency = "USD")
|
||||
outflows.reject(&:transfer?).select { |t| t.currency == currency }.sum(&:amount_money)
|
||||
end
|
||||
|
||||
def mark_transfers!
|
||||
update_all marked_as_transfer: true
|
||||
|
||||
# Attempt to "auto match" and save a transfer if 2 transactions selected
|
||||
Account::Transfer.new(transactions: all).save if all.count == 2
|
||||
end
|
||||
|
||||
def daily_totals(transactions, period: Period.last_30_days, currency: Current.family.currency)
|
||||
# Sum spending and income for each day in the period with the given currency
|
||||
select(
|
||||
@@ -60,7 +80,7 @@ class Transaction < ApplicationRecord
|
||||
"COALESCE(SUM(converted_amount) FILTER (WHERE converted_amount > 0), 0) AS spending",
|
||||
"COALESCE(SUM(-converted_amount) FILTER (WHERE converted_amount < 0), 0) AS income"
|
||||
)
|
||||
.from(transactions.with_converted_amount(currency), :t)
|
||||
.from(transactions.with_converted_amount(currency).where(marked_as_transfer: false), :t)
|
||||
.joins(sanitize_sql([ "RIGHT JOIN generate_series(?, ?, interval '1 day') AS gs(date) ON t.date = gs.date", period.date_range.first, period.date_range.last ]))
|
||||
.group("gs.date")
|
||||
end
|
||||
@@ -83,7 +103,7 @@ class Transaction < ApplicationRecord
|
||||
end
|
||||
|
||||
def search(params)
|
||||
query = all
|
||||
query = all.includes(:transfer)
|
||||
query = query.by_name(params[:search]) if params[:search].present?
|
||||
query = query.with_categories(params[:categories]) if params[:categories].present?
|
||||
query = query.with_accounts(params[:accounts]) if params[:accounts].present?
|
||||
@@ -96,7 +116,6 @@ class Transaction < ApplicationRecord
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def previous_transaction_date
|
||||
self.account
|
||||
.transactions
|
||||
62
app/models/account/transfer.rb
Normal file
62
app/models/account/transfer.rb
Normal file
@@ -0,0 +1,62 @@
|
||||
class Account::Transfer < ApplicationRecord
|
||||
has_many :transactions, dependent: :nullify
|
||||
|
||||
validate :net_zero_flows, if: :single_currency_transfer?
|
||||
validate :transaction_count, :from_different_accounts, :all_transactions_marked
|
||||
|
||||
def inflow_transaction
|
||||
transactions.find { |t| t.inflow? }
|
||||
end
|
||||
|
||||
def outflow_transaction
|
||||
transactions.find { |t| t.outflow? }
|
||||
end
|
||||
|
||||
def destroy_and_remove_marks!
|
||||
transaction do
|
||||
transactions.each do |t|
|
||||
t.update! marked_as_transfer: false
|
||||
end
|
||||
|
||||
destroy!
|
||||
end
|
||||
end
|
||||
|
||||
class << self
|
||||
def build_from_accounts(from_account, to_account, date:, amount:, currency:, name:)
|
||||
outflow = from_account.transactions.build(amount: amount.abs, currency: currency, date: date, name: name, marked_as_transfer: true)
|
||||
inflow = to_account.transactions.build(amount: -amount.abs, currency: currency, date: date, name: name, marked_as_transfer: true)
|
||||
|
||||
new transactions: [ outflow, inflow ]
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def single_currency_transfer?
|
||||
transactions.map(&:currency).uniq.size == 1
|
||||
end
|
||||
|
||||
def transaction_count
|
||||
unless transactions.size == 2
|
||||
errors.add :transactions, "must have exactly 2 transactions"
|
||||
end
|
||||
end
|
||||
|
||||
def from_different_accounts
|
||||
accounts = transactions.map(&:account_id).uniq
|
||||
errors.add :transactions, "must be from different accounts" if accounts.size < transactions.size
|
||||
end
|
||||
|
||||
def net_zero_flows
|
||||
unless transactions.sum(&:amount).zero?
|
||||
errors.add :transactions, "must have an inflow and outflow that net to zero"
|
||||
end
|
||||
end
|
||||
|
||||
def all_transactions_marked
|
||||
unless transactions.all?(&:marked_as_transfer)
|
||||
errors.add :transactions, "must be marked as transfer"
|
||||
end
|
||||
end
|
||||
end
|
||||
48
app/models/account/valuation.rb
Normal file
48
app/models/account/valuation.rb
Normal file
@@ -0,0 +1,48 @@
|
||||
class Account::Valuation < ApplicationRecord
|
||||
include Monetizable
|
||||
|
||||
monetize :value
|
||||
|
||||
belongs_to :account
|
||||
|
||||
validates :account, :date, :value, presence: true
|
||||
validates :date, uniqueness: { scope: :account_id }
|
||||
|
||||
scope :chronological, -> { order(:date) }
|
||||
scope :reverse_chronological, -> { order(date: :desc) }
|
||||
|
||||
def trend
|
||||
@trend ||= create_trend
|
||||
end
|
||||
|
||||
def oldest?
|
||||
account.valuations.chronological.limit(1).pluck(:date).first == self.date
|
||||
end
|
||||
|
||||
def sync_account_later
|
||||
if destroyed?
|
||||
sync_start_date = previous_valuation&.date
|
||||
else
|
||||
sync_start_date = [ date_previously_was, date ].compact.min
|
||||
end
|
||||
|
||||
account.sync_later(sync_start_date)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def previous_valuation
|
||||
@previous_valuation ||= self.account
|
||||
.valuations
|
||||
.where("date < ?", date)
|
||||
.order(date: :desc)
|
||||
.first
|
||||
end
|
||||
|
||||
def create_trend
|
||||
TimeSeries::Trend.new \
|
||||
current: self.value,
|
||||
previous: previous_valuation&.value,
|
||||
favorable_direction: account.favorable_direction
|
||||
end
|
||||
end
|
||||
@@ -1,3 +0,0 @@
|
||||
class Account::Vehicle < ApplicationRecord
|
||||
include Accountable
|
||||
end
|
||||
@@ -1,5 +1,5 @@
|
||||
class Transaction::Category < ApplicationRecord
|
||||
has_many :transactions, dependent: :nullify
|
||||
class Category < ApplicationRecord
|
||||
has_many :transactions, dependent: :nullify, class_name: "Account::Transaction"
|
||||
belongs_to :family
|
||||
|
||||
validates :name, :color, :family, presence: true
|
||||
@@ -24,7 +24,7 @@ class Transaction::Category < ApplicationRecord
|
||||
]
|
||||
|
||||
def self.create_default_categories(family)
|
||||
if family.transaction_categories.size > 0
|
||||
if family.categories.size > 0
|
||||
raise ArgumentError, "Family already has some categories"
|
||||
end
|
||||
|
||||
@@ -1,28 +1,19 @@
|
||||
module Accountable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
ASSET_TYPES = %w[ Account::Depository Account::Investment Account::Crypto Account::OtherAsset Account::Property Account::Vehicle ]
|
||||
LIABILITY_TYPES = %w[ Account::Credit Account::Loan Account::OtherLiability ]
|
||||
ASSET_TYPES = %w[ Depository Investment Crypto Property Vehicle OtherAsset ]
|
||||
LIABILITY_TYPES = %w[ CreditCard Loan OtherLiability ]
|
||||
TYPES = ASSET_TYPES + LIABILITY_TYPES
|
||||
|
||||
def self.from_type(type)
|
||||
return nil unless types.include?(type) || TYPES.include?(type)
|
||||
"Account::#{type.demodulize}".constantize
|
||||
return nil unless TYPES.include?(type)
|
||||
type.constantize
|
||||
end
|
||||
|
||||
def self.by_classification
|
||||
{ assets: ASSET_TYPES, liabilities: LIABILITY_TYPES }
|
||||
end
|
||||
|
||||
def self.types(classification = nil)
|
||||
types = classification ? (classification.to_sym == :asset ? ASSET_TYPES : LIABILITY_TYPES) : TYPES
|
||||
types.map { |type| type.demodulize }
|
||||
end
|
||||
|
||||
def self.classification(type)
|
||||
ASSET_TYPES.include?(type) ? :asset : :liability
|
||||
end
|
||||
|
||||
included do
|
||||
has_one :account, as: :accountable, touch: true
|
||||
end
|
||||
|
||||
3
app/models/credit_card.rb
Normal file
3
app/models/credit_card.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
class CreditCard < ApplicationRecord
|
||||
include Accountable
|
||||
end
|
||||
3
app/models/crypto.rb
Normal file
3
app/models/crypto.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
class Crypto < ApplicationRecord
|
||||
include Accountable
|
||||
end
|
||||
3
app/models/depository.rb
Normal file
3
app/models/depository.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
class Depository < ApplicationRecord
|
||||
include Accountable
|
||||
end
|
||||
@@ -2,23 +2,24 @@ class Family < ApplicationRecord
|
||||
has_many :users, dependent: :destroy
|
||||
has_many :tags, dependent: :destroy
|
||||
has_many :accounts, dependent: :destroy
|
||||
has_many :transactions, through: :accounts
|
||||
has_many :institutions, dependent: :destroy
|
||||
has_many :transactions, through: :accounts, class_name: "Account::Transaction"
|
||||
has_many :imports, through: :accounts
|
||||
has_many :transaction_categories, dependent: :destroy, class_name: "Transaction::Category"
|
||||
has_many :transaction_merchants, dependent: :destroy, class_name: "Transaction::Merchant"
|
||||
has_many :categories, dependent: :destroy
|
||||
has_many :merchants, dependent: :destroy
|
||||
|
||||
def snapshot(period = Period.all)
|
||||
query = accounts.active.joins(:balances)
|
||||
.where("account_balances.currency = ?", self.currency)
|
||||
.select(
|
||||
"account_balances.currency",
|
||||
"account_balances.date",
|
||||
"SUM(CASE WHEN accounts.classification = 'liability' THEN account_balances.balance ELSE 0 END) AS liabilities",
|
||||
"SUM(CASE WHEN accounts.classification = 'asset' THEN account_balances.balance ELSE 0 END) AS assets",
|
||||
"SUM(CASE WHEN accounts.classification = 'asset' THEN account_balances.balance WHEN accounts.classification = 'liability' THEN -account_balances.balance ELSE 0 END) AS net_worth",
|
||||
)
|
||||
.group("account_balances.date, account_balances.currency")
|
||||
.order("account_balances.date")
|
||||
.where("account_balances.currency = ?", self.currency)
|
||||
.select(
|
||||
"account_balances.currency",
|
||||
"account_balances.date",
|
||||
"SUM(CASE WHEN accounts.classification = 'liability' THEN account_balances.balance ELSE 0 END) AS liabilities",
|
||||
"SUM(CASE WHEN accounts.classification = 'asset' THEN account_balances.balance ELSE 0 END) AS assets",
|
||||
"SUM(CASE WHEN accounts.classification = 'asset' THEN account_balances.balance WHEN accounts.classification = 'liability' THEN -account_balances.balance ELSE 0 END) AS net_worth",
|
||||
)
|
||||
.group("account_balances.date, account_balances.currency")
|
||||
.order("account_balances.date")
|
||||
|
||||
query = query.where("account_balances.date >= ?", period.date_range.begin) if period.date_range.begin
|
||||
query = query.where("account_balances.date <= ?", period.date_range.end) if period.date_range.end
|
||||
@@ -34,15 +35,16 @@ class Family < ApplicationRecord
|
||||
def snapshot_account_transactions
|
||||
period = Period.last_30_days
|
||||
results = accounts.active.joins(:transactions)
|
||||
.select(
|
||||
"accounts.*",
|
||||
"COALESCE(SUM(amount) FILTER (WHERE amount > 0), 0) AS spending",
|
||||
"COALESCE(SUM(-amount) FILTER (WHERE amount < 0), 0) AS income"
|
||||
)
|
||||
.where("transactions.date >= ?", period.date_range.begin)
|
||||
.where("transactions.date <= ?", period.date_range.end)
|
||||
.group("id")
|
||||
.to_a
|
||||
.select(
|
||||
"accounts.*",
|
||||
"COALESCE(SUM(amount) FILTER (WHERE amount > 0), 0) AS spending",
|
||||
"COALESCE(SUM(-amount) FILTER (WHERE amount < 0), 0) AS income"
|
||||
)
|
||||
.where("account_transactions.date >= ?", period.date_range.begin)
|
||||
.where("account_transactions.date <= ?", period.date_range.end)
|
||||
.where("account_transactions.marked_as_transfer = ?", false)
|
||||
.group("id")
|
||||
.to_a
|
||||
|
||||
results.each do |r|
|
||||
r.define_singleton_method(:savings_rate) do
|
||||
@@ -58,7 +60,7 @@ class Family < ApplicationRecord
|
||||
end
|
||||
|
||||
def snapshot_transactions
|
||||
rolling_totals = Transaction.daily_rolling_totals(transactions, period: Period.last_30_days, currency: self.currency)
|
||||
rolling_totals = Account::Transaction.daily_rolling_totals(transactions, period: Period.last_30_days, currency: self.currency)
|
||||
|
||||
spending = []
|
||||
income = []
|
||||
|
||||
@@ -38,7 +38,7 @@ class Import < ApplicationRecord
|
||||
end
|
||||
|
||||
def get_selected_header_for_field(field)
|
||||
column_mappings&.dig(field) || field.key
|
||||
column_mappings&.dig(field.key) || field.key
|
||||
end
|
||||
|
||||
def update_csv!(row_idx:, col_idx:, value:)
|
||||
@@ -124,7 +124,7 @@ class Import < ApplicationRecord
|
||||
tags << tag_cache[tag_string] ||= account.family.tags.find_or_initialize_by(name: tag_string)
|
||||
end
|
||||
|
||||
category = category_cache[category_name] ||= account.family.transaction_categories.find_or_initialize_by(name: category_name) if category_name.present?
|
||||
category = category_cache[category_name] ||= account.family.categories.find_or_initialize_by(name: category_name) if category_name.present?
|
||||
|
||||
txn = account.transactions.build \
|
||||
name: row["name"].presence || FALLBACK_TRANSACTION_NAME,
|
||||
@@ -148,15 +148,18 @@ class Import < ApplicationRecord
|
||||
|
||||
name_field = Import::Field.new \
|
||||
key: "name",
|
||||
label: "Name"
|
||||
label: "Name",
|
||||
is_optional: true
|
||||
|
||||
category_field = Import::Field.new \
|
||||
key: "category",
|
||||
label: "Category"
|
||||
label: "Category",
|
||||
is_optional: true
|
||||
|
||||
tags_field = Import::Field.new \
|
||||
key: "tags",
|
||||
label: "Tags"
|
||||
label: "Tags",
|
||||
is_optional: true
|
||||
|
||||
amount_field = Import::Field.new \
|
||||
key: "amount",
|
||||
|
||||
@@ -15,12 +15,17 @@ class Import::Field
|
||||
|
||||
attr_reader :key, :label, :validator
|
||||
|
||||
def initialize(key:, label:, validator: nil)
|
||||
def initialize(key:, label:, is_optional: false, validator: nil)
|
||||
@key = key.to_s
|
||||
@label = label
|
||||
@is_optional = is_optional
|
||||
@validator = validator
|
||||
end
|
||||
|
||||
def optional?
|
||||
@is_optional
|
||||
end
|
||||
|
||||
def define_validator(validator = nil, &block)
|
||||
@validator = validator || block
|
||||
end
|
||||
|
||||
7
app/models/institution.rb
Normal file
7
app/models/institution.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
class Institution < ApplicationRecord
|
||||
belongs_to :family
|
||||
has_many :accounts, dependent: :nullify
|
||||
has_one_attached :logo
|
||||
|
||||
scope :alphabetically, -> { order(name: :asc) }
|
||||
end
|
||||
@@ -1,4 +1,4 @@
|
||||
class Account::Investment < ApplicationRecord
|
||||
class Investment < ApplicationRecord
|
||||
include Accountable
|
||||
|
||||
SUBTYPES = [
|
||||
3
app/models/loan.rb
Normal file
3
app/models/loan.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
class Loan < ApplicationRecord
|
||||
include Accountable
|
||||
end
|
||||
@@ -1,5 +1,5 @@
|
||||
class Transaction::Merchant < ApplicationRecord
|
||||
has_many :transactions, dependent: :nullify
|
||||
class Merchant < ApplicationRecord
|
||||
has_many :transactions, dependent: :nullify, class_name: "Account::Transaction"
|
||||
belongs_to :family
|
||||
|
||||
validates :name, :color, :family, presence: true
|
||||
3
app/models/other_asset.rb
Normal file
3
app/models/other_asset.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
class OtherAsset < ApplicationRecord
|
||||
include Accountable
|
||||
end
|
||||
3
app/models/other_liability.rb
Normal file
3
app/models/other_liability.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
class OtherLiability < ApplicationRecord
|
||||
include Accountable
|
||||
end
|
||||
3
app/models/property.rb
Normal file
3
app/models/property.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
class Property < ApplicationRecord
|
||||
include Accountable
|
||||
end
|
||||
@@ -40,6 +40,26 @@ class Provider::Github
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_latest_releases_notes
|
||||
begin
|
||||
Rails.cache.fetch("latest_github_releases_notes", expires_in: 2.hours) do
|
||||
releases = Octokit.releases(repo)
|
||||
releases.map do |release|
|
||||
{
|
||||
avatar: release.author.avatar_url,
|
||||
name: release.name,
|
||||
published_at: release.published_at,
|
||||
body: Octokit.markdown(release.body, mode: "gfm", context: repo)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
rescue => e
|
||||
Rails.logger.error "Failed to fetch latest GitHub releases notes: #{e.message}"
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def repo
|
||||
"#{owner}/#{name}"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class Tag < ApplicationRecord
|
||||
belongs_to :family
|
||||
has_many :taggings, dependent: :destroy
|
||||
has_many :transactions, through: :taggings, source: :taggable, source_type: "Transaction"
|
||||
has_many :transactions, through: :taggings, source: :taggable, source_type: "Account::Transaction"
|
||||
|
||||
validates :name, presence: true, uniqueness: { scope: :family }
|
||||
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
class TimeSeries::Trend
|
||||
include ActiveModel::Validations
|
||||
|
||||
attr_reader :current, :previous
|
||||
|
||||
delegate :favorable_direction, to: :series
|
||||
attr_reader :current, :previous, :favorable_direction
|
||||
|
||||
validate :values_must_be_of_same_type, :values_must_be_of_known_type
|
||||
|
||||
def initialize(current:, previous:, series: nil)
|
||||
def initialize(current:, previous:, series: nil, favorable_direction: nil)
|
||||
@current = current
|
||||
@previous = previous
|
||||
@series = series
|
||||
@favorable_direction = get_favorable_direction(favorable_direction)
|
||||
|
||||
validate!
|
||||
end
|
||||
@@ -25,6 +24,17 @@ class TimeSeries::Trend
|
||||
end.inquiry
|
||||
end
|
||||
|
||||
def color
|
||||
case direction
|
||||
when "up"
|
||||
favorable_direction.down? ? red_hex : green_hex
|
||||
when "down"
|
||||
favorable_direction.down? ? green_hex : red_hex
|
||||
else
|
||||
gray_hex
|
||||
end
|
||||
end
|
||||
|
||||
def value
|
||||
if previous.nil?
|
||||
current.is_a?(Money) ? Money.new(0) : 0
|
||||
@@ -56,8 +66,21 @@ class TimeSeries::Trend
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :series
|
||||
|
||||
def red_hex
|
||||
"#F13636" # red-500
|
||||
end
|
||||
|
||||
def green_hex
|
||||
"#10A861" # green-600
|
||||
end
|
||||
|
||||
def gray_hex
|
||||
"#737373" # gray-500
|
||||
end
|
||||
|
||||
def values_must_be_of_same_type
|
||||
unless current.class == previous.class || [ previous, current ].any?(&:nil?)
|
||||
errors.add :current, "must be of the same type as previous"
|
||||
@@ -90,4 +113,9 @@ class TimeSeries::Trend
|
||||
obj
|
||||
end
|
||||
end
|
||||
|
||||
def get_favorable_direction(favorable_direction)
|
||||
direction = favorable_direction.presence || series&.favorable_direction
|
||||
(direction.presence_in(TimeSeries::DIRECTIONS) || "up").inquiry
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
class Valuation < ApplicationRecord
|
||||
include Monetizable
|
||||
|
||||
belongs_to :account
|
||||
validates :account, :date, :value, presence: true
|
||||
monetize :value
|
||||
|
||||
scope :in_period, ->(period) { period.date_range.nil? ? all : where(date: period.date_range) }
|
||||
|
||||
def self.to_series
|
||||
TimeSeries.from_collection all, :value_money
|
||||
end
|
||||
end
|
||||
3
app/models/vehicle.rb
Normal file
3
app/models/vehicle.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
class Vehicle < ApplicationRecord
|
||||
include Accountable
|
||||
end
|
||||
|
Before Width: | Height: | Size: 653 B After Width: | Height: | Size: 653 B |
1
app/views/account/transaction/rows/show.html.erb
Normal file
1
app/views/account/transaction/rows/show.html.erb
Normal file
@@ -0,0 +1 @@
|
||||
<%= render "account/transactions/transaction", transaction: @transaction %>
|
||||
@@ -1,6 +1,7 @@
|
||||
<% content_for :sidebar do %>
|
||||
<%= render "settings/nav" %>
|
||||
<% end %>
|
||||
|
||||
<div class="space-y-4">
|
||||
<h1 class="text-gray-900 text-xl font-medium mb-4">Rules</h1>
|
||||
<div class="bg-white shadow-xs border border-alpha-black-25 rounded-xl p-4">
|
||||
@@ -9,7 +10,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4">
|
||||
<%= previous_setting("Merchants", transaction_merchants_path) %>
|
||||
<%= previous_setting("Merchants", merchants_path) %>
|
||||
<%= next_setting("Imports", imports_path) %>
|
||||
</div>
|
||||
</div>
|
||||
5
app/views/account/transactions/_loading.html.erb
Normal file
5
app/views/account/transactions/_loading.html.erb
Normal file
@@ -0,0 +1,5 @@
|
||||
<div class="bg-white space-y-4 p-5 border border-alpha-black-25 rounded-xl shadow-xs">
|
||||
<div class="p-5 flex justify-center items-center">
|
||||
<%= tag.p t(".loading"), class: "text-gray-500 animate-pulse text-sm" %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -8,6 +8,22 @@
|
||||
<div class="flex items-center gap-1 text-gray-500">
|
||||
<%= turbo_frame_tag "bulk_transaction_edit_drawer" %>
|
||||
|
||||
<%= form_with url: mark_transfers_transactions_path,
|
||||
builder: ActionView::Helpers::FormBuilder,
|
||||
scope: "bulk_update",
|
||||
data: {
|
||||
turbo_frame: "_top",
|
||||
turbo_confirm: {
|
||||
title: t(".mark_transfers"),
|
||||
body: t(".mark_transfers_message"),
|
||||
accept: t(".mark_transfers_confirm"),
|
||||
}
|
||||
} do |f| %>
|
||||
<button id="bulk-transfer-btn" type="button" data-bulk-select-scope-param="bulk_update" data-action="bulk-select#submitBulkRequest" class="p-1.5 group hover:bg-gray-700 flex items-center justify-center rounded-md" title="Mark as transfer">
|
||||
<%= lucide_icon "arrow-right-left", class: "w-5 group-hover:text-white" %>
|
||||
</button>
|
||||
<% end %>
|
||||
|
||||
<%= link_to bulk_edit_transactions_path,
|
||||
class: "p-1.5 group hover:bg-gray-700 flex items-center justify-center rounded-md",
|
||||
title: "Edit",
|
||||
@@ -15,7 +31,7 @@
|
||||
<%= 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 %>
|
||||
<%= form_with url: bulk_delete_transactions_path, builder: ActionView::Helpers::FormBuilder, data: { turbo_confirm: true, turbo_frame: "_top" } 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>
|
||||
89
app/views/account/transactions/_transaction.html.erb
Normal file
89
app/views/account/transactions/_transaction.html.erb
Normal file
@@ -0,0 +1,89 @@
|
||||
<%# locals: (transaction:, selectable: true, editable: true, short: false, show_tags: false) %>
|
||||
<%= turbo_frame_tag dom_id(transaction), class: "grid grid-cols-12 items-center text-gray-900 text-sm font-medium p-4" do %>
|
||||
|
||||
<% name_col_span = transaction.transfer? ? "col-span-10" : short ? "col-span-6" : "col-span-4" %>
|
||||
<div class="pr-10 flex items-center gap-4 <%= name_col_span %>">
|
||||
<% if selectable %>
|
||||
<%= 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" } %>
|
||||
<% end %>
|
||||
|
||||
<div class="max-w-full">
|
||||
<%= content_tag :div, class: ["flex items-center gap-2"] do %>
|
||||
<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="truncate text-gray-900">
|
||||
<% if transaction.new_record? %>
|
||||
<%= content_tag :p, transaction.name %>
|
||||
<% else %>
|
||||
<%= link_to transaction.name,
|
||||
account_transaction_path(transaction.account, transaction),
|
||||
data: { turbo_frame: "drawer", turbo_prefetch: false },
|
||||
class: "hover:underline hover:text-gray-800" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if unconfirmed_transfer?(transaction) %>
|
||||
<% if editable %>
|
||||
<%= form_with url: unmark_transfers_transactions_path, builder: ActionView::Helpers::FormBuilder, class: "flex items-center", data: {
|
||||
turbo_confirm: {
|
||||
title: t(".remove_transfer"),
|
||||
body: t(".remove_transfer_body"),
|
||||
accept: t(".remove_transfer_confirm"),
|
||||
},
|
||||
turbo_frame: "_top"
|
||||
} do |f| %>
|
||||
<%= f.hidden_field "bulk_update[transaction_ids][]", value: transaction.id %>
|
||||
<%= f.button class: "flex items-center justify-center group", title: "Remove transfer" do %>
|
||||
<%= lucide_icon "arrow-left-right", class: "group-hover:hidden text-gray-500 w-4 h-4" %>
|
||||
<%= lucide_icon "unlink", class: "hidden group-hover:inline-block text-gray-900 w-4 h-4" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= lucide_icon "arrow-left-right", class: "text-gray-500 w-4 h-4" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% unless transaction.transfer? %>
|
||||
<% unless short %>
|
||||
<div class="flex items-center gap-1 <%= show_tags ? "col-span-6" : "col-span-3" %>">
|
||||
<% if editable %>
|
||||
<%= render "categories/menu", transaction: transaction %>
|
||||
<% else %>
|
||||
<%= render "categories/badge", category: transaction.category %>
|
||||
<% end %>
|
||||
|
||||
<% if show_tags %>
|
||||
<% transaction.tags.each do |tag| %>
|
||||
<%= render partial: "tags/badge", locals: { tag: tag } %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% unless show_tags %>
|
||||
<%= tag.div class: short ? "col-span-4" : "col-span-3" do %>
|
||||
<% if transaction.new_record? %>
|
||||
<%= tag.p transaction.account.name %>
|
||||
<% else %>
|
||||
<%= link_to transaction.account.name,
|
||||
account_path(transaction.account, tab: "transactions"),
|
||||
data: { turbo_frame: "_top" },
|
||||
class: "hover:underline" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<div class="col-span-2 ml-auto">
|
||||
<%= content_tag :p,
|
||||
format_money(-transaction.amount_money),
|
||||
class: ["text-green-600": transaction.inflow?] %>
|
||||
</div>
|
||||
<% end %>
|
||||
21
app/views/account/transactions/_transaction_group.html.erb
Normal file
21
app/views/account/transactions/_transaction_group.html.erb
Normal file
@@ -0,0 +1,21 @@
|
||||
<%# locals: (date:, transactions:, transfers: [], selectable: true, **transaction_opts) %>
|
||||
<div id="date-group-<%= date %>" 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">
|
||||
<% if selectable %>
|
||||
<%= check_box_tag "#{date}_transactions_selection",
|
||||
class: ["maybe-checkbox maybe-checkbox--light", "hidden": transactions.count == 0],
|
||||
id: "selection_transaction_#{date}",
|
||||
data: { action: "bulk-select#toggleGroupSelection" } %>
|
||||
<% end %>
|
||||
|
||||
<%= tag.span "#{date.strftime('%b %d, %Y')} · #{transactions.size + (transfers.size * 2)}" %>
|
||||
</div>
|
||||
|
||||
<%= totals_by_currency(collection: transactions, money_method: :amount_money, negate: true) %>
|
||||
</div>
|
||||
<div class="bg-white shadow-xs rounded-md border border-alpha-black-25 divide-y divide-alpha-black-50">
|
||||
<%= render transactions, selectable:, **transaction_opts.except(:selectable) %>
|
||||
<%= render transfers %>
|
||||
</div>
|
||||
</div>
|
||||
29
app/views/account/transactions/index.html.erb
Normal file
29
app/views/account/transactions/index.html.erb
Normal file
@@ -0,0 +1,29 @@
|
||||
<%= turbo_frame_tag dom_id(@account, "transactions") do %>
|
||||
<div class="bg-white space-y-4 p-5 border border-alpha-black-25 rounded-xl shadow-xs">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="font-medium text-lg"><%= t(".transactions") %></h3>
|
||||
<%= link_to new_transaction_path(account_id: @account),
|
||||
class: "flex gap-1 font-medium items-center bg-gray-50 text-gray-900 p-2 rounded-lg",
|
||||
data: { turbo_frame: :modal } do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5 text-gray-900") %>
|
||||
<span class="text-sm"><%= t(".new") %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div id="transactions" data-controller="bulk-select" data-bulk-select-resource-value="<%= t(".transaction") %>">
|
||||
<div hidden id="transaction-selection-bar" data-bulk-select-target="selectionBar">
|
||||
<%= render "account/transactions/selection_bar" %>
|
||||
</div>
|
||||
|
||||
<% if @transactions.empty? %>
|
||||
<p class="text-gray-500 py-4"><%= t(".no_transactions") %></p>
|
||||
<% else %>
|
||||
<div class="space-y-6">
|
||||
<% group_transactions_by_date(@transactions).each do |date, group| %>
|
||||
<%= render "transaction_group", date:, transactions: group[:transactions], transfers: group[:transfers] %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
103
app/views/account/transactions/show.html.erb
Normal file
103
app/views/account/transactions/show.html.erb
Normal file
@@ -0,0 +1,103 @@
|
||||
<%= drawer do %>
|
||||
<div>
|
||||
<header class="mb-4 space-y-1">
|
||||
<div class="flex items-center gap-4">
|
||||
<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>
|
||||
|
||||
<% if @transaction.marked_as_transfer %>
|
||||
<%= lucide_icon "arrow-left-right", class: "text-gray-500 mt-1 w-5 h-5" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<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: [@account, @transaction], url: account_transaction_path, html: { data: { controller: "auto-submit-form" } } do |f| %>
|
||||
<div class="space-y-2">
|
||||
<%= f.text_field :name, label: t(".name_label"), "data-auto-submit-form-target": "auto" %>
|
||||
<%= f.date_field :date, label: t(".date_label"), max: Date.today, "data-auto-submit-form-target": "auto" %>
|
||||
|
||||
<% unless @transaction.marked_as_transfer %>
|
||||
<%= f.collection_select :category_id, Current.family.categories.alphabetically, :id, :name, { prompt: t(".category_placeholder"), label: t(".category_label"), class: "text-gray-400" }, "data-auto-submit-form-target": "auto" %>
|
||||
<%= f.collection_select :merchant_id, Current.family.merchants.alphabetically, :id, :name, { prompt: t(".merchant_placeholder"), label: t(".merchant_label"), class: "text-gray-400" }, "data-auto-submit-form-target": "auto" %>
|
||||
<% end %>
|
||||
|
||||
<%= f.collection_select :account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".account_placeholder"), label: t(".account_label"), class: "text-gray-500" }, { class: "form-field__input cursor-not-allowed text-gray-400", disabled: "disabled" } %>
|
||||
</div>
|
||||
<% 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: [@account, @transaction], url: account_transaction_path, 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(".tags_label"),
|
||||
class: "placeholder:text-gray-500"
|
||||
},
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
<%= f.text_area :notes, label: t(".note_label"), placeholder: t(".note_placeholder"), "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: [@account, @transaction], url: account_transaction_path, html: { class: "p-3 space-y-3", data: { controller: "auto-submit-form" } } do |f| %>
|
||||
<div class="flex cursor-pointer items-center gap-2 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="account_transaction_excluded" class="maybe-switch"></label>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% unless @transaction.transfer? %>
|
||||
<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"),
|
||||
account_transaction_path(@account, @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>
|
||||
<% end %>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
32
app/views/account/transfers/_form.html.erb
Normal file
32
app/views/account/transfers/_form.html.erb
Normal file
@@ -0,0 +1,32 @@
|
||||
<%= form_with model: transfer, data: { turbo_frame: "_top" } do |f| %>
|
||||
<section>
|
||||
<fieldset class="bg-gray-50 rounded-lg p-1 grid grid-flow-col justify-stretch gap-x-2">
|
||||
<%= link_to new_transaction_path(nature: "expense"), data: { turbo_frame: :modal }, class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-gray-400" do %>
|
||||
<%= lucide_icon "minus-circle", class: "w-5 h-5" %>
|
||||
<%= tag.span t(".expense") %>
|
||||
<% end %>
|
||||
|
||||
<%= link_to new_transaction_path(nature: "income"), data: { turbo_frame: :modal }, class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-gray-400" do %>
|
||||
<%= lucide_icon "plus-circle", class: "w-5 h-5" %>
|
||||
<%= tag.span t(".income") %>
|
||||
<% end %>
|
||||
|
||||
<%= tag.div class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-gray-400 bg-white text-gray-800 shadow-sm" do %>
|
||||
<%= lucide_icon "arrow-right-left", class: "w-5 h-5" %>
|
||||
<%= tag.span t(".transfer") %>
|
||||
<% end %>
|
||||
</fieldset>
|
||||
</section>
|
||||
|
||||
<section class="space-y-2">
|
||||
<%= f.text_field :name, value: transfer.transactions.first&.name, label: t(".description"), placeholder: t(".description_placeholder"), required: true %>
|
||||
<%= f.collection_select :from_account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".select_account"), label: t(".from") }, required: true %>
|
||||
<%= f.collection_select :to_account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".select_account"), label: t(".to") }, required: true %>
|
||||
<%= f.money_field :amount_money, label: t(".amount"), required: true %>
|
||||
<%= f.date_field :date, value: transfer.transactions.first&.date, label: t(".date"), required: true, max: Date.current %>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<%= f.submit t(".submit") %>
|
||||
</section>
|
||||
<% end %>
|
||||
34
app/views/account/transfers/_transfer.html.erb
Normal file
34
app/views/account/transfers/_transfer.html.erb
Normal file
@@ -0,0 +1,34 @@
|
||||
<%= turbo_frame_tag dom_id(transfer), class: "block" do %>
|
||||
<details class="group flex items-center text-gray-900 text-sm font-medium">
|
||||
<summary class="flex items-center justify-between p-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<%= button_to account_transfer_path(transfer),
|
||||
method: :delete,
|
||||
class: "flex items-center group/transfer",
|
||||
data: {
|
||||
turbo_frame: "_top",
|
||||
turbo_confirm: {
|
||||
title: t(".remove_title"),
|
||||
body: t(".remove_body"),
|
||||
confirm: t(".remove_confirm")
|
||||
}
|
||||
} do %>
|
||||
<%= lucide_icon "arrow-left-right", class: "group-hover/transfer:hidden w-5 h-5 text-gray-500" %>
|
||||
<%= lucide_icon "unlink", class: "group-hover/transfer:inline-block hidden w-5 h-5 text-gray-500" %>
|
||||
<% end %>
|
||||
|
||||
<div class="max-w-full pr-10 select-none">
|
||||
<%= tag.p t(".transfer_name", from_account: transfer.outflow_transaction&.account&.name, to_account: transfer.inflow_transaction&.account&.name) %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-gray-500 w-5" %>
|
||||
</summary>
|
||||
|
||||
<div class="divide-y divide-alpha-black-200">
|
||||
<% transfer.transactions.each do |transaction| %>
|
||||
<%= render transaction, selectable: false, editable: false %>
|
||||
<% end %>
|
||||
</div>
|
||||
</details>
|
||||
<% end %>
|
||||
17
app/views/account/transfers/new.html.erb
Normal file
17
app/views/account/transfers/new.html.erb
Normal file
@@ -0,0 +1,17 @@
|
||||
<%= modal do %>
|
||||
<article class="mx-auto p-4 space-y-4 w-screen max-w-xl">
|
||||
<header class="flex justify-between">
|
||||
<%= tag.h2 t(".title"), class: "font-medium text-xl" %>
|
||||
<%= lucide_icon "x", class: "w-5 h-5 text-gray-500", data: { action: "click->modal#close" } %>
|
||||
</header>
|
||||
|
||||
<% if @transfer.errors.present? %>
|
||||
<div class="text-red-600 flex items-center gap-2">
|
||||
<%= lucide_icon "circle-alert", class: "w-5 h-5" %>
|
||||
<p class="text-sm"><%= @transfer.errors.full_messages.to_sentence %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= render "form", transfer: @transfer %>
|
||||
</article>
|
||||
<% end %>
|
||||
23
app/views/account/valuations/_form.html.erb
Normal file
23
app/views/account/valuations/_form.html.erb
Normal file
@@ -0,0 +1,23 @@
|
||||
<%# locals: (valuation:) %>
|
||||
<%= form_with model: valuation,
|
||||
data: { turbo_frame: "_top" },
|
||||
url: valuation.new_record? ? account_valuations_path(valuation.account) : account_valuation_path(valuation.account, valuation),
|
||||
builder: ActionView::Helpers::FormBuilder do |f| %>
|
||||
<div class="grid grid-cols-10 p-4 items-center">
|
||||
<div class="col-span-7 flex items-center gap-4">
|
||||
<div class="w-8 h-8 rounded-full p-1.5 flex items-center justify-center bg-gray-500/5">
|
||||
<%= lucide_icon("pencil-line", class: "w-4 h-4 text-gray-500") %>
|
||||
</div>
|
||||
<div class="w-full flex items-center justify-between gap-2">
|
||||
<%= f.date_field :date, required: "required", max: Date.today, class: "border border-alpha-black-200 bg-white rounded-lg shadow-xs min-w-[200px] px-3 py-1.5 text-gray-900 text-sm" %>
|
||||
<%= f.number_field :value, required: "required", placeholder: "0.00", step: "0.01", class: "bg-white border border-alpha-black-200 rounded-lg shadow-xs text-gray-900 text-sm px-3 py-1.5 text-right" %>
|
||||
<%= f.hidden_field :currency, value: valuation.account.currency %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-3 flex gap-2 justify-end items-center">
|
||||
<%= link_to t(".cancel"), account_valuations_path(valuation.account), class: "text-sm text-gray-900 hover:text-gray-800 font-medium px-3 py-1.5" %>
|
||||
<%= f.submit class: "bg-gray-50 rounded-lg font-medium px-3 py-1.5 cursor-pointer hover:bg-gray-100 text-sm" %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
5
app/views/account/valuations/_loading.html.erb
Normal file
5
app/views/account/valuations/_loading.html.erb
Normal file
@@ -0,0 +1,5 @@
|
||||
<div class="bg-white space-y-4 p-5 border border-alpha-black-25 rounded-xl shadow-xs">
|
||||
<div class="p-5 flex justify-center items-center">
|
||||
<%= tag.p t(".loading"), class: "text-gray-500 animate-pulse text-sm" %>
|
||||
</div>
|
||||
</div>
|
||||
44
app/views/account/valuations/_valuation.html.erb
Normal file
44
app/views/account/valuations/_valuation.html.erb
Normal file
@@ -0,0 +1,44 @@
|
||||
<%= turbo_frame_tag dom_id(valuation) do %>
|
||||
<div class="p-4 grid grid-cols-10 items-center">
|
||||
<div class="col-span-5 flex items-center gap-4">
|
||||
<%= tag.div class: "w-8 h-8 rounded-full p-1.5 flex items-center justify-center", style: valuation_style(valuation).html_safe do %>
|
||||
<%= lucide_icon valuation_icon(valuation), class: "w-4 h-4" %>
|
||||
<% end %>
|
||||
|
||||
<div class="text-sm">
|
||||
<%= tag.p valuation.date, class: "text-gray-900 font-medium" %>
|
||||
<%= tag.p valuation.oldest? ? t(".start_balance") : t(".value_update"), class: "text-gray-500" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 justify-self-end">
|
||||
<%= tag.p format_money(valuation.value_money), class: "font-medium text-sm text-gray-900" %>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 justify-self-end font-medium text-sm" style="color: <%= valuation.trend.color %>">
|
||||
<% if valuation.trend.direction.flat? %>
|
||||
<%= tag.span t(".no_change"), class: "text-gray-500" %>
|
||||
<% else %>
|
||||
<%= tag.span format_money(valuation.trend.value) %>
|
||||
<%= tag.span "(#{valuation.trend.percent}%)" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="col-span-1 justify-self-end">
|
||||
<%= 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">
|
||||
<%= contextual_menu_modal_action_item t(".edit_entry"), edit_account_valuation_path(valuation.account, valuation) %>
|
||||
|
||||
<%= contextual_menu_destructive_item t(".delete_entry"),
|
||||
account_valuation_path(valuation.account, valuation),
|
||||
turbo_frame: "_top",
|
||||
turbo_confirm: {
|
||||
title: t(".confirm_title"),
|
||||
body: t(".confirm_body_html"),
|
||||
accept: t(".confirm_accept")
|
||||
} %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
1
app/views/account/valuations/_valuation_ruler.html.erb
Normal file
1
app/views/account/valuations/_valuation_ruler.html.erb
Normal file
@@ -0,0 +1 @@
|
||||
<div class="h-px bg-alpha-black-50 ml-16 mr-4"></div>
|
||||
3
app/views/account/valuations/edit.html.erb
Normal file
3
app/views/account/valuations/edit.html.erb
Normal file
@@ -0,0 +1,3 @@
|
||||
<%= turbo_frame_tag dom_id(@valuation) do %>
|
||||
<%= render "form", valuation: @valuation %>
|
||||
<% end %>
|
||||
35
app/views/account/valuations/index.html.erb
Normal file
35
app/views/account/valuations/index.html.erb
Normal file
@@ -0,0 +1,35 @@
|
||||
<%= turbo_frame_tag dom_id(@account, "valuations") do %>
|
||||
<div class="bg-white space-y-4 p-5 border border-alpha-black-25 rounded-xl shadow-xs">
|
||||
<div class="flex items-center justify-between">
|
||||
<%= tag.h2 t(".valuations"), class: "font-medium text-lg" %>
|
||||
<%= link_to new_account_valuation_path(@account),
|
||||
data: { turbo_frame: dom_id(Account::Valuation.new) },
|
||||
class: "flex gap-1 font-medium items-center bg-gray-50 text-gray-900 p-2 rounded-lg" do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5 text-gray-900") %>
|
||||
<%= tag.span t(".new_entry"), class: "text-sm" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl bg-gray-25 p-1">
|
||||
<div class="grid grid-cols-10 items-center uppercase text-xs font-medium text-gray-500 px-4 py-2">
|
||||
<%= tag.p t(".date"), class: "col-span-5" %>
|
||||
<%= tag.p t(".value"), class: "col-span-2 justify-self-end" %>
|
||||
<%= tag.p t(".change"), class: "col-span-2 justify-self-end" %>
|
||||
<%= tag.div class: "col-span-1" %>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg bg-white border-alpha-black-25 shadow-xs">
|
||||
<%= turbo_frame_tag dom_id(Account::Valuation.new) %>
|
||||
|
||||
<% valuations = @account.valuations.reverse_chronological %>
|
||||
<% if valuations.any? %>
|
||||
<%= render partial: "valuation",
|
||||
collection: @account.valuations.reverse_chronological,
|
||||
spacer_template: "valuation_ruler" %>
|
||||
<% else %>
|
||||
<p class="text-gray-500 text-sm p-4"><%= t(".no_valuations") %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
4
app/views/account/valuations/new.html.erb
Normal file
4
app/views/account/valuations/new.html.erb
Normal file
@@ -0,0 +1,4 @@
|
||||
<%= turbo_frame_tag dom_id(@valuation) do %>
|
||||
<%= render "form", valuation: @valuation %>
|
||||
<div class="h-px bg-alpha-black-50 ml-20 mr-4"></div>
|
||||
<% end %>
|
||||
1
app/views/account/valuations/show.html.erb
Normal file
1
app/views/account/valuations/show.html.erb
Normal file
@@ -0,0 +1 @@
|
||||
<%= render "valuation", valuation: @valuation %>
|
||||
@@ -1,21 +1,27 @@
|
||||
<%= turbo_frame_tag dom_id(account) do %>
|
||||
<div class="p-4 flex items-center justify-between gap-3">
|
||||
<div class="p-4 flex items-center justify-between gap-3 group/account">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 flex items-center justify-center rounded-full text-xs font-medium <%= account.is_active ? "bg-blue-500/10 text-blue-500" : "bg-gray-500/10 text-gray-500" %>">
|
||||
<%= account.name[0].upcase %>
|
||||
</div>
|
||||
<p class="text-sm font-medium <%= account.is_active ? "text-gray-900" : "text-gray-400" %>">
|
||||
<%= account.name %>
|
||||
</p>
|
||||
<%= link_to account.name, account, class: [(account.is_active ? "text-gray-900" : "text-gray-400"), "text-sm font-medium hover:underline"], data: { turbo_frame: "_top" } %>
|
||||
|
||||
<%= link_to edit_account_path(account), data: { turbo_frame: :modal }, class: "group-hover/account:flex hidden hover:opacity-80 items-center justify-center" do %>
|
||||
<%= lucide_icon "pencil-line", class: "w-4 h-4 text-gray-500" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex items-center gap-8">
|
||||
<p class="text-sm font-medium <%= account.is_active ? "text-gray-900" : "text-gray-400" %>">
|
||||
<%= format_money account.balance_money %>
|
||||
</p>
|
||||
<%= form_with model: account, method: :patch, html: { class: "flex items-center", data: { turbo_frame: "_top" } } do |form| %>
|
||||
|
||||
<%= form_with model: account,
|
||||
namespace: account.id,
|
||||
builder: ActionView::Helpers::FormBuilder,
|
||||
data: { controller: "auto-submit-form", turbo_frame: "_top" } do |form| %>
|
||||
<div class="relative inline-block select-none">
|
||||
<%= form.check_box :is_active, class: "sr-only peer", id: "is_active_#{account.id}", onchange: "this.form.requestSubmit();" %>
|
||||
<label for="is_active_<%= account.id %>" class="block bg-gray-100 w-9 h-5 rounded-full cursor-pointer after:content-[''] after:block after:absolute after:top-0.5 after:left-0.5 after:bg-white after:w-4 after:h-4 after:rounded-full after:transition-transform after:duration-300 after:ease-in-out peer-checked:bg-green-600 peer-checked:after:translate-x-4"></label>
|
||||
<%= form.check_box :is_active, { class: "sr-only peer", data: { "auto-submit-form-target": "auto" } } %>
|
||||
<%= form.label :is_active, " ".html_safe, class: "maybe-switch" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
<%# locals: (account:, valuations:) %>
|
||||
<div class="bg-white space-y-4 p-5 border border-alpha-black-25 rounded-xl shadow-xs">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="font-medium text-lg">History</h3>
|
||||
<%= link_to new_account_valuation_path(account), data: { turbo_frame: dom_id(Valuation.new) }, class: "flex gap-1 font-medium items-center bg-gray-50 text-gray-900 p-2 rounded-lg" do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5 text-gray-900") %>
|
||||
<span class="text-sm">New entry</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="rounded-xl bg-gray-25 p-1">
|
||||
<div class="flex flex-col rounded-lg space-y-1">
|
||||
<div class="text-xs font-medium text-gray-500 uppercase flex items-center px-4 py-2">
|
||||
<div class="w-16">date</div>
|
||||
<div class="flex items-center justify-between grow">
|
||||
<div></div>
|
||||
<div>value</div>
|
||||
</div>
|
||||
<div class="w-56 text-right">change</div>
|
||||
<div class="w-[72px]"></div>
|
||||
</div>
|
||||
<div class="rounded-lg bg-white border-alpha-black-25 shadow-xs">
|
||||
<%= turbo_frame_tag dom_id(Valuation.new) %>
|
||||
<%= turbo_frame_tag "valuations_list" do %>
|
||||
<%= render partial: "accounts/account_valuation_list", locals: { valuation_series: valuations } %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2,7 +2,7 @@
|
||||
<% type = Accountable.from_type(group.name) %>
|
||||
<% if group %>
|
||||
<details class="mb-1 text-sm group" data-controller="account-collapse" data-account-collapse-type-value="<%= type %>">
|
||||
<summary class="flex gap-4 px-3 py-2 items-center w-full rounded-[10px] font-medium hover:bg-gray-100">
|
||||
<summary class="flex gap-4 px-3 py-2 items-center w-full rounded-[10px] font-medium hover:bg-gray-100 cursor-pointer">
|
||||
<%= 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 class="text-left"><%= type.model_name.human %></div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<%= link_to new_account_path(step: "method", type: type.class.name.demodulize), class: "flex items-center gap-4 w-full text-center focus:outline-none focus:bg-gray-25 border border-transparent focus:border focus:border-gray-200 block px-2 hover:bg-gray-25 rounded-lg p-2" do %>
|
||||
<%= link_to new_account_path(step: "method", type: type.class.name.demodulize, institution_id: params[:institution_id]), class: "flex items-center gap-4 w-full text-center focus:outline-none focus:bg-gray-25 border border-transparent focus:border focus:border-gray-200 block px-2 hover:bg-gray-25 rounded-lg p-2" do %>
|
||||
<span class="flex w-8 h-8 shrink-0 grow-0 items-center justify-center rounded-lg <%= bg_color %> border border-alpha-black-25">
|
||||
<%= lucide_icon(icon, class: "#{text_color} w-5 h-5") %>
|
||||
</span>
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
<%# locals: (valuation_series:) %>
|
||||
<% valuation_series.values.reverse_each.with_index do |valuation, index| %>
|
||||
<% valuation_styles = trend_styles(valuation.trend) %>
|
||||
<%= turbo_frame_tag dom_id(valuation.original) do %>
|
||||
<div class="p-4 flex items-center">
|
||||
<div class="w-16">
|
||||
<div class="w-8 h-8 rounded-full p-1.5 flex items-center justify-center <%= valuation_styles[:bg_class] %>">
|
||||
<%= lucide_icon(valuation_styles[:icon], class: "w-4 h-4 #{valuation_styles[:text_class]}") %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between grow">
|
||||
<div class="text-sm">
|
||||
<p><%= valuation.date %></p>
|
||||
<%# TODO: Add descriptive name of valuation %>
|
||||
<p class="text-gray-500">Manually entered</p>
|
||||
</div>
|
||||
<div class="flex text-sm font-medium text-right"><%= format_money valuation.value %></div>
|
||||
</div>
|
||||
<div class="flex w-56 justify-end text-right text-sm font-medium">
|
||||
<% if valuation.trend.value == 0 %>
|
||||
<span class="text-gray-500">No change</span>
|
||||
<% else %>
|
||||
<span class="<%= valuation_styles[:text_class] %>"><%= valuation_styles[:symbol] %><%= format_money valuation.trend.value.abs %></span>
|
||||
<span class="<%= valuation_styles[:text_class] %>">(<%= lucide_icon(valuation_styles[:icon], class: "w-4 h-4 align-text-bottom inline") %> <%= valuation.trend.percent %>%)</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="relative w-[72px]" data-controller="menu">
|
||||
<button
|
||||
data-menu-target="button"
|
||||
class="ml-auto flex items-center justify-center hover:bg-gray-50 w-8 h-8 rounded-lg">
|
||||
<%= lucide_icon("more-horizontal", class: "w-5 h-5 text-gray-500") %>
|
||||
</button>
|
||||
<div
|
||||
data-menu-target="content"
|
||||
class="absolute min-w-[200px] z-10 top-10 right-0 bg-white p-1 rounded-sm shadow-xs border border-alpha-black-25 w-fit">
|
||||
<%= link_to edit_valuation_path(valuation.original),
|
||||
class: "flex gap-1 items-center hover:bg-gray-50 rounded-md p-2" do %>
|
||||
<%= lucide_icon("pencil-line", class: "w-5 h-5 text-gray-500 shrink-0") %>
|
||||
<span class="text-gray-900 text-sm">Edit entry</span>
|
||||
<% end %>
|
||||
<%= link_to valuation_path(valuation.original),
|
||||
data: { turbo_method: :delete,
|
||||
turbo_confirm: { title: t(".confirm_title"),
|
||||
body: t(".confirm_body_html"),
|
||||
accept: t(".confirm_accept") } },
|
||||
class: "text-red-600 flex gap-1 items-center hover:bg-gray-50 rounded-md p-2" do %>
|
||||
<%= lucide_icon("trash-2", class: "w-5 h-5 shrink-0") %>
|
||||
<span class="text-sm">Delete entry</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% unless index == valuation_series.values.size - 1 %>
|
||||
<div class="h-px bg-alpha-black-50 ml-20 mr-4"></div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user