Compare commits
59 Commits
v0.1.0-alp
...
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9f11e002a | ||
|
|
c744237b55 | ||
|
|
7dfd7408c7 | ||
|
|
8f8988c03a | ||
|
|
a21061fb56 | ||
|
|
c5bf1db230 | ||
|
|
3610c6cae7 | ||
|
|
79ca7e2039 | ||
|
|
34ebd96c4c | ||
|
|
3399b74849 | ||
|
|
77fc5caecf | ||
|
|
a20809eee3 | ||
|
|
cd9f20747c | ||
|
|
1746533842 | ||
|
|
6b46831199 | ||
|
|
aa16807c6c | ||
|
|
dce9adb534 | ||
|
|
26bd655e4c | ||
|
|
5c7d2f2b01 | ||
|
|
90278630ed | ||
|
|
977da34efc | ||
|
|
6288139a41 | ||
|
|
ff5408c131 | ||
|
|
0a303ccbd5 | ||
|
|
a2ab217925 | ||
|
|
b4d0fdbe0d | ||
|
|
4bfe47540d | ||
|
|
f5cb13b42f | ||
|
|
3893060f8e | ||
|
|
54596d51f7 | ||
|
|
7758f51be9 | ||
|
|
40c09279f3 | ||
|
|
ad52207a25 | ||
|
|
a33ba11ce9 | ||
|
|
47a43a888c | ||
|
|
0afab5296c | ||
|
|
0d7164af9b | ||
|
|
597079dc8d | ||
|
|
fc91a34691 | ||
|
|
fd941d714d | ||
|
|
9263dd3bbe | ||
|
|
31f3ff6a16 | ||
|
|
41dff228e8 | ||
|
|
78b0674052 | ||
|
|
3461182725 | ||
|
|
59e4eff24a | ||
|
|
e70d3d1902 | ||
|
|
2f6479f058 | ||
|
|
ffd54e4065 | ||
|
|
591d149da9 | ||
|
|
c397f1bd2b | ||
|
|
d2a6ab1e45 | ||
|
|
5e3a3b0b38 | ||
|
|
563db0f8eb | ||
|
|
2dda598e8a | ||
|
|
388f8e4197 | ||
|
|
1d56c67b4f | ||
|
|
f6619aa4e5 | ||
|
|
9453313f68 |
24
.env.example
24
.env.example
@@ -1,6 +1,6 @@
|
||||
# Custom port config
|
||||
# For users who have other applications listening at 3000, this allows them to set a value puma will listen to.
|
||||
PORT=
|
||||
PORT=3000
|
||||
|
||||
# Exchange Rate API
|
||||
# This is used to convert between different currencies in the app. We use Synth, which is a Maybe product. You can sign up for a free account at synthfinance.com.
|
||||
@@ -15,7 +15,7 @@ SMTP_USERNAME=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_TLS_ENABLED=true
|
||||
|
||||
# Email Configuration
|
||||
# Address that emails are sent from
|
||||
EMAIL_SENDER=
|
||||
|
||||
# Database Configuration
|
||||
@@ -36,8 +36,8 @@ SENTRY_DSN=
|
||||
# This is useful for controlling who can sign up for your Maybe instance.
|
||||
REQUIRE_INVITE_CODE=false
|
||||
|
||||
# Enables self hosting features
|
||||
SELF_HOSTING_ENABLED=false
|
||||
# Enables self hosting features (should be set to true for most folks)
|
||||
SELF_HOSTED=true
|
||||
|
||||
# The hosting platform used to deploy the app (e.g. "render")
|
||||
# `localhost` (or unset) is used for local development and testing
|
||||
@@ -86,3 +86,19 @@ GITHUB_REPO_BRANCH=main
|
||||
# S3_SECRET_ACCESS_KEY=
|
||||
# S3_REGION= # defaults to `us-east-1` if not set
|
||||
# S3_BUCKET=
|
||||
#
|
||||
# Cloudflare R2
|
||||
# =============
|
||||
# ACTIVE_STORAGE_SERVICE=cloudflare
|
||||
# CLOUDFLARE_ACCOUNT_ID=
|
||||
# CLOUDFLARE_ACCESS_KEY_ID=
|
||||
# CLOUDFLARE_SECRET_ACCESS_KEY=
|
||||
# CLOUDFLARE_BUCKET=
|
||||
|
||||
# ======================================================================================================
|
||||
# Billing Module - responsible for handling billing
|
||||
# ======================================================================================================
|
||||
#
|
||||
STRIPE_PUBLISHABLE_KEY=
|
||||
STRIPE_SECRET_KEY=
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -52,9 +52,6 @@ jobs:
|
||||
- name: Lint code for consistent style
|
||||
run: bin/rubocop -f github
|
||||
|
||||
- name: Lint templates for consistent style
|
||||
run: ./bin/erblint ./app/**/*.erb
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
4
Gemfile
4
Gemfile
@@ -38,6 +38,7 @@ gem "image_processing", ">= 1.2"
|
||||
gem "bcrypt", "~> 3.1"
|
||||
gem "faraday"
|
||||
gem "faraday-retry"
|
||||
gem "faraday-multipart"
|
||||
gem "inline_svg"
|
||||
gem "octokit"
|
||||
gem "pagy"
|
||||
@@ -45,6 +46,9 @@ gem "rails-settings-cached"
|
||||
gem "tzinfo-data", platforms: %i[windows jruby]
|
||||
gem "csv"
|
||||
gem "redcarpet"
|
||||
gem "stripe"
|
||||
gem "intercom-rails"
|
||||
gem "holidays"
|
||||
|
||||
group :development, :test do
|
||||
gem "debug", platforms: %i[mri windows]
|
||||
|
||||
61
Gemfile.lock
61
Gemfile.lock
@@ -1,6 +1,6 @@
|
||||
GIT
|
||||
remote: https://github.com/maybe-finance/lucide-rails.git
|
||||
revision: 79d989593ee4ac6c50106ec5e4d2bd4ec8f5af87
|
||||
revision: 272e5fb8418ea458da3995d6abe0ba0ceee9c9f0
|
||||
specs:
|
||||
lucide-rails (0.2.0)
|
||||
railties (>= 4.1.0)
|
||||
@@ -78,11 +78,11 @@ GEM
|
||||
minitest (>= 5.1)
|
||||
securerandom (>= 0.3)
|
||||
tzinfo (~> 2.0, >= 2.0.5)
|
||||
addressable (2.8.6)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
ast (2.4.2)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.981.0)
|
||||
aws-partitions (1.985.0)
|
||||
aws-sdk-core (3.209.1)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
@@ -91,7 +91,7 @@ GEM
|
||||
aws-sdk-kms (1.94.0)
|
||||
aws-sdk-core (~> 3, >= 3.207.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.166.0)
|
||||
aws-sdk-s3 (1.167.0)
|
||||
aws-sdk-core (~> 3, >= 3.207.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
@@ -157,6 +157,8 @@ GEM
|
||||
faraday-net_http (>= 2.0, < 3.4)
|
||||
json
|
||||
logger
|
||||
faraday-multipart (1.0.4)
|
||||
multipart-post (~> 2)
|
||||
faraday-net_http (3.3.0)
|
||||
net-http
|
||||
faraday-retry (2.2.1)
|
||||
@@ -179,8 +181,9 @@ GEM
|
||||
fugit (>= 1.11.0)
|
||||
railties (>= 6.1.0)
|
||||
thor (>= 1.0.0)
|
||||
hashdiff (1.1.0)
|
||||
hashdiff (1.1.1)
|
||||
highline (3.0.1)
|
||||
holidays (8.8.0)
|
||||
hotwire-livereload (1.4.1)
|
||||
actioncable (>= 6.0.0)
|
||||
listen (>= 3.0.0)
|
||||
@@ -200,13 +203,15 @@ GEM
|
||||
image_processing (1.13.0)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
importmap-rails (2.0.1)
|
||||
importmap-rails (2.0.2)
|
||||
actionpack (>= 6.0.0)
|
||||
activesupport (>= 6.0.0)
|
||||
railties (>= 6.0.0)
|
||||
inline_svg (1.10.0)
|
||||
activesupport (>= 3.0)
|
||||
nokogiri (>= 1.6)
|
||||
intercom-rails (1.0.1)
|
||||
activesupport (> 4.0)
|
||||
io-console (0.7.2)
|
||||
irb (1.14.1)
|
||||
rdoc (>= 4.0.0)
|
||||
@@ -239,6 +244,7 @@ GEM
|
||||
mocha (2.4.5)
|
||||
ruby2_keywords (>= 0.0.5)
|
||||
msgpack (1.7.2)
|
||||
multipart-post (2.4.1)
|
||||
net-http (0.4.1)
|
||||
uri
|
||||
net-imap (0.4.14)
|
||||
@@ -266,21 +272,21 @@ GEM
|
||||
octokit (9.1.0)
|
||||
faraday (>= 1, < 3)
|
||||
sawyer (~> 0.9)
|
||||
pagy (9.0.9)
|
||||
pagy (9.1.0)
|
||||
parallel (1.25.1)
|
||||
parser (3.3.4.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
pg (1.5.8)
|
||||
prism (1.0.0)
|
||||
propshaft (1.0.1)
|
||||
prism (1.1.0)
|
||||
propshaft (1.1.0)
|
||||
actionpack (>= 7.0.0)
|
||||
activesupport (>= 7.0.0)
|
||||
rack
|
||||
railties (>= 7.0.0)
|
||||
psych (5.1.2)
|
||||
stringio
|
||||
public_suffix (5.1.0)
|
||||
public_suffix (6.0.1)
|
||||
puma (6.4.3)
|
||||
nio4r (~> 2.0)
|
||||
raabro (1.4.0)
|
||||
@@ -333,7 +339,7 @@ GEM
|
||||
rb-fsevent (0.11.2)
|
||||
rb-inotify (0.11.1)
|
||||
ffi (~> 1.0)
|
||||
rbs (3.5.3)
|
||||
rbs (3.6.1)
|
||||
logger
|
||||
rdoc (6.7.0)
|
||||
psych (>= 4.0.0)
|
||||
@@ -341,7 +347,7 @@ GEM
|
||||
regexp_parser (2.9.2)
|
||||
reline (0.5.10)
|
||||
io-console (~> 0.5)
|
||||
rexml (3.3.7)
|
||||
rexml (3.3.8)
|
||||
rubocop (1.65.1)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
@@ -371,13 +377,13 @@ GEM
|
||||
rubocop-minitest
|
||||
rubocop-performance
|
||||
rubocop-rails
|
||||
ruby-lsp (0.18.1)
|
||||
ruby-lsp (0.19.1)
|
||||
language_server-protocol (~> 3.17.0)
|
||||
prism (~> 1.0)
|
||||
prism (>= 1.1, < 2.0)
|
||||
rbs (>= 3, < 4)
|
||||
sorbet-runtime (>= 0.5.10782)
|
||||
ruby-lsp-rails (0.3.16)
|
||||
ruby-lsp (>= 0.18.0, < 0.19.0)
|
||||
ruby-lsp-rails (0.3.18)
|
||||
ruby-lsp (>= 0.19.0, < 0.20.0)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-vips (2.2.2)
|
||||
ffi (~> 1.12)
|
||||
@@ -407,22 +413,23 @@ GEM
|
||||
simplecov-html (0.12.3)
|
||||
simplecov_json_formatter (0.1.4)
|
||||
smart_properties (1.17.0)
|
||||
sorbet-runtime (0.5.11577)
|
||||
sorbet-runtime (0.5.11597)
|
||||
stackprof (0.2.26)
|
||||
stimulus-rails (1.3.4)
|
||||
railties (>= 6.0.0)
|
||||
stringio (3.1.1)
|
||||
tailwindcss-rails (2.7.6)
|
||||
stripe (13.0.0)
|
||||
tailwindcss-rails (2.7.7)
|
||||
railties (>= 7.0.0)
|
||||
tailwindcss-rails (2.7.6-aarch64-linux)
|
||||
tailwindcss-rails (2.7.7-aarch64-linux)
|
||||
railties (>= 7.0.0)
|
||||
tailwindcss-rails (2.7.6-arm-linux)
|
||||
tailwindcss-rails (2.7.7-arm-linux)
|
||||
railties (>= 7.0.0)
|
||||
tailwindcss-rails (2.7.6-arm64-darwin)
|
||||
tailwindcss-rails (2.7.7-arm64-darwin)
|
||||
railties (>= 7.0.0)
|
||||
tailwindcss-rails (2.7.6-x86_64-darwin)
|
||||
tailwindcss-rails (2.7.7-x86_64-darwin)
|
||||
railties (>= 7.0.0)
|
||||
tailwindcss-rails (2.7.6-x86_64-linux)
|
||||
tailwindcss-rails (2.7.7-x86_64-linux)
|
||||
railties (>= 7.0.0)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
@@ -443,7 +450,7 @@ GEM
|
||||
activemodel (>= 6.0.0)
|
||||
bindex (>= 0.4.0)
|
||||
railties (>= 6.0.0)
|
||||
webmock (3.23.1)
|
||||
webmock (3.24.0)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
@@ -477,13 +484,16 @@ DEPENDENCIES
|
||||
erb_lint
|
||||
faker
|
||||
faraday
|
||||
faraday-multipart
|
||||
faraday-retry
|
||||
good_job
|
||||
holidays
|
||||
hotwire-livereload
|
||||
i18n-tasks
|
||||
image_processing (>= 1.2)
|
||||
importmap-rails
|
||||
inline_svg
|
||||
intercom-rails
|
||||
letter_opener
|
||||
lucide-rails!
|
||||
mocha
|
||||
@@ -503,6 +513,7 @@ DEPENDENCIES
|
||||
simplecov
|
||||
stackprof
|
||||
stimulus-rails
|
||||
stripe
|
||||
tailwindcss-rails
|
||||
turbo-rails
|
||||
tzinfo-data
|
||||
|
||||
BIN
app/assets/images/bg-grid.png
Normal file
BIN
app/assets/images/bg-grid.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 326 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 932 B |
1
app/assets/images/discord-icon.svg
Normal file
1
app/assets/images/discord-icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"><path fill="#5865f2" d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"/></svg>
|
||||
|
After Width: | Height: | Size: 764 B |
Binary file not shown.
|
Before Width: | Height: | Size: 548 B |
1
app/assets/images/github-icon.svg
Normal file
1
app/assets/images/github-icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#24292f"/></svg>
|
||||
|
After Width: | Height: | Size: 963 B |
160
app/assets/images/logo-squircle.svg
Normal file
160
app/assets/images/logo-squircle.svg
Normal file
@@ -0,0 +1,160 @@
|
||||
<svg width="70" height="70" viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_iii_4725_68011)">
|
||||
<path d="M1.66199 28.3573C3.33915 4.37286 8.83917 -0.408237 32.8236 1.26892L41.868 1.90136C65.8524 3.57851 70.6335 9.07854 68.9563 33.063L68.3239 42.1073C66.6467 66.0917 61.1467 70.8728 37.1623 69.1957L28.1179 68.5632C4.13349 66.8861 -0.647606 61.3861 1.02955 37.4016L1.66199 28.3573Z" fill="url(#paint0_linear_4725_68011)"/>
|
||||
<path d="M1.66199 28.3573C3.33915 4.37286 8.83917 -0.408237 32.8236 1.26892L41.868 1.90136C65.8524 3.57851 70.6335 9.07854 68.9563 33.063L68.3239 42.1073C66.6467 66.0917 61.1467 70.8728 37.1623 69.1957L28.1179 68.5632C4.13349 66.8861 -0.647606 61.3861 1.02955 37.4016L1.66199 28.3573Z" fill="black" fill-opacity="0.7"/>
|
||||
</g>
|
||||
<path d="M2.82179 28.4384C3.23922 22.4687 3.89051 17.7733 4.98031 14.1012C6.06625 10.4421 7.56711 7.86966 9.64032 6.06745C11.7135 4.26524 14.4698 3.13701 18.2445 2.57088C22.0324 2.00274 26.7729 2.01127 32.7425 2.42871L41.7868 3.06115C47.7565 3.47859 52.452 4.12988 56.124 5.21968C59.7831 6.30562 62.3556 7.80648 64.1578 9.87969C65.96 11.9529 67.0882 14.7092 67.6544 18.4838C68.2225 22.2718 68.214 27.0122 67.7965 32.9819L67.1641 42.0262C66.7466 47.9959 66.0953 52.6913 65.0056 56.3634C63.9196 60.0225 62.4188 62.5949 60.3455 64.3971C58.2723 66.1994 55.516 67.3276 51.7414 67.8937C47.9534 68.4619 43.213 68.4533 37.2434 68.0359L28.199 67.4034C22.2294 66.986 17.5339 66.3347 13.8619 65.2449C10.2028 64.159 7.6303 62.6581 5.82808 60.5849C4.02587 58.5117 2.89764 55.7554 2.33151 51.9808C1.76337 48.1928 1.77191 43.4524 2.18934 37.4827L2.82179 28.4384Z" stroke="white" stroke-width="2.32525"/>
|
||||
<path d="M2.82179 28.4384C3.23922 22.4687 3.89051 17.7733 4.98031 14.1012C6.06625 10.4421 7.56711 7.86966 9.64032 6.06745C11.7135 4.26524 14.4698 3.13701 18.2445 2.57088C22.0324 2.00274 26.7729 2.01127 32.7425 2.42871L41.7868 3.06115C47.7565 3.47859 52.452 4.12988 56.124 5.21968C59.7831 6.30562 62.3556 7.80648 64.1578 9.87969C65.96 11.9529 67.0882 14.7092 67.6544 18.4838C68.2225 22.2718 68.214 27.0122 67.7965 32.9819L67.1641 42.0262C66.7466 47.9959 66.0953 52.6913 65.0056 56.3634C63.9196 60.0225 62.4188 62.5949 60.3455 64.3971C58.2723 66.1994 55.516 67.3276 51.7414 67.8937C47.9534 68.4619 43.213 68.4533 37.2434 68.0359L28.199 67.4034C22.2294 66.986 17.5339 66.3347 13.8619 65.2449C10.2028 64.159 7.6303 62.6581 5.82808 60.5849C4.02587 58.5117 2.89764 55.7554 2.33151 51.9808C1.76337 48.1928 1.77191 43.4524 2.18934 37.4827L2.82179 28.4384Z" stroke="url(#paint1_linear_4725_68011)" stroke-width="2.32525"/>
|
||||
<path d="M3.66933 28.4976C4.08541 22.5474 4.73164 17.9253 5.79481 14.343C6.85131 10.7831 8.28392 8.3723 10.1977 6.70866C12.1115 5.04503 14.6982 3.96188 18.3705 3.4111C22.0659 2.85684 26.7329 2.86017 32.6832 3.27625L41.7276 3.9087C47.6779 4.32478 52.2999 4.97101 55.8823 6.03418C59.4422 7.09068 61.8529 8.52329 63.5166 10.4371C65.1802 12.3509 66.2634 14.9376 66.8141 18.6098C67.3684 22.3053 67.3651 26.9723 66.949 32.9226L66.3165 41.967C65.9005 47.9172 65.2542 52.5393 64.1911 56.1216C63.1345 59.6815 61.7019 62.0923 59.7882 63.7559C57.8744 65.4196 55.2877 66.5027 51.6154 67.0535C47.92 67.6078 43.2529 67.6044 37.3026 67.1883L28.2583 66.5559C22.308 66.1398 17.6859 65.4936 14.1036 64.4304C10.5437 63.3739 8.13293 61.9413 6.4693 60.0275C4.80566 58.1137 3.72251 55.527 3.17173 51.8548C2.61747 48.1593 2.6208 43.4923 3.03688 37.542L3.66933 28.4976Z" stroke="url(#paint2_linear_4725_68011)" stroke-opacity="0.05" stroke-width="4.02448"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.20106 14.4635C5.15118 18.0011 4.50747 22.5866 4.09206 28.5272L3.45962 37.5716C3.04421 43.5122 3.04348 48.1426 3.59081 51.7919C4.13394 55.4131 5.1946 57.9152 6.78912 59.7495C8.38363 61.5838 10.7138 62.9824 14.2242 64.0242C17.7617 65.0741 22.3472 65.7178 28.2878 66.1332L37.3322 66.7656C43.2728 67.181 47.9033 67.1818 51.5525 66.6344C55.1738 66.0913 57.6759 65.0306 59.5101 63.4361C61.3444 61.8416 62.743 59.5115 63.7848 56.0011C64.8347 52.4635 65.4784 47.878 65.8938 41.9374L66.5262 32.893C66.9417 26.9524 66.9424 22.322 66.3951 18.6727C65.8519 15.0515 64.7913 12.5494 63.1968 10.7151C61.6022 8.88082 59.2721 7.48225 55.7617 6.44043C52.2241 5.39055 47.6387 4.74684 41.698 4.33143L32.6537 3.69899C26.713 3.28358 22.0826 3.28284 18.4333 3.83018C14.8121 4.3733 12.31 5.43397 10.4757 7.02849C8.64145 8.623 7.24288 10.9531 6.20106 14.4635ZM32.8236 1.26892C8.83917 -0.408237 3.33915 4.37286 1.66199 28.3573L1.02955 37.4016C-0.647606 61.3861 4.13349 66.8861 28.1179 68.5632L37.1623 69.1957C61.1467 70.8728 66.6467 66.0917 68.3239 42.1073L68.9563 33.063C70.6335 9.07854 65.8524 3.57851 41.868 1.90136L32.8236 1.26892Z" fill="url(#paint3_linear_4725_68011)"/>
|
||||
<g filter="url(#filter1_ddii_4725_68011)">
|
||||
<path d="M20.8165 43.8927L14.5692 43.4559C13.0888 43.3523 11.8006 44.5292 11.6919 46.0845C11.5831 47.6398 12.695 48.9845 14.1753 49.088L20.4227 49.5248C21.903 49.6284 23.1912 48.4515 23.3 46.8962C23.4087 45.3409 22.2968 43.9962 20.8165 43.8927Z" fill="#F23E94"/>
|
||||
<path d="M14.5574 43.6244L20.8047 44.0612C22.1842 44.1577 23.2343 45.4143 23.1315 46.8844C23.0287 48.3546 21.814 49.4528 20.4344 49.3564L14.1871 48.9195C12.8076 48.823 11.7576 47.5664 11.8604 46.0963C11.9632 44.6261 13.1779 43.5279 14.5574 43.6244Z" stroke="url(#paint4_linear_4725_68011)" stroke-opacity="0.1" stroke-width="0.33782" stroke-linejoin="round"/>
|
||||
<path d="M48.3652 51.4775L54.6125 51.9144C56.0928 52.0179 57.381 50.841 57.4898 49.2857C57.5985 47.7305 56.4866 46.3858 55.0063 46.2823L48.759 45.8454C47.2787 45.7419 45.9905 46.9188 45.8817 48.474C45.7729 50.0293 46.8848 51.374 48.3652 51.4775Z" fill="#F23E94"/>
|
||||
<path d="M54.6243 51.7459L48.3769 51.309C46.9974 51.2126 45.9474 49.956 46.0502 48.4858C46.153 47.0157 47.3677 45.9174 48.7472 46.0139L54.9945 46.4508C56.3741 46.5472 57.4241 47.8038 57.3213 49.274C57.2185 50.7441 56.0038 51.8423 54.6243 51.7459Z" stroke="url(#paint5_linear_4725_68011)" stroke-opacity="0.1" stroke-width="0.33782" stroke-linejoin="round"/>
|
||||
<path d="M37.3154 45.0271L32.2298 44.6714C30.7495 44.5679 29.4613 45.7448 29.3525 47.3001C29.2438 48.8553 30.3556 50.2001 31.836 50.3036L36.9215 50.6592C38.4019 50.7627 39.6901 49.5858 39.7988 48.0306C39.9076 46.4753 38.7957 45.1306 37.3154 45.0271Z" fill="#F23E94"/>
|
||||
<path d="M32.218 44.8399L37.3036 45.1956C38.6831 45.292 39.7331 46.5486 39.6303 48.0188C39.5275 49.4889 38.3128 50.5872 36.9333 50.4907L31.8478 50.1351C30.4682 50.0386 29.4182 48.782 29.521 47.3119C29.6238 45.8417 30.8385 44.7435 32.218 44.8399Z" stroke="url(#paint6_linear_4725_68011)" stroke-opacity="0.1" stroke-width="0.33782" stroke-linejoin="round"/>
|
||||
<path d="M46.5039 43.2046L52.5198 43.6253C54.0001 43.7288 55.2884 42.5519 55.3971 40.9967C55.5059 39.4414 54.394 38.0967 52.9136 37.9932L46.8977 37.5725C45.4174 37.469 44.1292 38.6459 44.0204 40.2011C43.9116 41.7564 45.0235 43.1011 46.5039 43.2046Z" fill="#6927DA"/>
|
||||
<path d="M52.5316 43.4568L46.5156 43.0361C45.1361 42.9397 44.0861 41.6831 44.1889 40.2129C44.2917 38.7428 45.5064 37.6445 46.8859 37.741L52.9019 38.1617C54.2814 38.2582 55.3314 39.5148 55.2286 40.9849C55.1258 42.455 53.9111 43.5533 52.5316 43.4568Z" stroke="url(#paint7_linear_4725_68011)" stroke-opacity="0.1" stroke-width="0.33782" stroke-linejoin="round"/>
|
||||
<path d="M23.7094 35.95L17.6934 35.5293C16.2131 35.4258 14.9249 36.6027 14.8161 38.158C14.7074 39.7133 15.8193 41.058 17.2996 41.1615L23.3155 41.5822C24.7959 41.6857 26.0841 40.5088 26.1928 38.9535C26.3016 37.3983 25.1897 36.0535 23.7094 35.95Z" fill="#6927DA"/>
|
||||
<path d="M17.6817 35.6978L23.6976 36.1185C25.0771 36.215 26.1272 37.4716 26.0244 38.9417C25.9215 40.4119 24.7069 41.5101 23.3273 41.4137L17.3114 40.993C15.9319 40.8965 14.8818 39.6399 14.9846 38.1698C15.0874 36.6996 16.3021 35.6014 17.6817 35.6978Z" stroke="url(#paint8_linear_4725_68011)" stroke-opacity="0.1" stroke-width="0.33782" stroke-linejoin="round"/>
|
||||
<path d="M39.8134 37.0582L30.8613 36.4322C29.381 36.3287 28.0927 37.5055 27.984 39.0608C27.8752 40.6161 28.9871 41.9608 30.4675 42.0643L39.4195 42.6903C40.8999 42.7938 42.1881 41.6169 42.2968 40.0617C42.4056 38.5064 41.2937 37.1617 39.8134 37.0582Z" fill="#6927DA"/>
|
||||
<path d="M30.8495 36.6007L39.8016 37.2267C41.1811 37.3231 42.2311 38.5797 42.1283 40.0499C42.0255 41.52 40.8108 42.6183 39.4313 42.5218L30.4792 41.8958C29.0997 41.7994 28.0497 40.5428 28.1525 39.0726C28.2553 37.6025 29.47 36.5042 30.8495 36.6007Z" stroke="url(#paint9_linear_4725_68011)" stroke-opacity="0.1" stroke-width="0.33782" stroke-linejoin="round"/>
|
||||
<path d="M32.3636 28.1666L20.9406 27.3679C19.4603 27.2643 18.1721 28.4412 18.0633 29.9965C17.9546 31.5518 19.0665 32.8965 20.5468 33L31.9698 33.7988C33.4501 33.9023 34.7383 32.7254 34.8471 31.1701C34.9558 29.6148 33.8439 28.2701 32.3636 28.1666Z" fill="#1570EF"/>
|
||||
<path d="M20.9289 27.5363L32.3518 28.3351C33.7314 28.4316 34.7814 29.6882 34.6786 31.1583C34.5758 32.6285 33.3611 33.7267 31.9816 33.6303L20.5586 32.8315C19.179 32.735 18.129 31.4784 18.2318 30.0083C18.3346 28.5381 19.5493 27.4399 20.9289 27.5363Z" stroke="url(#paint10_linear_4725_68011)" stroke-opacity="0.1" stroke-width="0.33782" stroke-linejoin="round"/>
|
||||
<path d="M39.7417 34.3403L50.4352 35.0881C51.9156 35.1916 53.2038 34.0147 53.3125 32.4594C53.4213 30.9042 52.3094 29.5595 50.8291 29.456L40.1356 28.7082C38.6552 28.6047 37.367 29.7816 37.2583 31.3368C37.1495 32.8921 38.2614 34.2368 39.7417 34.3403Z" fill="#1570EF"/>
|
||||
<path d="M50.447 34.9196L39.7535 34.1718C38.374 34.0754 37.324 32.8188 37.4268 31.3486C37.5296 29.8785 38.7442 28.7802 40.1238 28.8767L50.8173 29.6245C52.1968 29.7209 53.2468 30.9775 53.144 32.4477C53.0412 33.9178 51.8265 35.0161 50.447 34.9196Z" stroke="url(#paint11_linear_4725_68011)" stroke-opacity="0.1" stroke-width="0.33782" stroke-linejoin="round"/>
|
||||
<path d="M48.8251 21.1728L42.7957 20.7512C41.3154 20.6476 40.0272 21.8245 39.9184 23.3798C39.8097 24.9351 40.9216 26.2798 42.4019 26.3833L48.4312 26.8049C49.9116 26.9084 51.1998 25.7315 51.3085 24.1763C51.4173 22.621 50.3054 21.2763 48.8251 21.1728Z" fill="#22CCEE"/>
|
||||
<path d="M42.784 20.9196L48.8133 21.3413C50.1928 21.4377 51.2428 22.6943 51.14 24.1645C51.0372 25.6346 49.8226 26.7329 48.443 26.6364L42.4137 26.2148C41.0342 26.1183 39.9841 24.8617 40.0869 23.3916C40.1897 21.9214 41.4044 20.8232 42.784 20.9196Z" stroke="url(#paint12_linear_4725_68011)" stroke-opacity="0.1" stroke-width="0.33782" stroke-linejoin="round"/>
|
||||
<path d="M30.0984 19.8627L24.0691 19.4411C22.5887 19.3376 21.3005 20.5145 21.1918 22.0697C21.083 23.625 22.1949 24.9697 23.6752 25.0732L29.7046 25.4948C31.1849 25.5983 32.4731 24.4215 32.5819 22.8662C32.6906 21.3109 31.5787 19.9662 30.0984 19.8627Z" fill="#22CCEE"/>
|
||||
<path d="M24.0573 19.6096L30.0866 20.0312C31.4661 20.1277 32.5162 21.3843 32.4134 22.8544C32.3106 24.3246 31.0959 25.4228 29.7163 25.3263L23.687 24.9047C22.3075 24.8083 21.2574 23.5517 21.3603 22.0815C21.4631 20.6114 22.6777 19.5131 24.0573 19.6096Z" stroke="url(#paint13_linear_4725_68011)" stroke-opacity="0.1" stroke-width="0.33782" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<g opacity="0.23" filter="url(#filter2_f_4725_68011)">
|
||||
<path d="M2.69258 40.4874L12.7122 44.9449L39.098 57.1213L66.9412 61.8859L45.5294 72.5984L-0.202764 68.4613L2.69258 40.4874Z" fill="#F24396"/>
|
||||
</g>
|
||||
<g opacity="0.23" filter="url(#filter3_f_4725_68011)">
|
||||
<path d="M2.56821 1.97031L52.6272 -2.04293L69.5358 11.3492L56.9932 16.1074L23.2807 14.6892L2.56821 1.97031Z" fill="#22CCEE"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_iii_4725_68011" x="0.72644" y="-2.61149" width="68.533" height="75.6876" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1.27762"/>
|
||||
<feGaussianBlur stdDeviation="1.59703"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.196078 0 0 0 0 0.188235 0 0 0 0 0.219608 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_4725_68011"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="3.57731"/>
|
||||
<feGaussianBlur stdDeviation="2.68298"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.321569 0 0 0 0 0.905882 0 0 0 0 1 0 0 0 0.6 0"/>
|
||||
<feBlend mode="normal" in2="effect1_innerShadow_4725_68011" result="effect2_innerShadow_4725_68011"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-3.57731"/>
|
||||
<feGaussianBlur stdDeviation="1.78866"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.933333 0 0 0 0 0.160784 0 0 0 0 0.509804 0 0 0 0.5 0"/>
|
||||
<feBlend mode="normal" in2="effect2_innerShadow_4725_68011" result="effect3_innerShadow_4725_68011"/>
|
||||
</filter>
|
||||
<filter id="filter1_ddii_4725_68011" x="1.54998" y="10.9893" width="66.0817" height="52.755" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1.6891"/>
|
||||
<feGaussianBlur stdDeviation="5.0673"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.29135 0 0 0 0 0.0895476 0 0 0 0 0.654593 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4725_68011"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1.6891"/>
|
||||
<feGaussianBlur stdDeviation="4.22275"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_4725_68011" result="effect2_dropShadow_4725_68011"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_4725_68011" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-1.6891"/>
|
||||
<feGaussianBlur stdDeviation="0.844549"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect3_innerShadow_4725_68011"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1.6891"/>
|
||||
<feGaussianBlur stdDeviation="0.844549"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.05 0"/>
|
||||
<feBlend mode="normal" in2="effect3_innerShadow_4725_68011" result="effect4_innerShadow_4725_68011"/>
|
||||
</filter>
|
||||
<filter id="filter2_f_4725_68011" x="-23.4553" y="17.2348" width="113.649" height="78.6161" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="11.6263" result="effect1_foregroundBlur_4725_68011"/>
|
||||
</filter>
|
||||
<filter id="filter3_f_4725_68011" x="-20.6843" y="-25.2955" width="113.473" height="64.6555" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="11.6263" result="effect1_foregroundBlur_4725_68011"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_4725_68011" x1="53.7772" y1="8.36942" x2="38.7315" y2="35.4937" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#363636"/>
|
||||
<stop offset="1" stop-color="#141414"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_4725_68011" x1="37.3458" y1="1.58514" x2="32.6401" y2="68.8795" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#52EDFF"/>
|
||||
<stop offset="0.274483" stop-color="#4361EE"/>
|
||||
<stop offset="0.629793" stop-color="#7209B7"/>
|
||||
<stop offset="1" stop-color="#F12980"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_4725_68011" x1="37.019" y1="6.25836" x2="33.4897" y2="56.7291" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_4725_68011" x1="37.3458" y1="1.58514" x2="32.6401" y2="68.8795" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#52EDFF"/>
|
||||
<stop offset="0.274483" stop-color="#4361EE"/>
|
||||
<stop offset="0.629793" stop-color="#7209B7"/>
|
||||
<stop offset="1" stop-color="#F12980"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_4725_68011" x1="17.6928" y1="43.6743" x2="17.299" y2="49.3064" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_4725_68011" x1="51.4888" y1="51.6959" x2="51.8826" y2="46.0638" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint6_linear_4725_68011" x1="34.7726" y1="44.8492" x2="34.3788" y2="50.4814" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint7_linear_4725_68011" x1="49.5118" y1="43.415" x2="49.9057" y2="37.7829" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint8_linear_4725_68011" x1="20.7014" y1="35.7397" x2="20.3076" y2="41.3718" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint9_linear_4725_68011" x1="35.3373" y1="36.7452" x2="34.9435" y2="42.3773" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint10_linear_4725_68011" x1="26.6521" y1="27.7672" x2="26.2583" y2="33.3994" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint11_linear_4725_68011" x1="45.0885" y1="34.7142" x2="45.4823" y2="29.0821" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint12_linear_4725_68011" x1="45.8104" y1="20.962" x2="45.4166" y2="26.5941" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint13_linear_4725_68011" x1="27.0837" y1="19.6519" x2="26.6899" y2="25.284" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 19 KiB |
@@ -25,7 +25,7 @@ class Account::EntriesController < ApplicationController
|
||||
def destroy
|
||||
@entry.destroy!
|
||||
@entry.sync_account_later
|
||||
redirect_back_or_to account_url(@entry.account), notice: t(".success")
|
||||
redirect_to account_url(@entry.account), notice: t(".success")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -2,7 +2,7 @@ class Account::HoldingsController < ApplicationController
|
||||
layout :with_sidebar
|
||||
|
||||
before_action :set_account
|
||||
before_action :set_holding, only: :show
|
||||
before_action :set_holding, only: %i[show destroy]
|
||||
|
||||
def index
|
||||
@holdings = @account.holdings.current
|
||||
@@ -11,6 +11,11 @@ class Account::HoldingsController < ApplicationController
|
||||
def show
|
||||
end
|
||||
|
||||
def destroy
|
||||
@holding.destroy_holding_and_entries!
|
||||
redirect_back_or_to account_holdings_path(@account)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
|
||||
@@ -2,6 +2,7 @@ class Account::TradesController < ApplicationController
|
||||
layout :with_sidebar
|
||||
|
||||
before_action :set_account
|
||||
before_action :set_entry, only: :update
|
||||
|
||||
def new
|
||||
@entry = @account.entries.account_trades.new(entryable_attributes: {})
|
||||
@@ -23,15 +24,36 @@ class Account::TradesController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@entry.update!(entry_params)
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to account_entry_path(@account, @entry), notice: t(".success") }
|
||||
format.turbo_stream { render turbo_stream: turbo_stream.replace(@entry) }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Current.family.accounts.find(params[:account_id])
|
||||
end
|
||||
|
||||
def set_entry
|
||||
@entry = @account.entries.find(params[:id])
|
||||
end
|
||||
|
||||
def entry_params
|
||||
params.require(:account_entry)
|
||||
.permit(:type, :date, :qty, :ticker, :price, :amount, :currency, :transfer_account_id)
|
||||
.permit(
|
||||
:type, :date, :qty, :ticker, :price, :amount, :notes, :excluded, :currency, :transfer_account_id, :entryable_type,
|
||||
entryable_attributes: [
|
||||
:id,
|
||||
:qty,
|
||||
:ticker,
|
||||
:price
|
||||
]
|
||||
)
|
||||
.merge(account: @account)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -33,11 +33,9 @@ class Account::TransactionsController < ApplicationController
|
||||
def entry_params
|
||||
params.require(:account_entry)
|
||||
.permit(
|
||||
:name, :date, :amount, :currency, :entryable_type, :nature,
|
||||
:name, :date, :amount, :currency, :excluded, :notes, :entryable_type, :nature,
|
||||
entryable_attributes: [
|
||||
:id,
|
||||
:notes,
|
||||
:excluded,
|
||||
:category_id,
|
||||
:merchant_id,
|
||||
{ tag_ids: [] }
|
||||
|
||||
@@ -41,14 +41,11 @@ class AccountsController < ApplicationController
|
||||
end
|
||||
|
||||
def edit
|
||||
@account.accountable.build_address if @account.accountable.is_a?(Property) && @account.accountable.address.blank?
|
||||
end
|
||||
|
||||
def update
|
||||
Account.transaction do
|
||||
@account.update! account_params.except(:accountable_type, :balance)
|
||||
@account.update_balance!(account_params[:balance]) if account_params[:balance]
|
||||
end
|
||||
@account.sync_later
|
||||
@account.update_with_sync!(account_params)
|
||||
redirect_back_or_to account_path(@account), notice: t(".success")
|
||||
end
|
||||
|
||||
|
||||
@@ -2,9 +2,6 @@ class ApplicationController < ActionController::Base
|
||||
include Localize, AutoSync, Authentication, Invitable, SelfHostable, StoreLocation
|
||||
include Pagy::Backend
|
||||
|
||||
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
|
||||
allow_browser versions: :modern
|
||||
|
||||
private
|
||||
|
||||
def with_sidebar
|
||||
|
||||
41
app/controllers/credit_cards_controller.rb
Normal file
41
app/controllers/credit_cards_controller.rb
Normal file
@@ -0,0 +1,41 @@
|
||||
class CreditCardsController < ApplicationController
|
||||
before_action :set_account, only: :update
|
||||
|
||||
def create
|
||||
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]
|
||||
|
||||
account.sync_later
|
||||
redirect_to account, notice: t(".success")
|
||||
end
|
||||
|
||||
def update
|
||||
@account.update_with_sync!(account_params)
|
||||
redirect_to @account, notice: t(".success")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Current.family.accounts.find(params[:id])
|
||||
end
|
||||
|
||||
def account_params
|
||||
params.require(:account)
|
||||
.permit(
|
||||
:name, :balance, :institution_id, :start_date, :start_balance, :currency, :accountable_type,
|
||||
accountable_attributes: [
|
||||
:id,
|
||||
:available_credit,
|
||||
:minimum_payment,
|
||||
:apr,
|
||||
:annual_fee,
|
||||
:expiration_date
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,6 @@
|
||||
class InviteCodesController < ApplicationController
|
||||
before_action :ensure_self_hosted
|
||||
|
||||
def index
|
||||
@invite_codes = InviteCode.all
|
||||
end
|
||||
@@ -7,4 +9,10 @@ class InviteCodesController < ApplicationController
|
||||
InviteCode.generate!
|
||||
redirect_back_or_to invite_codes_path, notice: "Code generated"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_self_hosted
|
||||
redirect_to root_path unless self_hosted?
|
||||
end
|
||||
end
|
||||
|
||||
39
app/controllers/loans_controller.rb
Normal file
39
app/controllers/loans_controller.rb
Normal file
@@ -0,0 +1,39 @@
|
||||
class LoansController < ApplicationController
|
||||
before_action :set_account, only: :update
|
||||
|
||||
def create
|
||||
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]
|
||||
|
||||
account.sync_later
|
||||
redirect_to account, notice: t(".success")
|
||||
end
|
||||
|
||||
def update
|
||||
@account.update_with_sync!(account_params)
|
||||
redirect_to @account, notice: t(".success")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Current.family.accounts.find(params[:id])
|
||||
end
|
||||
|
||||
def account_params
|
||||
params.require(:account)
|
||||
.permit(
|
||||
:name, :balance, :institution_id, :start_date, :start_balance, :currency, :accountable_type,
|
||||
accountable_attributes: [
|
||||
:id,
|
||||
:rate_type,
|
||||
:interest_rate,
|
||||
:term_months
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,6 @@
|
||||
class PagesController < ApplicationController
|
||||
layout :with_sidebar
|
||||
skip_before_action :authenticate_user!, only: %i[early_access]
|
||||
layout :with_sidebar, except: %i[early_access]
|
||||
|
||||
include Filterable
|
||||
|
||||
@@ -25,7 +26,7 @@ class PagesController < ApplicationController
|
||||
|
||||
# TODO: Placeholders for trendlines
|
||||
placeholder_series_data = 10.times.map do |i|
|
||||
{ date: Date.current - i.days, value: Money.new(0) }
|
||||
{ date: Date.current - i.days, value: Money.new(0, Current.family.currency) }
|
||||
end
|
||||
@investing_series = TimeSeries.new(placeholder_series_data)
|
||||
end
|
||||
@@ -37,6 +38,11 @@ class PagesController < ApplicationController
|
||||
def feedback
|
||||
end
|
||||
|
||||
def invites
|
||||
def early_access
|
||||
redirect_to root_path if self_hosted?
|
||||
|
||||
@invite_codes_count = InviteCode.count
|
||||
@invite_code = InviteCode.order("RANDOM()").limit(1).first
|
||||
render layout: false
|
||||
end
|
||||
end
|
||||
|
||||
@@ -14,8 +14,7 @@ class PropertiesController < ApplicationController
|
||||
end
|
||||
|
||||
def update
|
||||
@account.update!(account_params)
|
||||
@account.sync_later
|
||||
@account.update_with_sync!(account_params)
|
||||
redirect_to @account, notice: t(".success")
|
||||
end
|
||||
|
||||
@@ -28,7 +27,7 @@ class PropertiesController < ApplicationController
|
||||
def account_params
|
||||
params.require(:account)
|
||||
.permit(
|
||||
:name, :balance, :start_date, :start_balance, :currency, :accountable_type,
|
||||
:name, :balance, :institution_id, :start_date, :start_balance, :currency, :accountable_type,
|
||||
accountable_attributes: [
|
||||
:id,
|
||||
:year_built,
|
||||
|
||||
2
app/controllers/settings/billings_controller.rb
Normal file
2
app/controllers/settings/billings_controller.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
class Settings::BillingsController < SettingsController
|
||||
end
|
||||
37
app/controllers/subscriptions_controller.rb
Normal file
37
app/controllers/subscriptions_controller.rb
Normal file
@@ -0,0 +1,37 @@
|
||||
class SubscriptionsController < ApplicationController
|
||||
def new
|
||||
client = Stripe::StripeClient.new(ENV["STRIPE_SECRET_KEY"])
|
||||
|
||||
if Current.family.stripe_customer_id.blank?
|
||||
customer = client.v1.customers.create(
|
||||
email: Current.family.primary_user.email,
|
||||
metadata: { family_id: Current.family.id }
|
||||
)
|
||||
Current.family.update(stripe_customer_id: customer.id)
|
||||
end
|
||||
|
||||
session = client.v1.checkout.sessions.create({
|
||||
customer: Current.family.stripe_customer_id,
|
||||
line_items: [ {
|
||||
price: ENV["STRIPE_PLAN_ID"],
|
||||
quantity: 1
|
||||
} ],
|
||||
mode: "subscription",
|
||||
allow_promotion_codes: true,
|
||||
success_url: settings_billing_url,
|
||||
cancel_url: settings_billing_url
|
||||
})
|
||||
|
||||
redirect_to session.url, allow_other_host: true, status: :see_other
|
||||
end
|
||||
|
||||
def show
|
||||
client = Stripe::StripeClient.new(ENV["STRIPE_SECRET_KEY"])
|
||||
|
||||
portal_session = client.v1.billing_portal.sessions.create(
|
||||
customer: Current.family.stripe_customer_id,
|
||||
return_url: settings_billing_url
|
||||
)
|
||||
redirect_to portal_session.url, allow_other_host: true, status: :see_other
|
||||
end
|
||||
end
|
||||
@@ -14,8 +14,7 @@ class VehiclesController < ApplicationController
|
||||
end
|
||||
|
||||
def update
|
||||
@account.update!(account_params)
|
||||
@account.sync_later
|
||||
@account.update_with_sync!(account_params)
|
||||
redirect_to @account, notice: t(".success")
|
||||
end
|
||||
|
||||
@@ -28,7 +27,7 @@ class VehiclesController < ApplicationController
|
||||
def account_params
|
||||
params.require(:account)
|
||||
.permit(
|
||||
:name, :balance, :start_date, :start_balance, :currency, :accountable_type,
|
||||
:name, :balance, :institution_id, :start_date, :start_balance, :currency, :accountable_type,
|
||||
accountable_attributes: [
|
||||
:id,
|
||||
:make,
|
||||
|
||||
61
app/controllers/webhooks_controller.rb
Normal file
61
app/controllers/webhooks_controller.rb
Normal file
@@ -0,0 +1,61 @@
|
||||
class WebhooksController < ApplicationController
|
||||
skip_before_action :verify_authenticity_token, only: [ :stripe ]
|
||||
skip_authentication
|
||||
|
||||
def stripe
|
||||
webhook_body = request.body.read
|
||||
sig_header = request.env["HTTP_STRIPE_SIGNATURE"]
|
||||
client = Stripe::StripeClient.new(ENV["STRIPE_SECRET_KEY"])
|
||||
|
||||
begin
|
||||
thin_event = client.parse_thin_event(webhook_body, sig_header, ENV["STRIPE_WEBHOOK_SECRET"])
|
||||
|
||||
event = client.v1.events.retrieve(thin_event.id)
|
||||
|
||||
case event.type
|
||||
when /^customer\.subscription\./
|
||||
handle_subscription_event(event)
|
||||
when "customer.created", "customer.updated", "customer.deleted"
|
||||
handle_customer_event(event)
|
||||
else
|
||||
Rails.logger.info "Unhandled event type: #{event.type}"
|
||||
end
|
||||
|
||||
rescue JSON::ParserError
|
||||
render json: { error: "Invalid payload" }, status: :bad_request
|
||||
return
|
||||
rescue Stripe::SignatureVerificationError
|
||||
render json: { error: "Invalid signature" }, status: :bad_request
|
||||
return
|
||||
end
|
||||
|
||||
render json: { received: true }, status: :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def handle_subscription_event(event)
|
||||
subscription = event.data.object
|
||||
family = Family.find_by(stripe_customer_id: subscription.customer)
|
||||
|
||||
if family
|
||||
family.update(
|
||||
stripe_plan_id: subscription.plan.id,
|
||||
stripe_subscription_status: subscription.status
|
||||
)
|
||||
else
|
||||
Rails.logger.error "Family not found for Stripe customer ID: #{subscription.customer}"
|
||||
end
|
||||
end
|
||||
|
||||
def handle_customer_event(event)
|
||||
customer = event.data.object
|
||||
family = Family.find_by(stripe_customer_id: customer.id)
|
||||
|
||||
if family
|
||||
family.update(stripe_customer_id: customer.id)
|
||||
else
|
||||
Rails.logger.error "Family not found for Stripe customer ID: #{customer.id}"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,9 @@
|
||||
module AccountsHelper
|
||||
def summary_card(title:, &block)
|
||||
content = capture(&block)
|
||||
render "accounts/summary_card", title: title, content: content
|
||||
end
|
||||
|
||||
def to_accountable_title(accountable)
|
||||
accountable.model_name.human
|
||||
end
|
||||
@@ -31,6 +36,10 @@ module AccountsHelper
|
||||
properties_path
|
||||
when "Vehicle"
|
||||
vehicles_path
|
||||
when "Loan"
|
||||
loans_path
|
||||
when "CreditCard"
|
||||
credit_cards_path
|
||||
else
|
||||
accounts_path
|
||||
end
|
||||
@@ -42,6 +51,10 @@ module AccountsHelper
|
||||
property_path(account)
|
||||
when "Vehicle"
|
||||
vehicle_path(account)
|
||||
when "Loan"
|
||||
loan_path(account)
|
||||
when "CreditCard"
|
||||
credit_card_path(account)
|
||||
else
|
||||
account_path(account)
|
||||
end
|
||||
@@ -55,8 +68,10 @@ module AccountsHelper
|
||||
transactions_tab = { key: "transactions", label: t("accounts.show.transactions"), path: account_path(account, tab: "transactions"), route: account_transactions_path(account) }
|
||||
trades_tab = { key: "trades", label: t("accounts.show.trades"), path: account_path(account, tab: "trades"), route: account_trades_path(account) }
|
||||
|
||||
return [ value_tab ] if account.other_asset? || account.other_liability?
|
||||
return [ overview_tab, value_tab ] if account.property? || account.vehicle?
|
||||
return [ holdings_tab, cash_tab, trades_tab ] if account.investment?
|
||||
return [ holdings_tab, cash_tab, trades_tab, value_tab ] if account.investment?
|
||||
return [ overview_tab, value_tab, transactions_tab ] if account.loan? || account.credit_card?
|
||||
|
||||
[ value_tab, transactions_tab ]
|
||||
end
|
||||
|
||||
@@ -137,12 +137,16 @@ module ApplicationHelper
|
||||
end
|
||||
|
||||
def format_money(number_or_money, options = {})
|
||||
return nil unless number_or_money
|
||||
|
||||
money = Money.new(number_or_money)
|
||||
options.reverse_merge!(money.format_options(I18n.locale))
|
||||
number_to_currency(money.amount, options)
|
||||
end
|
||||
|
||||
def format_money_without_symbol(number_or_money, options = {})
|
||||
return nil unless number_or_money
|
||||
|
||||
money = Money.new(number_or_money)
|
||||
options.reverse_merge!(money.format_options(I18n.locale))
|
||||
ActiveSupport::NumberHelper.number_to_delimited(money.amount.round(options[:precision] || 0), { delimiter: options[:delimiter], separator: options[:separator] })
|
||||
|
||||
2
app/helpers/settings/billing_helper.rb
Normal file
2
app/helpers/settings/billing_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
module Settings::BillingHelper
|
||||
end
|
||||
@@ -3,6 +3,7 @@ module SettingsHelper
|
||||
{ name: I18n.t("settings.nav.profile_label"), path: :settings_profile_path },
|
||||
{ name: I18n.t("settings.nav.preferences_label"), path: :settings_preferences_path },
|
||||
{ name: I18n.t("settings.nav.self_hosting_label"), path: :settings_hosting_path, condition: :self_hosted? },
|
||||
{ name: I18n.t("settings.nav.billing_label"), path: :settings_billing_path },
|
||||
{ name: I18n.t("settings.nav.accounts_label"), path: :accounts_path },
|
||||
{ name: I18n.t("settings.nav.tags_label"), path: :tags_path },
|
||||
{ name: I18n.t("settings.nav.categories_label"), path: :categories_path },
|
||||
|
||||
@@ -39,11 +39,11 @@ class StyledFormBuilder < ActionView::Helpers::FormBuilder
|
||||
build_styled_field(label, field, options, remove_padding_right: true)
|
||||
end
|
||||
|
||||
def money_field(amount_method, currency_method, options = {})
|
||||
def money_field(amount_method, options = {})
|
||||
@template.render partial: "shared/money_field", locals: {
|
||||
form: self,
|
||||
amount_method:,
|
||||
currency_method:,
|
||||
currency_method: options[:currency_method] || :currency,
|
||||
**options
|
||||
}
|
||||
end
|
||||
|
||||
2
app/helpers/subscription_helper.rb
Normal file
2
app/helpers/subscription_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
module SubscriptionHelper
|
||||
end
|
||||
2
app/helpers/webhooks_helper.rb
Normal file
2
app/helpers/webhooks_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
module WebhooksHelper
|
||||
end
|
||||
@@ -1,21 +1,21 @@
|
||||
import {Controller} from "@hotwired/stimulus"
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
// Connects to data-controller="bulk-select"
|
||||
export default class extends Controller {
|
||||
static targets = ["row", "group", "selectionBar", "selectionBarText", "bulkEditDrawerTitle"]
|
||||
static values = {
|
||||
resource: String,
|
||||
selectedIds: {type: Array, default: []}
|
||||
selectedIds: { type: Array, default: [] }
|
||||
}
|
||||
|
||||
connect() {
|
||||
document.addEventListener("turbo:load", this.#updateView)
|
||||
document.addEventListener("turbo:load", this._updateView)
|
||||
|
||||
this.#updateView()
|
||||
this._updateView()
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
document.removeEventListener("turbo:load", this.#updateView)
|
||||
document.removeEventListener("turbo:load", this._updateView)
|
||||
}
|
||||
|
||||
bulkEditDrawerTitleTargetConnected(element) {
|
||||
@@ -63,7 +63,7 @@ export default class extends Controller {
|
||||
}
|
||||
|
||||
selectedIdsValueChanged() {
|
||||
this.#updateView()
|
||||
this._updateView()
|
||||
}
|
||||
|
||||
#addHiddenFormInputsForSelectedIds(form, paramName, transactionIds) {
|
||||
@@ -101,7 +101,7 @@ export default class extends Controller {
|
||||
this.selectedIdsValue = this.rowTargets.map(t => t.dataset.id)
|
||||
}
|
||||
|
||||
#updateView = () => {
|
||||
_updateView = () => {
|
||||
this.#updateSelectionBar()
|
||||
this.#updateGroups()
|
||||
this.#updateRows()
|
||||
|
||||
@@ -11,12 +11,12 @@ export default class extends Controller {
|
||||
usePercentSign: Boolean
|
||||
}
|
||||
|
||||
#d3SvgMemo = null
|
||||
#d3GroupMemo = null
|
||||
#d3Tooltip = null
|
||||
#d3InitialContainerWidth = 0
|
||||
#d3InitialContainerHeight = 0
|
||||
#normalDataPoints = []
|
||||
#d3SvgMemo = null;
|
||||
#d3GroupMemo = null;
|
||||
#d3Tooltip = null;
|
||||
#d3InitialContainerWidth = 0;
|
||||
#d3InitialContainerHeight = 0;
|
||||
#normalDataPoints = [];
|
||||
|
||||
connect() {
|
||||
this.#install()
|
||||
@@ -181,7 +181,7 @@ export default class extends Controller {
|
||||
.call(
|
||||
d3
|
||||
.axisBottom(this.#d3XScale)
|
||||
.tickValues([ this.#normalDataPoints[0].date, this.#normalDataPoints[this.#normalDataPoints.length - 1].date ])
|
||||
.tickValues([this.#normalDataPoints[0].date, this.#normalDataPoints[this.#normalDataPoints.length - 1].date])
|
||||
.tickSize(0)
|
||||
.tickFormat(d3.timeFormat("%d %b %Y"))
|
||||
)
|
||||
@@ -247,7 +247,6 @@ export default class extends Controller {
|
||||
.style("fill", `url(#${this.element.id}-trendline-gradient)`)
|
||||
}
|
||||
|
||||
|
||||
#drawTooltip() {
|
||||
this.#d3Tooltip = d3
|
||||
.select(`#${this.element.id}`)
|
||||
@@ -345,7 +344,7 @@ export default class extends Controller {
|
||||
}
|
||||
|
||||
#tooltipTemplate(datum) {
|
||||
return(`
|
||||
return (`
|
||||
<div style="margin-bottom: 4px; color: ${tailwindColors.gray[500]};">
|
||||
${d3.timeFormat("%b %d, %Y")(datum.date)}
|
||||
</div>
|
||||
@@ -428,7 +427,7 @@ export default class extends Controller {
|
||||
.append("svg")
|
||||
.attr("width", this.#d3InitialContainerWidth)
|
||||
.attr("height", this.#d3InitialContainerHeight)
|
||||
.attr("viewBox", [ 0, 0, this.#d3InitialContainerWidth, this.#d3InitialContainerHeight ])
|
||||
.attr("viewBox", [0, 0, this.#d3InitialContainerWidth, this.#d3InitialContainerHeight])
|
||||
}
|
||||
|
||||
#createMainGroup() {
|
||||
@@ -502,7 +501,7 @@ export default class extends Controller {
|
||||
get #d3XScale() {
|
||||
return d3
|
||||
.scaleTime()
|
||||
.rangeRound([ 0, this.#d3ContainerWidth ])
|
||||
.rangeRound([0, this.#d3ContainerWidth])
|
||||
.domain(d3.extent(this.#normalDataPoints, d => d.date))
|
||||
}
|
||||
|
||||
@@ -514,7 +513,7 @@ export default class extends Controller {
|
||||
|
||||
return d3
|
||||
.scaleLinear()
|
||||
.rangeRound([ this.#d3ContainerHeight, 0 ])
|
||||
.domain([ dataMin - padding, dataMax + padding ])
|
||||
.rangeRound([this.#d3ContainerHeight, 0])
|
||||
.domain([dataMin - padding, dataMax + padding])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
class ApplicationMailer < ActionMailer::Base
|
||||
default from: ENV["EMAIL_SENDER"] if ENV["EMAIL_SENDER"].present?
|
||||
layout "mailer"
|
||||
end
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
class PasswordMailer < ApplicationMailer
|
||||
def password_reset
|
||||
mail to: params[:user].email
|
||||
@user = params[:user]
|
||||
@subject = t(".subject")
|
||||
@cta = t(".cta")
|
||||
|
||||
mail to: @user.email, subject: @subject
|
||||
end
|
||||
end
|
||||
|
||||
@@ -39,13 +39,16 @@ class Account < ApplicationRecord
|
||||
|
||||
Accountable.by_classification.each do |classification, types|
|
||||
types.each do |type|
|
||||
group = grouped_accounts[classification.to_sym].add_child_group(type, currency)
|
||||
self.where(accountable_type: type).each do |account|
|
||||
group.add_value_node(
|
||||
account,
|
||||
account.balance_money.exchange_to(currency, fallback_rate: 0),
|
||||
account.series(period: period, currency: currency)
|
||||
)
|
||||
accounts = self.where(accountable_type: type)
|
||||
if accounts.any?
|
||||
group = grouped_accounts[classification.to_sym].add_child_group(type, currency)
|
||||
accounts.each do |account|
|
||||
group.add_value_node(
|
||||
account,
|
||||
account.balance_money.exchange_to(currency, fallback_rate: 0),
|
||||
account.series(period: period, currency: currency)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -79,6 +82,11 @@ class Account < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
def original_balance
|
||||
balance_amount = balances.chronological.first&.balance || balance
|
||||
Money.new(balance_amount, currency)
|
||||
end
|
||||
|
||||
def owns_ticker?(ticker)
|
||||
security_id = Security.find_by(ticker: ticker)&.id
|
||||
entries.account_trades
|
||||
@@ -90,6 +98,15 @@ class Account < ApplicationRecord
|
||||
classification == "asset" ? "up" : "down"
|
||||
end
|
||||
|
||||
def update_with_sync!(attributes)
|
||||
transaction do
|
||||
update!(attributes)
|
||||
update_balance!(attributes[:balance]) if attributes[:balance]
|
||||
end
|
||||
|
||||
sync_later
|
||||
end
|
||||
|
||||
def update_balance!(balance)
|
||||
valuation = entries.account_valuations.find_by(date: Date.current)
|
||||
|
||||
|
||||
@@ -109,8 +109,8 @@ class Account::Entry < ApplicationRecord
|
||||
def bulk_update!(bulk_update_params)
|
||||
bulk_attributes = {
|
||||
date: bulk_update_params[:date],
|
||||
notes: bulk_update_params[:notes],
|
||||
entryable_attributes: {
|
||||
notes: bulk_update_params[:notes],
|
||||
category_id: bulk_update_params[:category_id],
|
||||
merchant_id: bulk_update_params[:merchant_id]
|
||||
}.compact_blank
|
||||
@@ -129,17 +129,21 @@ class Account::Entry < ApplicationRecord
|
||||
end
|
||||
|
||||
def income_total(currency = "USD")
|
||||
without_transfers.account_transactions.includes(:entryable)
|
||||
total = without_transfers.account_transactions.includes(:entryable)
|
||||
.where("account_entries.amount <= 0")
|
||||
.map { |e| e.amount_money.exchange_to(currency, date: e.date, fallback_rate: 0) }
|
||||
.sum
|
||||
|
||||
Money.new(total, currency)
|
||||
end
|
||||
|
||||
def expense_total(currency = "USD")
|
||||
without_transfers.account_transactions.includes(:entryable)
|
||||
total = without_transfers.account_transactions.includes(:entryable)
|
||||
.where("account_entries.amount > 0")
|
||||
.map { |e| e.amount_money.exchange_to(currency, date: e.date, fallback_rate: 0) }
|
||||
.sum
|
||||
|
||||
Money.new(total, currency)
|
||||
end
|
||||
|
||||
def search(params)
|
||||
|
||||
@@ -37,6 +37,19 @@ class Account::Holding < ApplicationRecord
|
||||
@trend ||= calculate_trend
|
||||
end
|
||||
|
||||
def trades
|
||||
account.entries.where(entryable: account.trades.where(security: security)).reverse_chronological
|
||||
end
|
||||
|
||||
def destroy_holding_and_entries!
|
||||
transaction do
|
||||
account.entries.where(entryable: account.trades.where(security: security)).destroy_all
|
||||
destroy
|
||||
end
|
||||
|
||||
account.sync_later
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def calculate_trend
|
||||
|
||||
@@ -25,4 +25,15 @@ class Account::Trade < ApplicationRecord
|
||||
def buy?
|
||||
qty > 0
|
||||
end
|
||||
|
||||
def unrealized_gain_loss
|
||||
return nil if sell?
|
||||
current_price = security.current_price
|
||||
return nil if current_price.nil?
|
||||
|
||||
current_value = current_price * qty.abs
|
||||
cost_basis = price_money * qty.abs
|
||||
|
||||
TimeSeries::Trend.new(current: current_value, previous: cost_basis)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
class Address < ApplicationRecord
|
||||
belongs_to :addressable, polymorphic: true
|
||||
|
||||
validates :line1, :locality, presence: true
|
||||
validates :postal_code, presence: true, if: :postal_code_required?
|
||||
|
||||
def to_s
|
||||
I18n.t("address.format",
|
||||
line1: line1,
|
||||
@@ -15,10 +12,4 @@ class Address < ApplicationRecord
|
||||
postal_code: postal_code
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def postal_code_required?
|
||||
country.in?(%w[US CA GB])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -28,7 +28,7 @@ module Accountable
|
||||
if balance_series.empty? && period.date_range.end == Date.current
|
||||
TimeSeries.new([ { date: Date.current, value: account.balance_money.exchange_to(currency) } ])
|
||||
else
|
||||
TimeSeries.from_collection(balance_series, :balance_money)
|
||||
TimeSeries.from_collection(balance_series, :balance_money, favorable_direction: account.asset? ? "up" : "down")
|
||||
end
|
||||
rescue Money::ConversionError
|
||||
TimeSeries.new([])
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
class CreditCard < ApplicationRecord
|
||||
include Accountable
|
||||
|
||||
def available_credit_money
|
||||
available_credit ? Money.new(available_credit, account.currency) : nil
|
||||
end
|
||||
|
||||
def minimum_payment_money
|
||||
minimum_payment ? Money.new(minimum_payment, account.currency) : nil
|
||||
end
|
||||
|
||||
def annual_fee_money
|
||||
annual_fee ? Money.new(annual_fee, account.currency) : nil
|
||||
end
|
||||
end
|
||||
|
||||
@@ -127,4 +127,12 @@ class Family < ApplicationRecord
|
||||
def synth_usage
|
||||
self.class.synth_provider&.usage
|
||||
end
|
||||
|
||||
def subscribed?
|
||||
stripe_subscription_status.present? && stripe_subscription_status == "active"
|
||||
end
|
||||
|
||||
def primary_user
|
||||
users.order(:created_at).first
|
||||
end
|
||||
end
|
||||
|
||||
@@ -32,7 +32,11 @@ class Gapfiller
|
||||
attr_reader :date_range, :cache
|
||||
|
||||
def should_gapfill?(date, record)
|
||||
date.on_weekend? && record.nil?
|
||||
(date.on_weekend? || holiday?(date)) && record.nil?
|
||||
end
|
||||
|
||||
def holiday?(date)
|
||||
Holidays.on(date, :federalreserve, :us, :observed, :informal).any?
|
||||
end
|
||||
|
||||
def create_gapfilled_record(prev_record, date)
|
||||
|
||||
@@ -71,10 +71,10 @@ class Import < ApplicationRecord
|
||||
{
|
||||
account: row[account_col_label].to_s,
|
||||
date: row[date_col_label].to_s,
|
||||
qty: row[qty_col_label].to_s,
|
||||
qty: sanitize_number(row[qty_col_label]).to_s,
|
||||
ticker: row[ticker_col_label].to_s,
|
||||
price: row[price_col_label].to_s,
|
||||
amount: row[amount_col_label].to_s,
|
||||
price: sanitize_number(row[price_col_label]).to_s,
|
||||
amount: sanitize_number(row[amount_col_label]).to_s,
|
||||
currency: (row[currency_col_label] || default_currency).to_s,
|
||||
name: (row[name_col_label] || default_row_name).to_s,
|
||||
category: row[category_col_label].to_s,
|
||||
@@ -113,6 +113,14 @@ class Import < ApplicationRecord
|
||||
cleaned? && mappings.all?(&:valid?)
|
||||
end
|
||||
|
||||
def has_unassigned_account?
|
||||
mappings.accounts.where(key: "").any?
|
||||
end
|
||||
|
||||
def requires_account?
|
||||
family.accounts.empty? && has_unassigned_account?
|
||||
end
|
||||
|
||||
private
|
||||
def import!
|
||||
# no-op, subclasses can implement for customization of algorithm
|
||||
@@ -134,4 +142,9 @@ class Import < ApplicationRecord
|
||||
converters: [ ->(str) { str&.strip } ]
|
||||
)
|
||||
end
|
||||
|
||||
def sanitize_number(value)
|
||||
return "" if value.nil?
|
||||
value.gsub(/[^\d.\-]/, "")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,7 +4,7 @@ class Import::Row < ApplicationRecord
|
||||
validates :amount, numericality: true, allow_blank: true
|
||||
validates :currency, presence: true
|
||||
|
||||
validate :date_matches_user_format
|
||||
validate :date_valid
|
||||
validate :required_columns
|
||||
validate :currency_is_valid
|
||||
|
||||
@@ -54,13 +54,21 @@ class Import::Row < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
def date_matches_user_format
|
||||
def date_valid
|
||||
return if date.blank?
|
||||
|
||||
parsed_date = Date.strptime(date, import.date_format) rescue nil
|
||||
|
||||
if parsed_date.nil?
|
||||
errors.add(:date, "must exactly match the format: #{import.date_format}")
|
||||
return
|
||||
end
|
||||
|
||||
min_date = Account::Entry.min_supported_date
|
||||
max_date = Date.current
|
||||
|
||||
if parsed_date < min_date || parsed_date > max_date
|
||||
errors.add(:date, "must be between #{min_date} and #{max_date}")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -6,11 +6,13 @@ class Investment < ApplicationRecord
|
||||
[ "Pension", "pension" ],
|
||||
[ "Retirement", "retirement" ],
|
||||
[ "401(k)", "401k" ],
|
||||
[ "529 plan", "529_plan" ],
|
||||
[ "Traditional 401(k)", "traditional_401k" ],
|
||||
[ "Roth 401(k)", "roth_401k" ],
|
||||
[ "529 Plan", "529_plan" ],
|
||||
[ "Health Savings Account", "hsa" ],
|
||||
[ "Mutual Fund", "mutual_fund" ],
|
||||
[ "Traditional IRA", "traditional_ira" ],
|
||||
[ "Roth IRA", "roth_ira" ],
|
||||
[ "Roth 401k", "roth_401k" ],
|
||||
[ "Angel", "angel" ]
|
||||
].freeze
|
||||
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
class Loan < ApplicationRecord
|
||||
include Accountable
|
||||
|
||||
def monthly_payment
|
||||
return nil if term_months.nil? || interest_rate.nil? || rate_type.nil? || rate_type != "fixed"
|
||||
return Money.new(0, account.currency) if account.original_balance.amount.zero? || term_months.zero?
|
||||
|
||||
annual_rate = interest_rate / 100.0
|
||||
monthly_rate = annual_rate / 12.0
|
||||
|
||||
if monthly_rate.zero?
|
||||
payment = account.original_balance.amount / term_months
|
||||
else
|
||||
payment = (account.original_balance.amount * monthly_rate * (1 + monthly_rate)**term_months) / ((1 + monthly_rate)**term_months - 1)
|
||||
end
|
||||
|
||||
Money.new(payment.round, account.currency)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -34,7 +34,8 @@ class MintImport < Import
|
||||
amount: row.signed_amount,
|
||||
name: row.name,
|
||||
currency: row.currency,
|
||||
entryable: Account::Transaction.new(category: category, tags: tags, notes: row.notes),
|
||||
notes: row.notes,
|
||||
entryable: Account::Transaction.new(category: category, tags: tags),
|
||||
import: self
|
||||
|
||||
entry.save!
|
||||
|
||||
@@ -47,6 +47,8 @@ class Provider::Github
|
||||
if release
|
||||
{
|
||||
avatar: release.author.avatar_url,
|
||||
# this is the username, it would be nice to get the full name
|
||||
username: release.author.login,
|
||||
name: release.name,
|
||||
published_at: release.published_at,
|
||||
body: Octokit.markdown(release.body, mode: "gfm", context: repo)
|
||||
|
||||
@@ -5,6 +5,12 @@ class Security < ApplicationRecord
|
||||
|
||||
validates :ticker, presence: true, uniqueness: { case_sensitive: false }
|
||||
|
||||
def current_price
|
||||
@current_price ||= Security::Price.find_price(ticker:, date: Date.current)
|
||||
return nil if @current_price.nil?
|
||||
Money.new(@current_price.price, @current_price.currency)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def upcase_ticker
|
||||
|
||||
@@ -38,13 +38,15 @@ module Security::Price::Provided
|
||||
|
||||
if response.success?
|
||||
response.prices.map do |price|
|
||||
new_price = Security::Price.new \
|
||||
new_price = Security::Price.find_or_initialize_by(
|
||||
ticker: ticker,
|
||||
date: price[:date],
|
||||
price: price[:price],
|
||||
currency: price[:currency]
|
||||
date: price[:date]
|
||||
) do |p|
|
||||
p.price = price[:price]
|
||||
p.currency = price[:currency]
|
||||
end
|
||||
|
||||
new_price.save! if cache
|
||||
new_price.save! if cache && new_price.new_record?
|
||||
new_price
|
||||
end
|
||||
else
|
||||
|
||||
@@ -3,14 +3,14 @@ class TimeSeries
|
||||
|
||||
attr_reader :values, :favorable_direction
|
||||
|
||||
def self.from_collection(collection, value_method)
|
||||
def self.from_collection(collection, value_method, favorable_direction: "up")
|
||||
collection.map do |obj|
|
||||
{
|
||||
date: obj.date,
|
||||
value: obj.public_send(value_method),
|
||||
original: obj
|
||||
}
|
||||
end.then { |data| new(data) }
|
||||
end.then { |data| new(data, favorable_direction: favorable_direction) }
|
||||
end
|
||||
|
||||
def initialize(data, favorable_direction: "up")
|
||||
|
||||
@@ -13,7 +13,8 @@ class TransactionImport < Import
|
||||
amount: row.signed_amount,
|
||||
name: row.name,
|
||||
currency: row.currency,
|
||||
entryable: Account::Transaction.new(category: category, tags: tags, notes: row.notes),
|
||||
notes: row.notes,
|
||||
entryable: Account::Transaction.new(category: category, tags: tags),
|
||||
import: self
|
||||
|
||||
entry.save!
|
||||
|
||||
@@ -14,7 +14,7 @@ class User < ApplicationRecord
|
||||
enum :role, { member: "member", admin: "admin" }, validate: true
|
||||
|
||||
has_one_attached :profile_image do |attachable|
|
||||
attachable.variant :thumbnail, resize_to_limit: [ 150, 150 ], preprocessed: true
|
||||
attachable.variant :thumbnail, resize_to_fill: [ 300, 300 ]
|
||||
end
|
||||
|
||||
validate :profile_image_size
|
||||
|
||||
@@ -30,9 +30,11 @@
|
||||
end
|
||||
end
|
||||
|
||||
first_child = children.first
|
||||
|
||||
summed_series = summed_by_date.map { |date, value| { date: date, value: value } }
|
||||
|
||||
TimeSeries.new(summed_series)
|
||||
TimeSeries.new(summed_series, favorable_direction: first_child&.series&.favorable_direction || "up")
|
||||
end
|
||||
|
||||
def series=(series)
|
||||
|
||||
@@ -9,36 +9,103 @@
|
||||
<%= render "shared/circle_logo", name: @holding.name %>
|
||||
</header>
|
||||
|
||||
<details class="group 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>
|
||||
<p class="pl-4 text-gray-500">Coming soon...</p>
|
||||
<div class="pb-4">
|
||||
<dl class="space-y-3 px-3 py-2">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<dt class="text-gray-500"><%= t(".ticker_label") %></dt>
|
||||
<dd class="text-gray-900"><%= @holding.ticker %></dd>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<dt class="text-gray-500"><%= t(".current_market_price_label") %></dt>
|
||||
<dd class="text-gray-900"><%= @holding.security.current_price ? format_money(@holding.security.current_price) : t(".unknown") %></dd>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<dt class="text-gray-500"><%= t(".portfolio_weight_label") %></dt>
|
||||
<dd class="text-gray-900"><%= @holding.weight ? number_to_percentage(@holding.weight, precision: 2) : t(".unknown") %></dd>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<dt class="text-gray-500"><%= t(".avg_cost_label") %></dt>
|
||||
<dd class="text-gray-900"><%= @holding.avg_cost ? format_money(@holding.avg_cost) : t(".unknown") %></dd>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<dt class="text-gray-500"><%= t(".trend_label") %></dt>
|
||||
<dd style="color: <%= @holding.trend&.color %>;">
|
||||
<%= @holding.trend ? render("shared/trend_change", trend: @holding.trend) : t(".unknown") %>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="group 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(".history") %></h4>
|
||||
<%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-gray-500 w-5" %>
|
||||
</summary>
|
||||
|
||||
<div>
|
||||
<p class="pl-4 text-gray-500">Coming soon...</p>
|
||||
<div class="space-y-2">
|
||||
<div class="px-3 py-4">
|
||||
<% if @holding.trades.any? %>
|
||||
<ul class="space-y-2">
|
||||
<% @holding.trades.each_with_index do |trade_entry, index| %>
|
||||
<li class="flex gap-4 text-sm space-y-1">
|
||||
<div class="flex flex-col items-center gap-1.5 pt-2">
|
||||
<div class="rounded-full h-1.5 w-1.5 bg-gray-300"></div>
|
||||
<% unless index == @holding.trades.length - 1 %>
|
||||
<div class="h-12 w-px bg-alpha-black-200"></div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-gray-500 text-xs uppercase"><%= l(trade_entry.date, format: :long) %></p>
|
||||
|
||||
<p><%= t(
|
||||
".trade_history_entry",
|
||||
qty: trade_entry.account_trade.qty,
|
||||
security: trade_entry.account_trade.security.ticker,
|
||||
price: format_money(trade_entry.account_trade.price)
|
||||
) %></p>
|
||||
</div>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
|
||||
<% else %>
|
||||
<p class="text-gray-500">No trade history available for this holding.</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="group 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(".settings") %></h4>
|
||||
<%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-gray-500 w-5" %>
|
||||
</summary>
|
||||
|
||||
<div>
|
||||
<p class="pl-4 text-gray-500">Coming soon...</p>
|
||||
<div class="pb-4">
|
||||
<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_holding_path(@holding.account, @holding),
|
||||
method: :delete,
|
||||
class: "rounded-lg px-3 py-2 text-red-500 text-sm font-medium border border-alpha-black-200",
|
||||
data: { turbo_confirm: true, turbo_frame: "_top" } %>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<%= form.date_field :date, label: true %>
|
||||
|
||||
<div data-trade-form-target="amountInput" hidden>
|
||||
<%= form.money_field :amount, :currency, label: t(".amount"), disable_currency: true %>
|
||||
<%= form.money_field :amount, label: t(".amount"), disable_currency: true %>
|
||||
</div>
|
||||
|
||||
<div data-trade-form-target="transferAccountInput" hidden>
|
||||
@@ -25,7 +25,7 @@
|
||||
</div>
|
||||
|
||||
<div data-trade-form-target="priceInput">
|
||||
<%= form.money_field :price, :currency, label: t(".price"), disable_currency: true %>
|
||||
<%= form.money_field :price, label: t(".price"), disable_currency: true %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,29 +1,146 @@
|
||||
<% entry = @entry %>
|
||||
<% entry, trade, account = @entry, @entry.account_trade, @entry.account %>
|
||||
|
||||
<%= 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 -entry.amount_money %></span>
|
||||
<span class="text-lg text-gray-500"><%= entry.currency %></span>
|
||||
</h3>
|
||||
</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 -entry.amount_money %>
|
||||
</span>
|
||||
|
||||
<span class="text-sm text-gray-500"><%= entry.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 pl-4 text-gray-500">
|
||||
<p>Details coming soon...</p>
|
||||
</div>
|
||||
</details>
|
||||
<span class="text-lg text-gray-500">
|
||||
<%= entry.currency %>
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<span class="text-sm text-gray-500">
|
||||
<%= I18n.l(entry.date, format: :long) %>
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div class="space-y-2">
|
||||
<!-- Overview Section -->
|
||||
<%= disclosure t(".overview") do %>
|
||||
<div class="pb-4">
|
||||
<dl class="space-y-3 px-3 py-2">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<dt class="text-gray-500"><%= t(".symbol_label") %></dt>
|
||||
<dd class="text-gray-900"><%= trade.security.ticker %></dd>
|
||||
</div>
|
||||
|
||||
<% if trade.buy? %>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<dt class="text-gray-500"><%= t(".purchase_qty_label") %></dt>
|
||||
<dd class="text-gray-900"><%= trade.qty.abs %></dd>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<dt class="text-gray-500"><%= t(".purchase_price_label") %></dt>
|
||||
<dd class="text-gray-900"><%= format_money trade.price_money %></dd>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<dt class="text-gray-500"><%= t(".current_market_price_label") %></dt>
|
||||
<dd class="text-gray-900"><%= format_money trade.security.current_price %></dd>
|
||||
</div>
|
||||
|
||||
<% if trade.buy? %>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<dt class="text-gray-500"><%= t(".total_return_label") %></dt>
|
||||
<dd style="color: <%= trade.unrealized_gain_loss.color %>;">
|
||||
<%= render "shared/trend_change", trend: trade.unrealized_gain_loss %>
|
||||
</dd>
|
||||
</div>
|
||||
<% end %>
|
||||
</dl>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Details Section -->
|
||||
<%= disclosure t(".details") do %>
|
||||
<div class="pb-4">
|
||||
<%= styled_form_with model: [account, entry],
|
||||
url: account_trade_path(account, entry),
|
||||
class: "space-y-2",
|
||||
data: { controller: "auto-submit-form" } do |f| %>
|
||||
<%= f.date_field :date,
|
||||
label: t(".date_label"),
|
||||
max: Date.current,
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
|
||||
<%= f.fields_for :entryable do |ef| %>
|
||||
<%= ef.number_field :qty,
|
||||
label: t(".quantity_label"),
|
||||
step: "any",
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
|
||||
<%= ef.money_field :price,
|
||||
label: t(".cost_per_share_label"),
|
||||
disable_currency: true,
|
||||
auto_submit: true,
|
||||
min: 0 %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Additional Section -->
|
||||
<%= disclosure t(".additional") do %>
|
||||
<div class="pb-4">
|
||||
<%= styled_form_with model: [account, entry],
|
||||
url: account_trade_path(account, entry),
|
||||
class: "space-y-2",
|
||||
data: { controller: "auto-submit-form" } do |f| %>
|
||||
<%= f.text_area :notes,
|
||||
label: t(".note_label"),
|
||||
placeholder: t(".note_placeholder"),
|
||||
rows: 5,
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Settings Section -->
|
||||
<%= disclosure t(".settings") do %>
|
||||
<div class="pb-4">
|
||||
<!-- Exclude Trade Form -->
|
||||
<%= styled_form_with model: [account, entry],
|
||||
url: account_trade_path(account, entry),
|
||||
class: "p-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_entry_excluded"
|
||||
class="maybe-switch"></label>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Delete Trade Form -->
|
||||
<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_entry_path(account, entry),
|
||||
method: :delete,
|
||||
class: "rounded-lg px-3 py-2 text-red-500 text-sm
|
||||
font-medium border border-alpha-black-200",
|
||||
data: { turbo_confirm: true, turbo_frame: "_top" } %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
{ container_class: "w-1/3", label: t(".nature"), selected: entry.amount.negative? ? "income" : "expense" },
|
||||
{ data: { "auto-submit-form-target": "auto" } } %>
|
||||
|
||||
<%= f.money_field :amount, :currency, label: t(".amount"),
|
||||
<%= f.money_field :amount, label: t(".amount"),
|
||||
container_class: "w-2/3",
|
||||
auto_submit: true,
|
||||
min: 0,
|
||||
@@ -104,7 +104,13 @@
|
||||
},
|
||||
{ "data-auto-submit-form-target": "auto" } %>
|
||||
|
||||
<%= ef.text_area :notes,
|
||||
<% end %>
|
||||
|
||||
<%= styled_form_with model: [account, entry],
|
||||
url: account_transaction_path(account, entry),
|
||||
class: "space-y-2",
|
||||
data: { controller: "auto-submit-form" } do |f| %>
|
||||
<%= f.text_area :notes,
|
||||
label: t(".note_label"),
|
||||
placeholder: t(".note_placeholder"),
|
||||
rows: 5,
|
||||
@@ -122,22 +128,20 @@
|
||||
url: account_transaction_path(account, entry),
|
||||
class: "p-3",
|
||||
data: { controller: "auto-submit-form" } do |f| %>
|
||||
<%= f.fields_for :entryable do |ef| %>
|
||||
<div class="flex cursor-pointer items-center gap-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="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">
|
||||
<%= ef.check_box :excluded,
|
||||
<div class="relative inline-block select-none">
|
||||
<%= f.check_box :excluded,
|
||||
class: "sr-only peer",
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
<label for="account_entry_entryable_attributes_excluded"
|
||||
<label for="account_entry_entryable_attributes_excluded"
|
||||
class="maybe-switch"></label>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Delete Transaction Form -->
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<%= f.text_field :name, value: transfer.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, :currency, label: t(".amount"), required: true %>
|
||||
<%= f.money_field :amount, label: t(".amount"), required: true, hide_currency: true %>
|
||||
<%= f.date_field :date, value: transfer.date, label: t(".date"), required: true, max: Date.current %>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<%= 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", min: Account::Entry.min_supported_date, max: Date.current, 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.date_field :date, required: "required", min: Account::Entry.min_supported_date, max: Date.current, value: Date.current, 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 :amount, 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: entry.account.currency %>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<%= 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 %>
|
||||
<%= 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-alpha-black-25 hover:bg-alpha-black-25 border border-transparent block px-2 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>
|
||||
|
||||
@@ -4,13 +4,19 @@
|
||||
<div class="grow space-y-2">
|
||||
<%= f.hidden_field :accountable_type %>
|
||||
<%= f.text_field :name, placeholder: t(".name_placeholder"), required: "required", label: t(".name_label"), autofocus: true %>
|
||||
<%= f.collection_select :institution_id, Current.family.institutions.alphabetically, :id, :name, { include_blank: t(".ungrouped"), label: t(".institution") } %>
|
||||
<%= f.money_field :balance, :currency, label: t(".balance"), required: true, default_currency: Current.family.currency %>
|
||||
|
||||
<% if account.new_record? %>
|
||||
<%= f.hidden_field :institution_id %>
|
||||
<% else %>
|
||||
<%= f.collection_select :institution_id, Current.family.institutions.alphabetically, :id, :name, { include_blank: t(".ungrouped"), label: t(".institution") } %>
|
||||
<% end %>
|
||||
|
||||
<%= f.money_field :balance, label: t(".balance"), required: true, default_currency: Current.family.currency %>
|
||||
|
||||
<% if account.new_record? %>
|
||||
<div class="flex items-center gap-2 mt-3 mb-6">
|
||||
<div class="w-1/2"><%= f.date_field :start_date, label: t(".start_date"), max: Date.yesterday, min: Account::Entry.min_supported_date %></div>
|
||||
<div class="w-1/2"><%= f.number_field :start_balance, label: t(".start_balance"), placeholder: 90 %></div>
|
||||
<div class="w-1/2"><%= f.money_field :start_balance, label: t(".start_balance"), placeholder: 90, hide_currency: true, default_currency: Current.family.currency %></div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
||||
@@ -13,8 +13,6 @@
|
||||
|
||||
<% end %>
|
||||
|
||||
<%= render "sync_all_button" %>
|
||||
|
||||
<%= link_to new_account_path, class: "rounded-lg bg-gray-900 text-white flex items-center gap-1 justify-center hover:bg-gray-700 px-3 py-2", data: { turbo_frame: :modal } do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||
<p class="text-sm font-medium"><%= t(".new") %></p>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<%# locals: (account:) %>
|
||||
|
||||
<%= render partial: "accounts/accountables/#{account.accountable_type.downcase}/overview", locals: { account: account } %>
|
||||
<%= render partial: "accounts/accountables/#{account.accountable_type.underscore}/overview", locals: { account: account } %>
|
||||
|
||||
8
app/views/accounts/_summary_card.html.erb
Normal file
8
app/views/accounts/_summary_card.html.erb
Normal file
@@ -0,0 +1,8 @@
|
||||
<%# locals: (title:, content:) %>
|
||||
|
||||
<div class="rounded-xl bg-white shadow-xs border border-alpha-black-25 p-4">
|
||||
<h4 class="text-gray-500 text-sm"><%= title %></h4>
|
||||
<p class="text-xl font-medium text-gray-900">
|
||||
<%= content %>
|
||||
</p>
|
||||
</div>
|
||||
@@ -0,0 +1,21 @@
|
||||
<div>
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="space-y-2">
|
||||
<%= f.fields_for :accountable do |credit_card_form| %>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= credit_card_form.number_field :available_credit, label: t(".available_credit"), placeholder: t(".available_credit_placeholder"), min: 0 %>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<%= credit_card_form.number_field :minimum_payment, label: t(".minimum_payment"), placeholder: t(".minimum_payment_placeholder"), min: 0 %>
|
||||
<%= credit_card_form.number_field :apr, label: t(".apr"), placeholder: t(".apr_placeholder"), min: 0, step: 0.01 %>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<%= credit_card_form.date_field :expiration_date, label: t(".expiration_date") %>
|
||||
<%= credit_card_form.number_field :annual_fee, label: t(".annual_fee"), placeholder: t(".annual_fee_placeholder"), min: 0 %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1 +1 @@
|
||||
<%= f.select :subtype, options_for_select([["Checking", "checking"], ["Savings", "savings"]], selected: ""), { label: "Type" } %>
|
||||
<%= f.select :subtype, [["Checking", "checking"], ["Savings", "savings"]], { label: true, prompt: t(".prompt"), include_blank: t(".none") } %>
|
||||
|
||||
@@ -1 +1 @@
|
||||
<%= f.select :subtype, options_for_select(Investment::SUBTYPES, selected: ""), { label: true } %>
|
||||
<%= f.select :subtype, Investment::SUBTYPES, { label: true, prompt: t(".prompt"), include_blank: t(".none") } %>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<div>
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="space-y-2">
|
||||
<%= f.fields_for :accountable do |loan_form| %>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= loan_form.number_field :interest_rate, label: t(".interest_rate"), placeholder: t(".interest_rate_placeholder"), min: 0, step: 0.01 %>
|
||||
<%= loan_form.select :rate_type, options_for_select([["Fixed", "fixed"], ["Variable", "variable"], ["Adjustable", "adjustable"]]), { label: t(".rate_type") } %>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<%= loan_form.number_field :term_months, label: t(".term_months"), placeholder: t(".term_months_placeholder") %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
<div>
|
||||
<hr class="my-4">
|
||||
|
||||
<h3 class="my-4 font-medium"><%= t(".additional_info") %> (<%= t(".optional") %>)</h3>
|
||||
|
||||
<div class="space-y-2">
|
||||
<%= f.fields_for :accountable do |af| %>
|
||||
<div class="flex gap-2">
|
||||
<%= af.number_field :year_built, label: t(".year_built"), placeholder: 2005 %>
|
||||
<%= af.number_field :area_value, label: t(".area_value"), placeholder: 2000 %>
|
||||
<%= af.number_field :year_built, label: t(".year_built"), placeholder: 2005, min: 1700, max: Time.current.year %>
|
||||
<%= af.number_field :area_value, label: t(".area_value"), placeholder: 2000, min: 1 %>
|
||||
<%= af.select :area_unit,
|
||||
[["Square feet", "sqft"], ["Square meters", "sqm"]],
|
||||
{ label: t(".area_unit") } %>
|
||||
@@ -15,18 +17,18 @@
|
||||
|
||||
<%= af.fields_for :address do |address_form| %>
|
||||
<div class="flex gap-2">
|
||||
<%= address_form.text_field :line1, label: t(".line1"), placeholder: "123 Main St", required: true %>
|
||||
<%= address_form.text_field :line1, label: t(".line1"), placeholder: "123 Main St" %>
|
||||
<%= address_form.text_field :line2, label: t(".line2"), placeholder: "Apt 1" %>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<%= address_form.text_field :locality, label: t(".city"), placeholder: "Sacramento", required: true %>
|
||||
<%= address_form.text_field :region, label: t(".state"), placeholder: "CA", required: true %>
|
||||
<%= address_form.text_field :locality, label: t(".city"), placeholder: "Sacramento" %>
|
||||
<%= address_form.text_field :region, label: t(".state"), placeholder: "CA" %>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<%= address_form.text_field :postal_code, label: t(".postal_code"), placeholder: "95814" %>
|
||||
<%= address_form.text_field :country, label: t(".country"), placeholder: "USA", required: true %>
|
||||
<%= address_form.text_field :country, label: t(".country"), placeholder: "USA" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<%= vehicle_form.text_field :year, label: t(".year"), placeholder: t(".year_placeholder") %>
|
||||
<%= vehicle_form.text_field :mileage_value, label: t(".mileage"), placeholder: t(".mileage_placeholder") %>
|
||||
<%= vehicle_form.number_field :year, label: t(".year"), placeholder: t(".year_placeholder"), min: 1900, max: Time.current.year %>
|
||||
<%= vehicle_form.number_field :mileage_value, label: t(".mileage"), placeholder: t(".mileage_placeholder"), min: 0 %>
|
||||
<%= vehicle_form.select :mileage_unit,
|
||||
[["Miles", "mi"], ["Kilometers", "km"]],
|
||||
{ label: t(".mileage_unit") } %>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<%# locals: (account:) %>
|
||||
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<%= summary_card title: t(".amount_owed") do %>
|
||||
<%= format_money(account.balance_money) %>
|
||||
<% end %>
|
||||
|
||||
<%= summary_card title: t(".available_credit") do %>
|
||||
<%= format_money(account.credit_card.available_credit_money) || t(".unknown") %>
|
||||
<% end %>
|
||||
|
||||
<%= summary_card title: t(".minimum_payment") do %>
|
||||
<%= format_money(account.credit_card.minimum_payment_money || Money.new(0, account.currency)) %>
|
||||
<% end %>
|
||||
|
||||
<%= summary_card title: t(".apr") do %>
|
||||
<%= account.credit_card.apr ? number_to_percentage(account.credit_card.apr, precision: 2) : t(".unknown") %>
|
||||
<% end %>
|
||||
|
||||
<%= summary_card title: t(".expiration_date") do %>
|
||||
<%= account.credit_card.expiration_date ? l(account.credit_card.expiration_date, format: :long) : t(".unknown") %>
|
||||
<% end %>
|
||||
|
||||
<%= summary_card title: t(".annual_fee") do %>
|
||||
<%= format_money(account.credit_card.annual_fee_money || Money.new(0, account.currency)) %>
|
||||
<% end %>
|
||||
</div>
|
||||
45
app/views/accounts/accountables/loan/_overview.html.erb
Normal file
45
app/views/accounts/accountables/loan/_overview.html.erb
Normal file
@@ -0,0 +1,45 @@
|
||||
<%# locals: (account:) %>
|
||||
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<%= summary_card title: t(".original_principal") do %>
|
||||
<%= format_money account.original_balance %>
|
||||
<% end %>
|
||||
|
||||
<%= summary_card title: t(".remaining_principal") do %>
|
||||
<%= format_money account.balance_money %>
|
||||
<% end %>
|
||||
|
||||
<%= summary_card title: t(".interest_rate") do %>
|
||||
<% if account.loan.interest_rate.present? %>
|
||||
<%= number_to_percentage(account.loan.interest_rate, precision: 2) %>
|
||||
<% else %>
|
||||
<%= t(".unknown") %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= summary_card title: t(".monthly_payment") do %>
|
||||
<% if account.loan.rate_type.present? && account.loan.rate_type != 'fixed' %>
|
||||
<%= t(".not_applicable") %>
|
||||
<% elsif account.loan.rate_type == 'fixed' && account.loan.monthly_payment.present? %>
|
||||
<%= format_money(account.loan.monthly_payment) %>
|
||||
<% else %>
|
||||
<%= t(".unknown") %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= summary_card title: t(".term") do %>
|
||||
<% if account.loan.term_months.present? %>
|
||||
<% if account.loan.term_months < 12 %>
|
||||
<%= pluralize(account.loan.term_months, "month") %>
|
||||
<% else %>
|
||||
<%= pluralize(account.loan.term_months / 12, "year") %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= t(".unknown") %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= summary_card title: t(".type") do %>
|
||||
<%= account.loan.rate_type&.titleize || t(".unknown") %>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -1,20 +1,15 @@
|
||||
<%# locals: (account:) %>
|
||||
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div class="rounded-xl bg-white shadow-xs border border-alpha-black-25 p-4">
|
||||
<h4 class="text-gray-500 text-sm"><%= t(".market_value") %></h4>
|
||||
<p class="text-xl font-medium text-gray-900"><%= format_money(account.balance_money) %></p>
|
||||
</div>
|
||||
<%= summary_card title: t(".market_value") do %>
|
||||
<%= format_money(account.balance_money) %>
|
||||
<% end %>
|
||||
|
||||
<div class="rounded-xl bg-white shadow-xs border border-alpha-black-25 p-4">
|
||||
<h4 class="text-gray-500 text-sm"><%= t(".purchase_price") %></h4>
|
||||
<p class="text-xl font-medium text-gray-900">
|
||||
<%= account.property.purchase_price ? format_money(account.property.purchase_price) : t(".unknown") %>
|
||||
</p>
|
||||
</div>
|
||||
<%= summary_card title: t(".purchase_price") do %>
|
||||
<%= account.property.purchase_price ? format_money(account.property.purchase_price) : t(".unknown") %>
|
||||
<% end %>
|
||||
|
||||
<div class="rounded-xl bg-white shadow-xs border border-alpha-black-25 p-4">
|
||||
<h4 class="text-gray-500 text-sm flex items-center gap-1"><%= t(".trend") %></h4>
|
||||
<%= summary_card title: t(".trend") do %>
|
||||
<div class="flex items-center gap-1" style="color: <%= account.property.trend.color %>">
|
||||
<p class="text-xl font-medium">
|
||||
<%= account.property.trend.value %>
|
||||
@@ -22,19 +17,13 @@
|
||||
|
||||
<p>(<%= account.property.trend.percent %>%)</p>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="rounded-xl bg-white shadow-xs border border-alpha-black-25 p-4">
|
||||
<h4 class="text-gray-500 text-sm"><%= t(".year_built") %></h4>
|
||||
<p class="text-xl font-medium text-gray-900">
|
||||
<%= account.property.year_built || t(".unknown") %>
|
||||
</p>
|
||||
</div>
|
||||
<%= summary_card title: t(".year_built") do %>
|
||||
<%= account.property.year_built || t(".unknown") %>
|
||||
<% end %>
|
||||
|
||||
<div class="rounded-xl bg-white shadow-xs border border-alpha-black-25 p-4">
|
||||
<h4 class="text-gray-500 text-sm"><%= t(".living_area") %></h4>
|
||||
<p class="text-xl font-medium text-gray-900">
|
||||
<%= account.property.area || t(".unknown") %>
|
||||
</p>
|
||||
</div>
|
||||
<%= summary_card title: t(".living_area") do %>
|
||||
<%= account.property.area || t(".unknown") %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -1,43 +1,27 @@
|
||||
<%# locals: (account:) %>
|
||||
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div class="rounded-xl bg-white shadow-xs border border-alpha-black-25 p-4">
|
||||
<h4 class="text-gray-500 text-sm"><%= t(".make_model") %></h4>
|
||||
<p class="text-xl font-medium text-gray-900">
|
||||
<%= [account.vehicle.make, account.vehicle.model].compact.join(" ").presence || t(".unknown") %>
|
||||
</p>
|
||||
</div>
|
||||
<%= summary_card title: t(".make_model") do %>
|
||||
<%= [account.vehicle.make, account.vehicle.model].compact.join(" ").presence || t(".unknown") %>
|
||||
<% end %>
|
||||
|
||||
<div class="rounded-xl bg-white shadow-xs border border-alpha-black-25 p-4">
|
||||
<h4 class="text-gray-500 text-sm"><%= t(".year") %></h4>
|
||||
<p class="text-xl font-medium text-gray-900">
|
||||
<%= account.vehicle.year || t(".unknown") %>
|
||||
</p>
|
||||
</div>
|
||||
<%= summary_card title: t(".year") do %>
|
||||
<%= account.vehicle.year || t(".unknown") %>
|
||||
<% end %>
|
||||
|
||||
<div class="rounded-xl bg-white shadow-xs border border-alpha-black-25 p-4">
|
||||
<h4 class="text-gray-500 text-sm flex items-center gap-1"><%= t(".mileage") %></h4>
|
||||
<p class="text-xl font-medium text-gray-900">
|
||||
<%= account.vehicle.mileage || t(".unknown") %>
|
||||
</p>
|
||||
</div>
|
||||
<%= summary_card title: t(".mileage") do %>
|
||||
<%= account.vehicle.mileage || t(".unknown") %>
|
||||
<% end %>
|
||||
|
||||
<div class="rounded-xl bg-white shadow-xs border border-alpha-black-25 p-4">
|
||||
<h4 class="text-gray-500 text-sm"><%= t(".purchase_price") %></h4>
|
||||
<p class="text-xl font-medium text-gray-900">
|
||||
<%= format_money account.vehicle.purchase_price %>
|
||||
</p>
|
||||
</div>
|
||||
<%= summary_card title: t(".purchase_price") do %>
|
||||
<%= format_money account.vehicle.purchase_price %>
|
||||
<% end %>
|
||||
|
||||
<div class="rounded-xl bg-white shadow-xs border border-alpha-black-25 p-4">
|
||||
<h4 class="text-gray-500 text-sm"><%= t(".current_price") %></h4>
|
||||
<p class="text-xl font-medium text-gray-900">
|
||||
<%= format_money account.balance_money %>
|
||||
</p>
|
||||
</div>
|
||||
<%= summary_card title: t(".current_price") do %>
|
||||
<%= format_money account.balance_money %>
|
||||
<% end %>
|
||||
|
||||
<div class="rounded-xl bg-white shadow-xs border border-alpha-black-25 p-4">
|
||||
<h4 class="text-gray-500 text-sm"><%= t(".trend") %></h4>
|
||||
<%= summary_card title: t(".trend") do %>
|
||||
<div class="flex items-center gap-1" style="color: <%= account.vehicle.trend.color %>">
|
||||
<p class="text-xl font-medium">
|
||||
<%= account.vehicle.trend.value %>
|
||||
@@ -45,5 +29,5 @@
|
||||
|
||||
<p>(<%= account.vehicle.trend.percent %>%)</p>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -8,15 +8,15 @@
|
||||
<div class="flex flex-col p-2 text-sm grow">
|
||||
<button hidden data-controller="hotkey" data-hotkey="k,K,ArrowUp,ArrowLeft" data-action="list-keyboard-navigation#focusPrevious">Previous</button>
|
||||
<button hidden data-controller="hotkey" data-hotkey="j,J,ArrowDown,ArrowRight" data-action="list-keyboard-navigation#focusNext">Next</button>
|
||||
<%= render "account_type", type: Depository.new, bg_color: "bg-blue-50", text_color: "text-blue-500", icon: "landmark" %>
|
||||
<%= render "account_type", type: Investment.new, bg_color: "bg-green-50", text_color: "text-green-500", icon: "line-chart" %>
|
||||
<%= render "account_type", type: Crypto.new, bg_color: "bg-green-50", text_color: "text-green-500", icon: "bitcoin" %>
|
||||
<%= render "account_type", type: Property.new, bg_color: "bg-pink-50", text_color: "text-pink-500", icon: "home" %>
|
||||
<%= render "account_type", type: Vehicle.new, bg_color: "bg-indigo-50", text_color: "text-indigo-500", icon: "car-front" %>
|
||||
<%= render "account_type", type: CreditCard.new, bg_color: "bg-violet-50", text_color: "text-violet-500", icon: "credit-card" %>
|
||||
<%= render "account_type", type: Loan.new, bg_color: "bg-yellow-50", text_color: "text-yellow-500", icon: "hand-coins" %>
|
||||
<%= render "account_type", type: OtherAsset.new, bg_color: "bg-green-50", text_color: "text-green-500", icon: "plus" %>
|
||||
<%= render "account_type", type: OtherLiability.new, bg_color: "bg-red-50", text_color: "text-red-500", icon: "minus" %>
|
||||
<%= render "account_type", type: Depository.new, bg_color: "bg-blue-500/5", text_color: "text-blue-500", icon: "landmark" %>
|
||||
<%= render "account_type", type: Investment.new, bg_color: "bg-green-500/5", text_color: "text-green-500", icon: "line-chart" %>
|
||||
<%= render "account_type", type: Crypto.new, bg_color: "bg-orange-500/5", text_color: "text-orange-500", icon: "bitcoin" %>
|
||||
<%= render "account_type", type: Property.new, bg_color: "bg-pink-500/5", text_color: "text-pink-500", icon: "home" %>
|
||||
<%= render "account_type", type: Vehicle.new, bg_color: "bg-cyan-500/5", text_color: "text-cyan-500", icon: "car-front" %>
|
||||
<%= render "account_type", type: CreditCard.new, bg_color: "bg-violet-500/5", text_color: "text-violet-500", icon: "credit-card" %>
|
||||
<%= render "account_type", type: Loan.new, bg_color: "bg-yellow-500/5", text_color: "text-yellow-500", icon: "hand-coins" %>
|
||||
<%= render "account_type", type: OtherAsset.new, bg_color: "bg-green-500/5", text_color: "text-green-500", icon: "plus" %>
|
||||
<%= render "account_type", type: OtherLiability.new, bg_color: "bg-red-500/5", text_color: "text-red-500", icon: "minus" %>
|
||||
</div>
|
||||
<div class="border-t border-alpha-black-25 p-4 text-gray-500 text-sm flex justify-between">
|
||||
<div class="flex space-x-5">
|
||||
@@ -46,8 +46,13 @@
|
||||
<button hidden data-controller="hotkey" data-hotkey="k,K,ArrowUp,ArrowLeft" data-action="list-keyboard-navigation#focusPrevious">Previous</button>
|
||||
<button hidden data-controller="hotkey" data-hotkey="j,J,ArrowDown,ArrowRight" data-action="list-keyboard-navigation#focusNext">Next</button>
|
||||
<%= render "entry_method", type: @account.accountable, text: "Enter account balance manually", icon: "keyboard" %>
|
||||
<%= link_to new_import_path, class: "flex items-center gap-4 w-full text-center focus:outline-none focus:bg-gray-50 border border-transparent focus:border focus:border-gray-200 px-2 hover:bg-gray-50 rounded-lg p-2" do %>
|
||||
<span class="flex w-8 h-8 shrink-0 grow-0 items-center justify-center rounded-lg bg-alpha-black-50 shadow-[inset_0_0_0_1px_rgba(0,0,0,0.02)]">
|
||||
<%= lucide_icon("sheet", class: "text-gray-500 w-5 h-5") %>
|
||||
</span>
|
||||
Upload CSV
|
||||
<% end %>
|
||||
<%= render "entry_method", type: @account.accountable, text: "Securely link bank account with data provider (coming soon)", icon: "link-2", disabled: true %>
|
||||
<%= render "entry_method", type: @account.accountable, text: "Upload spreadsheet (coming soon)", icon: "sheet", disabled: true %>
|
||||
</div>
|
||||
<div class="border-t border-alpha-black-25 p-4 text-gray-500 text-sm flex justify-between">
|
||||
<div class="flex space-x-5">
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
<div>
|
||||
<h2 class="font-medium text-xl"><%= @account.name %></h2>
|
||||
|
||||
<% if @account.property? && @account.property.address %>
|
||||
<% if @account.property? && @account.property.address&.line1.present? %>
|
||||
<p class="text-gray-500"><%= @account.property.address %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<%= button_to sync_account_path(@account), method: :post, class: "flex items-center gap-2", title: "Sync Account" do %>
|
||||
<%= lucide_icon "refresh-cw", class: "w-4 h-4 text-gray-900 hover:text-gray-500" %>
|
||||
<%= lucide_icon "refresh-cw", class: "w-4 h-4 text-gray-500 hover:text-gray-400" %>
|
||||
<% end %>
|
||||
|
||||
<%= contextual_menu do %>
|
||||
@@ -27,7 +27,7 @@
|
||||
<span><%= t(".edit") %></span>
|
||||
<% end %>
|
||||
|
||||
<%= link_to new_import_path(account_id: @account.id),
|
||||
<%= link_to new_import_path,
|
||||
class: "block w-full py-2 px-3 space-x-2 text-gray-900 hover:bg-gray-50 flex items-center rounded-lg" do %>
|
||||
<%= lucide_icon "download", class: "w-5 h-5 text-gray-500" %>
|
||||
|
||||
@@ -60,11 +60,15 @@
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-1">
|
||||
<div>
|
||||
<%= tag.p t(".total_value"), class: "text-sm font-medium text-gray-500" %>
|
||||
<% if @account.asset? %>
|
||||
<%= tag.p t(".total_value"), class: "text-sm font-medium text-gray-500" %>
|
||||
<% else %>
|
||||
<%= tag.p t(".total_owed"), class: "text-sm font-medium text-gray-500" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= render "tooltip", account: @account if @account.investment? %>
|
||||
</div>
|
||||
<%= tag.p format_money(@account.value, precision: 0), class: "text-gray-900 text-3xl font-medium" %>
|
||||
<%= tag.p format_money(@account.value), class: "text-gray-900 text-3xl font-medium" %>
|
||||
<div>
|
||||
<% if @series.trend.direction.flat? %>
|
||||
<%= tag.span t(".no_change"), class: "text-gray-500" %>
|
||||
|
||||
@@ -2,75 +2,89 @@
|
||||
|
||||
<%= render "header" %>
|
||||
|
||||
<% if @accounts.empty? %>
|
||||
<%= render "shared/no_account_empty_state" %>
|
||||
<% else %>
|
||||
<div class="bg-white rounded-xl shadow-xs border border-alpha-black-100 flex divide-x divide-gray-200">
|
||||
<div class="w-1/2 p-4 flex items-stretch justify-between">
|
||||
<div class="space-y-2 grow">
|
||||
<%= render partial: "shared/value_heading", locals: {
|
||||
<div class="bg-white rounded-xl shadow-xs border border-alpha-black-100 flex divide-x divide-gray-200">
|
||||
<div class="w-1/2 p-4 flex items-stretch justify-between">
|
||||
<div class="space-y-2 grow">
|
||||
<%= render partial: "shared/value_heading", locals: {
|
||||
label: "Assets",
|
||||
period: @period,
|
||||
value: Current.family.assets,
|
||||
trend: @asset_series.trend
|
||||
} %>
|
||||
</div>
|
||||
<div
|
||||
</div>
|
||||
<div
|
||||
id="assetsChart"
|
||||
class="h-full w-2/5"
|
||||
data-controller="time-series-chart"
|
||||
data-time-series-chart-data-value="<%= @asset_series.to_json %>"
|
||||
data-time-series-chart-use-labels-value="false"></div>
|
||||
</div>
|
||||
<div class="w-1/2 p-4 flex items-stretch justify-between">
|
||||
<div class="space-y-2 grow">
|
||||
<%= render partial: "shared/value_heading", locals: {
|
||||
</div>
|
||||
<div class="w-1/2 p-4 flex items-stretch justify-between">
|
||||
<div class="space-y-2 grow">
|
||||
<%= render partial: "shared/value_heading", locals: {
|
||||
label: "Liabilities",
|
||||
period: @period,
|
||||
size: "md",
|
||||
value: Current.family.liabilities,
|
||||
trend: @liability_series.trend
|
||||
} %>
|
||||
</div>
|
||||
<div
|
||||
</div>
|
||||
<div
|
||||
id="liabilitiesChart"
|
||||
class="h-full w-2/5"
|
||||
data-controller="time-series-chart"
|
||||
data-time-series-chart-data-value="<%= @liability_series.to_json %>"
|
||||
data-time-series-chart-use-labels-value="false"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 bg-white rounded-xl shadow-xs border border-alpha-black-25 space-y-4">
|
||||
<div class="flex justify-between items-center mb-5">
|
||||
<h2 class="text-lg font-medium text-gray-900">Assets</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= link_to new_account_path, class: "btn btn--secondary flex items-center gap-1", data: { turbo_frame: "modal" } do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5 text-gray-500") %>
|
||||
<p><%= t(".new") %></p>
|
||||
<% end %>
|
||||
<%= form_with url: summary_accounts_path, method: :get, data: { controller: "auto-submit-form" } do |form| %>
|
||||
<%= period_select form: form, selected: @period.name %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 bg-white rounded-xl shadow-xs border border-alpha-black-25 space-y-4">
|
||||
<div class="flex justify-between items-center mb-5">
|
||||
<h2 class="text-lg font-medium text-gray-900">Assets</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= link_to new_account_path, class: "flex items-center gap-1 p-2 pr-3 text-gray-900 text-sm font-medium bg-gray-50 rounded-lg hover:bg-gray-100", data: { turbo_frame: "modal" } do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5 text-gray-500") %>
|
||||
<p><%= t(".new") %></p>
|
||||
<% end %>
|
||||
<%= form_with url: summary_accounts_path, method: :get, data: { controller: "auto-submit-form" } do |form| %>
|
||||
<%= period_select form: form, selected: @period.name %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if @account_groups[:assets].children.any? %>
|
||||
<%= render partial: "pages/account_percentages_bar", locals: { account_groups: @account_groups[:assets].children } %>
|
||||
<%= render partial: "pages/account_percentages_table", locals: { account_groups: @account_groups[:assets].children } %>
|
||||
</div>
|
||||
<div class="p-4 bg-white rounded-xl shadow-xs border border-alpha-black-25 space-y-4">
|
||||
<div class="flex justify-between items-center mb-5">
|
||||
<h2 class="text-lg font-medium text-gray-900">Liabilities</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= link_to new_account_path, class: "flex items-center gap-1 p-2 pr-3 text-gray-900 text-sm font-medium bg-gray-50 rounded-lg hover:bg-gray-100", data: { turbo_frame: "modal" } do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5 text-gray-500") %>
|
||||
<p><%= t(".new") %></p>
|
||||
<% end %>
|
||||
<%= form_with url: summary_accounts_path, method: :get, data: { controller: "auto-submit-form" } do |form| %>
|
||||
<%= period_select form: form, selected: @period.name %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="py-20 flex flex-col items-center">
|
||||
<%= lucide_icon "blocks", class: "w-6 h-6 shrink-0 text-gray-500" %>
|
||||
<p class="text-gray-900 text-sm font-medium mb-1 mt-4"><%= t(".no_assets") %></p>
|
||||
<p class="text-gray-500 text-sm max-w-xs text-center"><%= t(".no_assets_description") %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="p-4 bg-white rounded-xl shadow-xs border border-alpha-black-25 space-y-4">
|
||||
<div class="flex justify-between items-center mb-5">
|
||||
<h2 class="text-lg font-medium text-gray-900">Liabilities</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= link_to new_account_path, class: "btn btn--secondary flex items-center gap-1", data: { turbo_frame: "modal" } do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5 text-gray-500") %>
|
||||
<p><%= t(".new") %></p>
|
||||
<% end %>
|
||||
<%= form_with url: summary_accounts_path, method: :get, data: { controller: "auto-submit-form" } do |form| %>
|
||||
<%= period_select form: form, selected: @period.name %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if @account_groups[:liabilities].children.any? %>
|
||||
<%= render partial: "pages/account_percentages_bar", locals: { account_groups: @account_groups[:liabilities].children } %>
|
||||
<%= render partial: "pages/account_percentages_table", locals: { account_groups: @account_groups[:liabilities].children } %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<div class="py-20 flex flex-col items-center">
|
||||
<%= lucide_icon "scale", class: "w-6 h-6 shrink-0 text-gray-500" %>
|
||||
<p class="text-gray-900 text-sm font-medium mb-1 mt-4"><%= t(".no_liabilities") %></p>
|
||||
<p class="text-gray-500 text-sm max-w-xs text-center"><%= t(".no_liabilities_description") %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
<% category ||= null_category %>
|
||||
|
||||
<div>
|
||||
<span class="flex items-center gap-1 text-sm font-medium rounded-full px-1.5 py-1 border border-alpha-black-25"
|
||||
<span class="flex items-center gap-1 text-sm font-medium rounded-full px-1.5 py-1 border"
|
||||
style="
|
||||
background-color: color-mix(in srgb, <%= category.color %> 5%, white);
|
||||
border-color: color-mix(in srgb, <%= category.color %> 30%, white);
|
||||
color: <%= category.color %>;">
|
||||
<%= category.name %>
|
||||
</span>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<div class="bg-white border border-alpha-black-100 rounded-lg p-3 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= lucide_icon "alert-triangle", class: "w-4 h-4 text-red-500" %>
|
||||
<p class="text-red-500">You have errors in your data</p>
|
||||
<p class="text-red-500 text-sm"><%= t(".errors_notice") %></p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
|
||||
@@ -3,12 +3,28 @@
|
||||
<% mappings = mapping_class.for_import(import) %>
|
||||
<% is_last_step = step_idx == import.mapping_steps.count - 1 %>
|
||||
|
||||
<% if mapping_class == Import::AccountMapping %>
|
||||
<% if import.requires_account? %>
|
||||
<div class="flex items-center justify-between p-4 mb-4 gap-4 text-gray-500 bg-red-100 border border-red-200 rounded-lg w-[650px]">
|
||||
<%= tag.p t(".no_accounts"), class: "text-sm" %>
|
||||
|
||||
<%= link_to t(".create_account"), new_account_path, class: "btn btn--primary whitespace-nowrap", data: { turbo_frame: :modal } %>
|
||||
</div>
|
||||
<% elsif import.has_unassigned_account? %>
|
||||
<div class="flex items-center justify-between p-4 mb-4 gap-4 text-gray-500 bg-yellow-100 border border-yellow-200 rounded-lg w-[650px]">
|
||||
<%= tag.p t(".unassigned_account"), class: "text-sm" %>
|
||||
|
||||
<%= link_to t(".create_account"), new_account_path, class: "btn btn--primary whitespace-nowrap", data: { turbo_frame: :modal } %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="bg-gray-25 rounded-xl p-1 space-y-1 w-[650px]">
|
||||
<div class="grid grid-cols-3 gap-2 text-xs font-medium text-gray-500 uppercase px-5 py-3">
|
||||
<p>CSV <%= mapping_label(mapping_class) %></p>
|
||||
<p>Maybe <%= mapping_label(mapping_class) %></p>
|
||||
<p class="justify-self-end">Rows</p>
|
||||
<p><%= t(".csv_mapping_label", mapping: mapping_label(mapping_class)) %></p>
|
||||
<p><%= t(".maybe_mapping_label", mapping: mapping_label(mapping_class)) %></p>
|
||||
<p class="justify-self-end"><%= t(".rows_label") %></p>
|
||||
</div>
|
||||
|
||||
<div class="border border-alpha-black-25 rounded-md shadow-xs divide-y divide-alpha-black-100 text-sm">
|
||||
|
||||
@@ -28,6 +28,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-screen-md mx-auto flex justify-center">
|
||||
<div class="max-w-screen-md mx-auto flex flex-col items-center">
|
||||
<%= render partial: "import/confirms/mappings", locals: { import: @import, mapping_class: step_mapping_class, step_idx: step_idx } %>
|
||||
</div>
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
<li><%= t(".instructions_2") %></li>
|
||||
<li><%= t(".instructions_3") %></li>
|
||||
<li><%= t(".instructions_4") %></li>
|
||||
<li><%= t(".instructions_5") %></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="flex justify-center items-center py-20">
|
||||
<div class="text-center flex flex-col items-center max-w-[300px] gap-4">
|
||||
<p class="text-gray-900 mb-1 font-medium text-sm"><%= t(".message") %></p>
|
||||
<%= link_to new_import_path(enable_type_selector: true), class: "btn btn--primary flex items-center gap-2", data: { turbo_frame: "modal" } do %>
|
||||
<%= link_to new_import_path, class: "btn btn--primary flex items-center gap-2", data: { turbo_frame: "modal" } do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||
<span><%= t(".new") %></span>
|
||||
<% end %>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<div class="text-white w-9 h-9 bg-gray-400 rounded-full flex items-center justify-center text-lg uppercase"><%= Current.user.initial %></div>
|
||||
<% end %>
|
||||
</button>
|
||||
<div data-menu-target="content" class="hidden absolute w-[240px] z-10 top-10 left-[255px] top-[72px] bg-white rounded-sm shadow-xs border border-alpha-black-25">
|
||||
<div data-menu-target="content" class="hidden absolute w-[240px] z-10 left-[255px] top-[72px] bg-white rounded-sm shadow-xs border border-alpha-black-25">
|
||||
<div class="p-3 flex items-center gap-3">
|
||||
<% if profile_image_attached %>
|
||||
<div class="text-white shrink-0 w-9 h-9">
|
||||
@@ -57,9 +57,16 @@
|
||||
<%= lucide_icon("megaphone", class: "w-5 h-5 text-gray-500 shrink-0") %>
|
||||
<span class="text-gray-900 text-sm">Feedback</span>
|
||||
<% end %>
|
||||
<%= link_to "https://link.maybe.co/discord", class: "flex gap-2 items-center hover:bg-gray-50 rounded-lg px-3 py-2" do %>
|
||||
<%= lucide_icon("message-square-more", class: "w-5 h-5 text-gray-500 shrink-0") %>
|
||||
<span class="text-gray-900 text-sm">Contact</span>
|
||||
<% if self_hosted? %>
|
||||
<%= link_to "https://link.maybe.co/discord", class: "flex gap-2 items-center hover:bg-gray-50 rounded-lg px-3 py-2" do %>
|
||||
<%= lucide_icon("message-square-more", class: "w-5 h-5 text-gray-500 shrink-0") %>
|
||||
<span class="text-gray-900 text-sm">Contact</span>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= link_to "mailto:hello@maybe.co", class: "flex gap-2 items-center hover:bg-gray-50 rounded-lg px-3 py-2", onclick: "Intercom('showNewMessage'); return false;" do %>
|
||||
<%= lucide_icon("message-square-more", class: "w-5 h-5 text-gray-500 shrink-0") %>
|
||||
<span class="text-gray-900 text-sm">Contact</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="p-1">
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
<meta name="viewport"
|
||||
content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="Maybe">
|
||||
<meta name="msapplication-TileColor" content="#ffffff">
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<%# locals: (account_groups:) %>
|
||||
<div class="space-y-4">
|
||||
<div class="flex gap-1">
|
||||
<% account_groups.each do |group| %>
|
||||
<% account_groups.sort_by(&:percent_of_total).reverse.each do |group| %>
|
||||
<div class="h-1.5 rounded-sm w-12 <%= accountable_bg_class(group.name) %>" style="width: <%= group.percent_of_total %>%;"></div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<% account_groups.each do |group| %>
|
||||
<% account_groups.sort_by(&:percent_of_total).reverse.each do |group| %>
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<div class="h-2.5 w-2.5 rounded-full <%= accountable_bg_class(group.name) %>"></div>
|
||||
<p class="text-gray-500"><%= to_accountable_title(Accountable.from_type(group.name)) %></p>
|
||||
|
||||
@@ -15,6 +15,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white border border-alpha-black-25 shadow-xs rounded-lg divide-y divide-alpha-black-50">
|
||||
<%= render partial: "pages/account_group_disclosure", collection: account_groups, as: :accountable_group %>
|
||||
<%= render partial: "pages/account_group_disclosure", collection: account_groups.sort_by(&:percent_of_total).reverse, as: :accountable_group %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<%= image_tag @release_notes[:avatar], class: "rounded-full w-full h-full object-cover" %>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-gray-900 font-medium text-sm"><%= @release_notes[:name] %></div>
|
||||
<a class="text-gray-900 font-medium text-sm" href="https://github.com/<%= @release_notes[:username] %>"><%= "@#{@release_notes[:username]}" %></a>
|
||||
<div class="text-gray-500 text-sm"><%= @release_notes[:published_at].strftime("%B %d, %Y") %></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
<header class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="sr-only"><%= t(".title") %></h1>
|
||||
<p class="text-xl font-medium text-gray-900 mb-1"><%= t(".greeting", name: Current.user.first_name ) %></p>
|
||||
<p class="text-xl font-medium text-gray-900 mb-1">
|
||||
<%= Current.user.first_name.present? ? t(".greeting", name: Current.user.first_name ) : t(".fallback_greeting") %>
|
||||
</p>
|
||||
<% unless @accounts.blank? %>
|
||||
<p class="text-gray-500 text-sm"><%= t(".subtitle") %></p>
|
||||
<% end %>
|
||||
@@ -22,7 +24,9 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<% if @accounts.empty? %>
|
||||
<% if !Current.family.subscribed? && !self_hosted? %>
|
||||
<%= render "shared/subscribe_prompt" %>
|
||||
<% elsif @accounts.empty? %>
|
||||
<%= render "shared/no_account_empty_state" %>
|
||||
<% else %>
|
||||
<section class="flex gap-4">
|
||||
@@ -133,9 +137,11 @@
|
||||
</div>
|
||||
<div class="flex gap-1.5">
|
||||
<% @top_savers.first(3).each do |account| %>
|
||||
<%= link_to account, class: "border border-alpha-black-25 rounded-full p-1 pr-2 flex items-center gap-1 text-xs text-gray-900 font-medium hover:bg-gray-25" do %>
|
||||
<%= image_tag account_logo_url(account), class: "w-5 h-5" %>
|
||||
<span><%= account.savings_rate > 0 ? "+" : "-" %><%= number_to_percentage(account.savings_rate.abs * 100, precision: 2) %></span>
|
||||
<% unless account.savings_rate.infinite? %>
|
||||
<%= link_to account, class: "border border-alpha-black-25 rounded-full p-1 pr-2 flex items-center gap-1 text-xs text-gray-900 font-medium hover:bg-gray-25" do %>
|
||||
<%= image_tag account_logo_url(account), class: "w-5 h-5" %>
|
||||
<span><%= account.savings_rate > 0 ? "+" : "-" %><%= number_to_percentage(account.savings_rate.abs * 100, precision: 2) %></span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if @top_savers.count > 3 %>
|
||||
|
||||
56
app/views/pages/early_access.html.erb
Normal file
56
app/views/pages/early_access.html.erb
Normal file
@@ -0,0 +1,56 @@
|
||||
<!DOCTYPE html>
|
||||
<html class="h-full" lang="en">
|
||||
<head>
|
||||
<title><%= content_for(:title) || "🔒 Maybe Early Access" %></title>
|
||||
|
||||
<%= csrf_meta_tags %>
|
||||
<%= csp_meta_tag %>
|
||||
|
||||
<%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %>
|
||||
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
||||
|
||||
<%= javascript_importmap_tags %>
|
||||
<%= hotwire_livereload_tags if Rails.env.development? %>
|
||||
<%= turbo_refreshes_with method: :morph, scroll: :preserve %>
|
||||
|
||||
<meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="Maybe">
|
||||
<meta name="msapplication-TileColor" content="#ffffff">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
<%= yield :head %>
|
||||
</head>
|
||||
|
||||
<body class="subpixel-antialiased h-full bg-black text-white flex flex-col bg-top bg-no-repeat bg-cover" style="background-image: url('<%= asset_path("bg-grid.png") %>');">
|
||||
<div class="flex-grow flex items-center justify-center p-4 sm:p-6 md:p-8">
|
||||
<div class="w-full max-w-sm mx-auto text-center rounded-lg p-6">
|
||||
<%= image_tag "logo-squircle.svg", alt: "Maybe Logo", class: "w-16 h-16 sm:w-18 sm:h-18 mx-auto mb-6 sm:mb-8" %>
|
||||
<h1 class="text-2xl font-normal my-4">Maybe Early Access</h1>
|
||||
<% if @invite_codes_count > 0 %>
|
||||
<p class="text-base text-gray-400">There <%= @invite_codes_count == 1 ? "is" : "are" %> <span class="text-white"><%= @invite_codes_count %> invite <%= "code".pluralize(@invite_codes_count) %></span> remaining.</p>
|
||||
<div class="bg-gray-900 border border-gray-800 p-2 rounded-xl my-4">
|
||||
<p class="text-sm text-gray-400 mt-1 mb-3 sm:mb-4">Your invite code is <span class="font-mono text-white"><%= @invite_code.token %></span></p>
|
||||
<p><%= link_to "Sign up with this code", new_registration_path(invite: @invite_code.token), class: "block w-full bg-white text-black py-2 px-3 rounded-lg no-underline text-sm sm:text-base hover:bg-gray-200 transition duration-150" %></p>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-gray-400">You may need to refresh the page to get a new invite code if someone else claimed it before you.</p>
|
||||
|
||||
<p class="mt-4 sm:mt-6">
|
||||
<%= link_to early_access_path, class: "w-full block text-center justify-center inline-flex items-center text-white hover:bg-gray-800 p-2 rounded-md text-base transition duration-150", data: { turbo_method: :get } do %>
|
||||
<%= lucide_icon "refresh-cw", class: "w-4 h-4 sm:w-5 sm:h-5 mr-2 text-gray-400" %>
|
||||
<span>Refresh page</span>
|
||||
<% end %>
|
||||
</p>
|
||||
<% else %>
|
||||
<p class="text-base text-gray-400 mb-6 sm:mb-8">Sorry, there are <span class="text-white">no invite codes</span> remaining. Join our <%= link_to "Discord server", "https://link.maybe.co/discord", target: "_blank", class: "text-white hover:text-gray-300" %> to get notified when new invite codes are available.</p>
|
||||
<p><%= link_to "Join Discord server", "https://link.maybe.co/discord", target: "_blank", class: "bg-white text-black px-3 py-2 rounded-md no-underline text-base hover:bg-gray-200 transition duration-150" %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="pb-6 sm:pb-10 text-center text-gray-400 text-sm">
|
||||
©2024 Maybe Finance, Inc.
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -9,17 +9,23 @@
|
||||
<p class="text-sm text-gray-500 mb-4">Let us know if you have any specific feedback. Feel free to include links to videos or screenshots.</p>
|
||||
<div class="flex gap-2">
|
||||
<%= link_to "https://github.com/maybe-finance/maybe/discussions/categories/feature-requests", target: "_blank", rel: "noopener noreferrer", class: "w-1/3 flex flex-col items-center p-4 border border-alpha-black-25 rounded-xl hover:bg-gray-50" do %>
|
||||
<%= image_tag "github-icon.png", class: "w-8 h-8 mb-2" %>
|
||||
<%= image_tag "github-icon.svg", class: "w-8 h-8 mb-2" %>
|
||||
<span class="text-sm font-medium text-gray-900">Write a feature request</span>
|
||||
<% end %>
|
||||
|
||||
<%= link_to "https://github.com/maybe-finance/maybe/issues/new?assignees=&labels=bug&template=bug_report.md&title=", target: "_blank", rel: "noopener noreferrer", class: "w-1/3 flex flex-col items-center p-4 border border-alpha-black-25 rounded-xl hover:bg-gray-50" do %>
|
||||
<%= image_tag "github-icon.png", class: "w-8 h-8 mb-2" %>
|
||||
<span class="text-sm font-medium text-gray-900">File a bug report</span>
|
||||
<% if self_hosted? %>
|
||||
<%= link_to "https://github.com/maybe-finance/maybe/issues/new?assignees=&labels=bug&template=bug_report.md&title=", target: "_blank", rel: "noopener noreferrer", class: "w-1/3 flex flex-col items-center p-4 border border-alpha-black-25 rounded-xl hover:bg-gray-50" do %>
|
||||
<%= image_tag "github-icon.svg", class: "w-8 h-8 mb-2" %>
|
||||
<span class="text-sm font-medium text-gray-900">File a bug report</span>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= link_to "mailto:hello@maybe.co", class: "w-1/3 flex flex-col items-center p-4 border border-alpha-black-25 rounded-xl hover:bg-gray-50", onclick: "Intercom('showNewMessage'); return false;" do %>
|
||||
<%= lucide_icon "bug", class: "w-8 h-8 mb-2" %>
|
||||
<span class="text-sm font-medium text-gray-900">File a bug report</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= link_to "https://link.maybe.co/discord", target: "_blank", rel: "noopener noreferrer", class: "w-1/3 flex flex-col items-center p-4 border border-alpha-black-25 rounded-xl hover:bg-gray-50" do %>
|
||||
<%= image_tag "discord-icon.png", class: "w-8 h-8 mb-2" %>
|
||||
<%= image_tag "discord-icon.svg", class: "w-8 h-8 mb-2" %>
|
||||
<span class="text-sm font-medium text-gray-900">Discuss Maybe with others</span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
<%= link_to t(".cta"), edit_password_reset_url(token: params[:token]) %>
|
||||
<p><%= t(".request_made") %></p>
|
||||
|
||||
<p><%= link_to t(".cta"), edit_password_reset_url(token: params[:token]) %></p>
|
||||
|
||||
<p><%= t(".ignore_if_not_requested") %></p>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<%= form.password_field :password, autocomplete: "new-password", required: "required", label: true %>
|
||||
<%= form.password_field :password_confirmation, autocomplete: "new-password", required: "required", label: true %>
|
||||
<% if invite_code_required? %>
|
||||
<%= form.password_field :invite_code, required: "required", label: true %>
|
||||
<%= form.text_field :invite_code, required: "required", label: true, value: params[:invite] %>
|
||||
<% end %>
|
||||
<%= form.submit %>
|
||||
<% end %>
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
<%= sidebar_link_to t(".self_hosting_label"), settings_hosting_path, icon: "database" %>
|
||||
</li>
|
||||
<% end %>
|
||||
<li>
|
||||
<%= sidebar_link_to t(".billing_label"), settings_billing_path, icon: "circle-dollar-sign" %>
|
||||
</li>
|
||||
<li>
|
||||
<%= sidebar_link_to t(".accounts_label"), accounts_path, icon: "layers" %>
|
||||
</li>
|
||||
|
||||
16
app/views/settings/billings/show.html.erb
Normal file
16
app/views/settings/billings/show.html.erb
Normal file
@@ -0,0 +1,16 @@
|
||||
<% content_for :sidebar do %>
|
||||
<%= render "settings/nav" %>
|
||||
<% end %>
|
||||
|
||||
<div class="space-y-4">
|
||||
<h1 class="text-gray-900 text-xl font-medium mb-4"><%= t(".page_title") %></h1>
|
||||
<%= settings_section title: t(".subscription_title"), subtitle: t(".subscription_subtitle") do %>
|
||||
<% if Current.family.stripe_plan_id.blank? %>
|
||||
<%= link_to t(".subscribe_button"), new_subscription_path, class: "w-fit flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-2", data: { turbo: false } %>
|
||||
<% else %>
|
||||
<%= link_to t(".manage_subscription_button"), subscription_path, class: "w-fit flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-2", data: { turbo: false } %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= settings_nav_footer %>
|
||||
</div>
|
||||
16
app/views/shared/_subscribe_prompt.html.erb
Normal file
16
app/views/shared/_subscribe_prompt.html.erb
Normal file
@@ -0,0 +1,16 @@
|
||||
<div class="flex justify-center items-center h-[800px]">
|
||||
<div class="text-center flex flex-col gap-4 items-center max-w-[300px]">
|
||||
<%= lucide_icon "circle-fading-arrow-up", class: "w-8 h-8 text-green-500" %>
|
||||
|
||||
<div class="space-y-1 text-sm">
|
||||
<p class="text-gray-900 font-medium"><%= t(".title") %></p>
|
||||
<p class="text-gray-500"><%= t(".subtitle") %></p>
|
||||
<p class="text-gray-400 text-xs"><%= t(".guarantee") %></p>
|
||||
</div>
|
||||
|
||||
<%= link_to new_subscription_path, class: "btn btn--primary flex items-center gap-1" do %>
|
||||
<%= lucide_icon("credit-card", class: "w-5 h-5") %>
|
||||
<span><%= t(".subscribe") %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user