Compare commits
158 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bac2e64c19 | ||
|
|
4866a4f8e4 | ||
|
|
027c18297b | ||
|
|
800eb4c146 | ||
|
|
b2a56aefc1 | ||
|
|
46131fb496 | ||
|
|
49c353e10c | ||
|
|
a59ca5b7c6 | ||
|
|
ee79016e2a | ||
|
|
13cf4d70df | ||
|
|
48e306a614 | ||
|
|
a9daba16c1 | ||
|
|
2cba5177ba | ||
|
|
13bec4599f | ||
|
|
565103caf3 | ||
|
|
c456950de8 | ||
|
|
9ec94cd1fa | ||
|
|
d73e7eacce | ||
|
|
890638e06d | ||
|
|
14fd5913fe | ||
|
|
e026f68895 | ||
|
|
1b8064b9fd | ||
|
|
d592495be5 | ||
|
|
c3248cd796 | ||
|
|
76f2714006 | ||
|
|
a9b61a655b | ||
|
|
955f211fe0 | ||
|
|
570a0c7ff6 | ||
|
|
de9ffa7ca0 | ||
|
|
b5666ad7a9 | ||
|
|
fc603a1733 | ||
|
|
6c503e4d26 | ||
|
|
57a87f2850 | ||
|
|
84f069448a | ||
|
|
25e9bd4c60 | ||
|
|
a4adfed82b | ||
|
|
03e92e63a5 | ||
|
|
c1034e6edf | ||
|
|
1c2f075053 | ||
|
|
571fc4db75 | ||
|
|
c8302a6d49 | ||
|
|
c309c8abf8 | ||
|
|
242eb5cea1 | ||
|
|
6996a225ba | ||
|
|
e641cfccd4 | ||
|
|
d1b506d16c | ||
|
|
81d604f3d4 | ||
|
|
fcb95207d7 | ||
|
|
743e291d56 | ||
|
|
6105f822b7 | ||
|
|
9cc9f42bdc | ||
|
|
8b672c4062 | ||
|
|
8befb8a8b0 | ||
|
|
f15875560e | ||
|
|
951a29d923 | ||
|
|
91eedfbd1b | ||
|
|
0af5faaa9f | ||
|
|
69f6d7f8ea | ||
|
|
cbba2ba675 | ||
|
|
3bc9da4105 | ||
|
|
9522a191de | ||
|
|
ed87023c0f | ||
|
|
3d7a74862d | ||
|
|
fc3695dda9 | ||
|
|
278d04a73a | ||
|
|
31ecd3ccd4 | ||
|
|
3ef67faf7e | ||
|
|
8ba04b0330 | ||
|
|
56ab092f6b | ||
|
|
b3ef995d1f | ||
|
|
a113d573d6 | ||
|
|
3b928775a8 | ||
|
|
31d9d926f7 | ||
|
|
154a1a971b | ||
|
|
e434ed0e1f | ||
|
|
2722254be9 | ||
|
|
455257bf51 | ||
|
|
f2739b79fb | ||
|
|
cee9692b35 | ||
|
|
18266c3352 | ||
|
|
c3400856c7 | ||
|
|
a0ad33e47c | ||
|
|
65d46397d7 | ||
|
|
905eb7bbe8 | ||
|
|
65db49273c | ||
|
|
12e4f1067d | ||
|
|
85779b4038 | ||
|
|
793bd852a0 | ||
|
|
09b269273a | ||
|
|
47288a1629 | ||
|
|
2b61821336 | ||
|
|
7946cd7819 | ||
|
|
e7f09e6f71 | ||
|
|
5e2b932648 | ||
|
|
5533b84895 | ||
|
|
c9917674aa | ||
|
|
cd91e66618 | ||
|
|
490f44589e | ||
|
|
bf695972e4 | ||
|
|
7d8028b505 | ||
|
|
c2561b5fb4 | ||
|
|
e5eb69bdc7 | ||
|
|
3cd364af09 | ||
|
|
277fb3dc39 | ||
|
|
439e50bb3e | ||
|
|
2141cbb041 | ||
|
|
d78f582af2 | ||
|
|
2adb54da99 | ||
|
|
45935db5f3 | ||
|
|
b75b41a5e2 | ||
|
|
2cc89195bf | ||
|
|
aa3342b0dc | ||
|
|
b611dfdf37 | ||
|
|
ba49fea89a | ||
|
|
89e107e36c | ||
|
|
d93fbbcaa8 | ||
|
|
e6403fab70 | ||
|
|
6baffe7539 | ||
|
|
1d20de770f | ||
|
|
73e184ad3d | ||
|
|
d3a6f7e0f0 | ||
|
|
9313620968 | ||
|
|
a4e87ffb4d | ||
|
|
728b10d08e | ||
|
|
a27b17deae | ||
|
|
1b654faf9a | ||
|
|
9b6a2cce56 | ||
|
|
5ff9012d3e | ||
|
|
da7f19d5ab | ||
|
|
a2e8fb5ce1 | ||
|
|
b074762809 | ||
|
|
3cc4cba2b3 | ||
|
|
cb752370cb | ||
|
|
720d7aedaf | ||
|
|
07264e86cb | ||
|
|
3c0fdd84ee | ||
|
|
263d65ea7e | ||
|
|
e8e100e1d8 | ||
|
|
c7c281073f | ||
|
|
4a3685f503 | ||
|
|
75a390f03e | ||
|
|
d4bfcfb6f4 | ||
|
|
b98f35af0e | ||
|
|
629565f7d8 | ||
|
|
4118cc8a31 | ||
|
|
61bf53f233 | ||
|
|
7f4c1755ef | ||
|
|
76decc06c3 | ||
|
|
f3bb80dde6 | ||
|
|
4ad28d6eff | ||
|
|
fa3b8b078c | ||
|
|
d4e7a983f4 | ||
|
|
7f7140b1cc | ||
|
|
437aa4bd39 | ||
|
|
eabec71f70 | ||
|
|
3bc960e6c1 | ||
|
|
57a81e44ef | ||
|
|
e357c0485f |
@@ -1,4 +1,4 @@
|
||||
ARG RUBY_VERSION=3.3.4
|
||||
ARG RUBY_VERSION=3.3.5
|
||||
FROM ruby:${RUBY_VERSION}-slim-bullseye
|
||||
|
||||
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
@@ -17,4 +17,8 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
RUN gem install bundler
|
||||
RUN gem install foreman
|
||||
|
||||
# Install Node.js 20
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||
&& apt-get install -y nodejs
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
@@ -10,5 +10,13 @@
|
||||
"remoteEnv": {
|
||||
"PATH": "/workspace/bin:${containerEnv:PATH}"
|
||||
},
|
||||
"postCreateCommand": "bundle install"
|
||||
"postCreateCommand": "bundle install && npm install",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"biomejs.biome",
|
||||
"EditorConfig.EditorConfig"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
23
.env.example
23
.env.example
@@ -1,9 +1,18 @@
|
||||
# ================================ PLEASE READ ==========================================
|
||||
# This file outlines all the possible environment variables supported by the Maybe app.
|
||||
#
|
||||
# This includes several features that are for our "hosted" version of Maybe, which most
|
||||
# open-source contributors won't need.
|
||||
#
|
||||
# If you are developing locally, you should be referencing `.env.local.example` instead.
|
||||
# =======================================================================================
|
||||
|
||||
# Custom port config
|
||||
# For users who have other applications listening at 3000, this allows them to set a value puma will listen to.
|
||||
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.
|
||||
# Exchange Rate & Stock Pricing API
|
||||
# This is used to convert between different currencies in the app. In addition, it fetches global stock prices. We use Synth, which is a Maybe product. You can sign up for a free account at synthfinance.com.
|
||||
SYNTH_API_KEY=
|
||||
|
||||
# SMTP Configuration
|
||||
@@ -101,4 +110,12 @@ GITHUB_REPO_BRANCH=main
|
||||
#
|
||||
STRIPE_PUBLISHABLE_KEY=
|
||||
STRIPE_SECRET_KEY=
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
|
||||
# ======================================================================================================
|
||||
# Plaid Configuration
|
||||
# ======================================================================================================
|
||||
#
|
||||
PLAID_CLIENT_ID=
|
||||
PLAID_SECRET=
|
||||
PLAID_ENV=
|
||||
5
.env.local.example
Normal file
5
.env.local.example
Normal file
@@ -0,0 +1,5 @@
|
||||
# To enable / disable self-hosting features.
|
||||
SELF_HOSTED=false
|
||||
|
||||
# Enable Synth market data (careful, this will use your API credits)
|
||||
SYNTH_API_KEY=yourapikeyhere
|
||||
8
.env.test
Normal file
8
.env.test
Normal file
@@ -0,0 +1,8 @@
|
||||
SELF_HOSTED=false
|
||||
SYNTH_API_KEY=fookey
|
||||
|
||||
# Set to true if you want SimpleCov reports generated
|
||||
COVERAGE=false
|
||||
|
||||
# Set to true to run test suite serially
|
||||
DISABLE_PARALLELIZATION=false
|
||||
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -20,6 +20,12 @@ Steps to reproduce the behavior:
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**What version of Maybe are you using?**
|
||||
This could be "Hosted" (i.e. app.maybefinance.com) or "Self-hosted". If "Self-hosted", please include the version you're currently on.
|
||||
|
||||
**What operating system and browser are you using?**
|
||||
The more info the better.
|
||||
|
||||
**Screenshots / Recordings**
|
||||
If applicable, add screenshots or short video recordings to help show the bug in more detail.
|
||||
|
||||
|
||||
20
.github/workflows/ci.yml
vendored
20
.github/workflows/ci.yml
vendored
@@ -52,6 +52,26 @@ jobs:
|
||||
- name: Lint code for consistent style
|
||||
run: bin/rubocop -f github
|
||||
|
||||
lint_js:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "npm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
shell: bash
|
||||
|
||||
- name: Lint/Format js code
|
||||
run: npm run lint
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -6,12 +6,13 @@
|
||||
|
||||
# Ignore bundler config.
|
||||
/.bundle
|
||||
/vendor/bundle
|
||||
|
||||
# Ignore all environment files (except templates).
|
||||
/.env*
|
||||
!/.env*.erb
|
||||
!.env.example
|
||||
!.env.test.example
|
||||
!.env.test
|
||||
!.env*.example
|
||||
|
||||
# Ignore all logfiles and tempfiles.
|
||||
/log/*
|
||||
@@ -43,7 +44,9 @@
|
||||
.idea
|
||||
|
||||
# Ignore VS Code
|
||||
.vscode
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
# Ignore macOS specific files
|
||||
*/.DS_Store
|
||||
@@ -59,4 +62,7 @@ compose-dev.yaml
|
||||
gcp-storage-keyfile.json
|
||||
|
||||
coverage
|
||||
.cursorrules
|
||||
.cursorrules
|
||||
|
||||
# Ignore node related files
|
||||
node_modules
|
||||
@@ -1 +1 @@
|
||||
3.3.4
|
||||
3.3.5
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# syntax = docker/dockerfile:1
|
||||
|
||||
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile
|
||||
ARG RUBY_VERSION=3.3.4
|
||||
ARG RUBY_VERSION=3.3.5
|
||||
FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim as base
|
||||
|
||||
# Rails app lives here
|
||||
|
||||
6
Gemfile
6
Gemfile
@@ -3,7 +3,7 @@ source "https://rubygems.org"
|
||||
ruby file: ".ruby-version"
|
||||
|
||||
# Rails
|
||||
gem "rails", "~> 7.2.1"
|
||||
gem "rails", "~> 7.2.2"
|
||||
|
||||
# Drivers
|
||||
gem "pg", "~> 1.5"
|
||||
@@ -21,6 +21,7 @@ gem "lucide-rails", github: "maybe-finance/lucide-rails"
|
||||
# Hotwire
|
||||
gem "stimulus-rails"
|
||||
gem "turbo-rails"
|
||||
gem "hotwire_combobox"
|
||||
|
||||
# Background Jobs
|
||||
gem "good_job"
|
||||
@@ -36,6 +37,7 @@ gem "image_processing", ">= 1.2"
|
||||
|
||||
# Other
|
||||
gem "bcrypt", "~> 3.1"
|
||||
gem "jwt"
|
||||
gem "faraday"
|
||||
gem "faraday-retry"
|
||||
gem "faraday-multipart"
|
||||
@@ -48,7 +50,7 @@ gem "csv"
|
||||
gem "redcarpet"
|
||||
gem "stripe"
|
||||
gem "intercom-rails"
|
||||
gem "holidays"
|
||||
gem "plaid"
|
||||
|
||||
group :development, :test do
|
||||
gem "debug", platforms: %i[mri windows]
|
||||
|
||||
291
Gemfile.lock
291
Gemfile.lock
@@ -8,29 +8,29 @@ GIT
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (7.2.1)
|
||||
actionpack (= 7.2.1)
|
||||
activesupport (= 7.2.1)
|
||||
actioncable (7.2.2)
|
||||
actionpack (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
zeitwerk (~> 2.6)
|
||||
actionmailbox (7.2.1)
|
||||
actionpack (= 7.2.1)
|
||||
activejob (= 7.2.1)
|
||||
activerecord (= 7.2.1)
|
||||
activestorage (= 7.2.1)
|
||||
activesupport (= 7.2.1)
|
||||
actionmailbox (7.2.2)
|
||||
actionpack (= 7.2.2)
|
||||
activejob (= 7.2.2)
|
||||
activerecord (= 7.2.2)
|
||||
activestorage (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
mail (>= 2.8.0)
|
||||
actionmailer (7.2.1)
|
||||
actionpack (= 7.2.1)
|
||||
actionview (= 7.2.1)
|
||||
activejob (= 7.2.1)
|
||||
activesupport (= 7.2.1)
|
||||
actionmailer (7.2.2)
|
||||
actionpack (= 7.2.2)
|
||||
actionview (= 7.2.2)
|
||||
activejob (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
mail (>= 2.8.0)
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (7.2.1)
|
||||
actionview (= 7.2.1)
|
||||
activesupport (= 7.2.1)
|
||||
actionpack (7.2.2)
|
||||
actionview (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
nokogiri (>= 1.8.5)
|
||||
racc
|
||||
rack (>= 2.2.4, < 3.2)
|
||||
@@ -39,36 +39,37 @@ GEM
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
useragent (~> 0.16)
|
||||
actiontext (7.2.1)
|
||||
actionpack (= 7.2.1)
|
||||
activerecord (= 7.2.1)
|
||||
activestorage (= 7.2.1)
|
||||
activesupport (= 7.2.1)
|
||||
actiontext (7.2.2)
|
||||
actionpack (= 7.2.2)
|
||||
activerecord (= 7.2.2)
|
||||
activestorage (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (7.2.1)
|
||||
activesupport (= 7.2.1)
|
||||
actionview (7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
activejob (7.2.1)
|
||||
activesupport (= 7.2.1)
|
||||
activejob (7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.2.1)
|
||||
activesupport (= 7.2.1)
|
||||
activerecord (7.2.1)
|
||||
activemodel (= 7.2.1)
|
||||
activesupport (= 7.2.1)
|
||||
activemodel (7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
activerecord (7.2.2)
|
||||
activemodel (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
timeout (>= 0.4.0)
|
||||
activestorage (7.2.1)
|
||||
actionpack (= 7.2.1)
|
||||
activejob (= 7.2.1)
|
||||
activerecord (= 7.2.1)
|
||||
activesupport (= 7.2.1)
|
||||
activestorage (7.2.2)
|
||||
actionpack (= 7.2.2)
|
||||
activejob (= 7.2.2)
|
||||
activerecord (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
marcel (~> 1.0)
|
||||
activesupport (7.2.1)
|
||||
activesupport (7.2.2)
|
||||
base64
|
||||
benchmark (>= 0.3)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||
connection_pool (>= 2.2.5)
|
||||
@@ -82,23 +83,24 @@ GEM
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
ast (2.4.2)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.985.0)
|
||||
aws-sdk-core (3.209.1)
|
||||
aws-partitions (1.1018.0)
|
||||
aws-sdk-core (3.214.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.94.0)
|
||||
aws-sdk-core (~> 3, >= 3.207.0)
|
||||
aws-sdk-kms (1.96.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.167.0)
|
||||
aws-sdk-core (~> 3, >= 3.207.0)
|
||||
aws-sdk-s3 (1.176.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.10.0)
|
||||
aws-sigv4 (1.10.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
base64 (0.2.0)
|
||||
bcrypt (3.1.20)
|
||||
benchmark (0.4.0)
|
||||
better_html (2.1.1)
|
||||
actionview (>= 6.0)
|
||||
activesupport (>= 6.0)
|
||||
@@ -110,7 +112,7 @@ GEM
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.18.4)
|
||||
msgpack (~> 1.2)
|
||||
brakeman (6.2.1)
|
||||
brakeman (6.2.2)
|
||||
racc
|
||||
builder (3.3.0)
|
||||
capybara (3.40.0)
|
||||
@@ -131,7 +133,7 @@ GEM
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
csv (3.3.0)
|
||||
date (3.3.4)
|
||||
date (3.4.0)
|
||||
debug (1.9.2)
|
||||
irb (~> 1.10)
|
||||
reline (>= 0.3.8)
|
||||
@@ -141,7 +143,7 @@ GEM
|
||||
dotenv (= 3.1.4)
|
||||
railties (>= 6.1)
|
||||
drb (2.2.1)
|
||||
erb_lint (0.6.0)
|
||||
erb_lint (0.7.0)
|
||||
activesupport
|
||||
better_html (>= 2.0.1)
|
||||
parser (>= 2.7.1.4)
|
||||
@@ -151,16 +153,16 @@ GEM
|
||||
erubi (1.13.0)
|
||||
et-orbi (1.2.11)
|
||||
tzinfo
|
||||
faker (3.4.2)
|
||||
faker (3.5.1)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (2.12.0)
|
||||
faraday-net_http (>= 2.0, < 3.4)
|
||||
faraday (2.12.1)
|
||||
faraday-net_http (>= 2.0, < 3.5)
|
||||
json
|
||||
logger
|
||||
faraday-multipart (1.0.4)
|
||||
multipart-post (~> 2)
|
||||
faraday-net_http (3.3.0)
|
||||
net-http
|
||||
faraday-net_http (3.4.0)
|
||||
net-http (>= 0.5.0)
|
||||
faraday-retry (2.2.1)
|
||||
faraday (~> 2.0)
|
||||
ffi (1.17.0-aarch64-linux-gnu)
|
||||
@@ -174,7 +176,7 @@ GEM
|
||||
raabro (~> 1.4)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
good_job (4.3.0)
|
||||
good_job (4.5.1)
|
||||
activejob (>= 6.1.0)
|
||||
activerecord (>= 6.1.0)
|
||||
concurrent-ruby (>= 1.3.1)
|
||||
@@ -183,11 +185,14 @@ GEM
|
||||
thor (>= 1.0.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)
|
||||
railties (>= 6.0.0)
|
||||
hotwire_combobox (0.3.2)
|
||||
rails (>= 7.0.7.2)
|
||||
stimulus-rails (>= 1.2)
|
||||
turbo-rails (>= 1.2)
|
||||
i18n (1.14.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
i18n-tasks (1.0.14)
|
||||
@@ -203,7 +208,7 @@ GEM
|
||||
image_processing (1.13.0)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
importmap-rails (2.0.2)
|
||||
importmap-rails (2.0.3)
|
||||
actionpack (>= 6.0.0)
|
||||
activesupport (>= 6.0.0)
|
||||
railties (>= 6.0.0)
|
||||
@@ -212,12 +217,14 @@ GEM
|
||||
nokogiri (>= 1.6)
|
||||
intercom-rails (1.0.1)
|
||||
activesupport (> 4.0)
|
||||
io-console (0.7.2)
|
||||
io-console (0.8.0)
|
||||
irb (1.14.1)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
jmespath (1.6.2)
|
||||
json (2.7.2)
|
||||
json (2.8.2)
|
||||
jwt (2.9.3)
|
||||
base64
|
||||
language_server-protocol (3.17.0.3)
|
||||
launchy (3.0.1)
|
||||
addressable (~> 2.8)
|
||||
@@ -227,8 +234,8 @@ GEM
|
||||
listen (3.9.0)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
logger (1.6.1)
|
||||
loofah (2.22.0)
|
||||
logger (1.6.2)
|
||||
loofah (2.23.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
mail (2.8.1)
|
||||
@@ -240,14 +247,14 @@ GEM
|
||||
matrix (0.4.2)
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
minitest (5.25.1)
|
||||
mocha (2.4.5)
|
||||
minitest (5.25.4)
|
||||
mocha (2.7.0)
|
||||
ruby2_keywords (>= 0.0.5)
|
||||
msgpack (1.7.2)
|
||||
multipart-post (2.4.1)
|
||||
net-http (0.4.1)
|
||||
net-http (0.5.0)
|
||||
uri
|
||||
net-imap (0.4.14)
|
||||
net-imap (0.5.0)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
@@ -256,79 +263,82 @@ GEM
|
||||
timeout
|
||||
net-smtp (0.5.0)
|
||||
net-protocol
|
||||
nio4r (2.7.3)
|
||||
nokogiri (1.16.7-aarch64-linux)
|
||||
nio4r (2.7.4)
|
||||
nokogiri (1.17.0-aarch64-linux)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.7-arm-linux)
|
||||
nokogiri (1.17.0-arm-linux)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.7-arm64-darwin)
|
||||
nokogiri (1.17.0-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.7-x86-linux)
|
||||
nokogiri (1.17.0-x86-linux)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.7-x86_64-darwin)
|
||||
nokogiri (1.17.0-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.7-x86_64-linux)
|
||||
nokogiri (1.17.0-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
octokit (9.1.0)
|
||||
octokit (9.2.0)
|
||||
faraday (>= 1, < 3)
|
||||
sawyer (~> 0.9)
|
||||
pagy (9.1.0)
|
||||
parallel (1.25.1)
|
||||
parser (3.3.4.0)
|
||||
pagy (9.3.3)
|
||||
parallel (1.26.3)
|
||||
parser (3.3.5.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
pg (1.5.8)
|
||||
prism (1.1.0)
|
||||
pg (1.5.9)
|
||||
plaid (34.0.0)
|
||||
faraday (>= 1.0.1, < 3.0)
|
||||
faraday-multipart (>= 1.0.1, < 2.0)
|
||||
prism (1.2.0)
|
||||
propshaft (1.1.0)
|
||||
actionpack (>= 7.0.0)
|
||||
activesupport (>= 7.0.0)
|
||||
rack
|
||||
railties (>= 7.0.0)
|
||||
psych (5.1.2)
|
||||
psych (5.2.1)
|
||||
date
|
||||
stringio
|
||||
public_suffix (6.0.1)
|
||||
puma (6.4.3)
|
||||
puma (6.5.0)
|
||||
nio4r (~> 2.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (3.1.7)
|
||||
rack (3.1.8)
|
||||
rack-session (2.0.0)
|
||||
rack (>= 3.0.0)
|
||||
rack-test (2.1.0)
|
||||
rack (>= 1.3)
|
||||
rackup (2.1.0)
|
||||
rackup (2.2.1)
|
||||
rack (>= 3)
|
||||
webrick (~> 1.8)
|
||||
rails (7.2.1)
|
||||
actioncable (= 7.2.1)
|
||||
actionmailbox (= 7.2.1)
|
||||
actionmailer (= 7.2.1)
|
||||
actionpack (= 7.2.1)
|
||||
actiontext (= 7.2.1)
|
||||
actionview (= 7.2.1)
|
||||
activejob (= 7.2.1)
|
||||
activemodel (= 7.2.1)
|
||||
activerecord (= 7.2.1)
|
||||
activestorage (= 7.2.1)
|
||||
activesupport (= 7.2.1)
|
||||
rails (7.2.2)
|
||||
actioncable (= 7.2.2)
|
||||
actionmailbox (= 7.2.2)
|
||||
actionmailer (= 7.2.2)
|
||||
actionpack (= 7.2.2)
|
||||
actiontext (= 7.2.2)
|
||||
actionview (= 7.2.2)
|
||||
activejob (= 7.2.2)
|
||||
activemodel (= 7.2.2)
|
||||
activerecord (= 7.2.2)
|
||||
activestorage (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 7.2.1)
|
||||
railties (= 7.2.2)
|
||||
rails-dom-testing (2.2.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.6.0)
|
||||
rails-html-sanitizer (1.6.1)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (~> 1.14)
|
||||
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||
rails-i18n (7.0.9)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 6.0.0, < 8)
|
||||
rails-settings-cached (2.9.4)
|
||||
rails-settings-cached (2.9.5)
|
||||
activerecord (>= 5.0.0)
|
||||
railties (>= 5.0.0)
|
||||
railties (7.2.1)
|
||||
actionpack (= 7.2.1)
|
||||
activesupport (= 7.2.1)
|
||||
railties (7.2.2)
|
||||
actionpack (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
irb (~> 1.13)
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
@@ -341,25 +351,24 @@ GEM
|
||||
ffi (~> 1.0)
|
||||
rbs (3.6.1)
|
||||
logger
|
||||
rdoc (6.7.0)
|
||||
rdoc (6.8.1)
|
||||
psych (>= 4.0.0)
|
||||
redcarpet (3.6.0)
|
||||
regexp_parser (2.9.2)
|
||||
reline (0.5.10)
|
||||
reline (0.5.12)
|
||||
io-console (~> 0.5)
|
||||
rexml (3.3.8)
|
||||
rubocop (1.65.1)
|
||||
rexml (3.3.9)
|
||||
rubocop (1.67.0)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.4, < 3.0)
|
||||
rexml (>= 3.2.5, < 4.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
rubocop-ast (>= 1.32.2, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 3.0)
|
||||
rubocop-ast (1.31.3)
|
||||
rubocop-ast (1.32.3)
|
||||
parser (>= 3.3.1.0)
|
||||
rubocop-minitest (0.35.0)
|
||||
rubocop (>= 1.61, < 2.0)
|
||||
@@ -377,13 +386,13 @@ GEM
|
||||
rubocop-minitest
|
||||
rubocop-performance
|
||||
rubocop-rails
|
||||
ruby-lsp (0.19.1)
|
||||
ruby-lsp (0.22.1)
|
||||
language_server-protocol (~> 3.17.0)
|
||||
prism (>= 1.1, < 2.0)
|
||||
prism (>= 1.2, < 2.0)
|
||||
rbs (>= 3, < 4)
|
||||
sorbet-runtime (>= 0.5.10782)
|
||||
ruby-lsp-rails (0.3.18)
|
||||
ruby-lsp (>= 0.19.0, < 0.20.0)
|
||||
ruby-lsp-rails (0.3.27)
|
||||
ruby-lsp (>= 0.22.0, < 0.23.0)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-vips (2.2.2)
|
||||
ffi (~> 1.12)
|
||||
@@ -393,17 +402,17 @@ GEM
|
||||
sawyer (0.9.2)
|
||||
addressable (>= 2.3.5)
|
||||
faraday (>= 0.17.3, < 3)
|
||||
securerandom (0.3.1)
|
||||
selenium-webdriver (4.25.0)
|
||||
securerandom (0.4.0)
|
||||
selenium-webdriver (4.27.0)
|
||||
base64 (~> 0.2)
|
||||
logger (~> 1.4)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
rubyzip (>= 1.2.2, < 3.0)
|
||||
websocket (~> 1.0)
|
||||
sentry-rails (5.20.1)
|
||||
sentry-rails (5.22.0)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby (~> 5.20.1)
|
||||
sentry-ruby (5.20.1)
|
||||
sentry-ruby (~> 5.22.0)
|
||||
sentry-ruby (5.22.0)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
simplecov (0.22.0)
|
||||
@@ -413,36 +422,33 @@ GEM
|
||||
simplecov-html (0.12.3)
|
||||
simplecov_json_formatter (0.1.4)
|
||||
smart_properties (1.17.0)
|
||||
sorbet-runtime (0.5.11597)
|
||||
sorbet-runtime (0.5.11663)
|
||||
stackprof (0.2.26)
|
||||
stimulus-rails (1.3.4)
|
||||
railties (>= 6.0.0)
|
||||
stringio (3.1.1)
|
||||
stripe (13.0.0)
|
||||
tailwindcss-rails (2.7.7)
|
||||
railties (>= 7.0.0)
|
||||
tailwindcss-rails (2.7.7-aarch64-linux)
|
||||
railties (>= 7.0.0)
|
||||
tailwindcss-rails (2.7.7-arm-linux)
|
||||
railties (>= 7.0.0)
|
||||
tailwindcss-rails (2.7.7-arm64-darwin)
|
||||
railties (>= 7.0.0)
|
||||
tailwindcss-rails (2.7.7-x86_64-darwin)
|
||||
railties (>= 7.0.0)
|
||||
tailwindcss-rails (2.7.7-x86_64-linux)
|
||||
stringio (3.1.2)
|
||||
stripe (13.2.0)
|
||||
tailwindcss-rails (3.0.0)
|
||||
railties (>= 7.0.0)
|
||||
tailwindcss-ruby
|
||||
tailwindcss-ruby (3.4.14)
|
||||
tailwindcss-ruby (3.4.14-aarch64-linux)
|
||||
tailwindcss-ruby (3.4.14-arm-linux)
|
||||
tailwindcss-ruby (3.4.14-arm64-darwin)
|
||||
tailwindcss-ruby (3.4.14-x86_64-darwin)
|
||||
tailwindcss-ruby (3.4.14-x86_64-linux)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
thor (1.3.2)
|
||||
timeout (0.4.1)
|
||||
turbo-rails (2.0.10)
|
||||
timeout (0.4.2)
|
||||
turbo-rails (2.0.11)
|
||||
actionpack (>= 6.0.0)
|
||||
railties (>= 6.0.0)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
unicode-display_width (2.5.0)
|
||||
uri (0.13.1)
|
||||
useragent (0.16.10)
|
||||
unicode-display_width (2.6.0)
|
||||
uri (1.0.2)
|
||||
useragent (0.16.11)
|
||||
vcr (6.3.1)
|
||||
base64
|
||||
web-console (4.2.1)
|
||||
@@ -454,14 +460,13 @@ GEM
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webrick (1.8.2)
|
||||
websocket (1.2.11)
|
||||
websocket-driver (0.7.6)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
zeitwerk (2.6.18)
|
||||
zeitwerk (2.7.1)
|
||||
|
||||
PLATFORMS
|
||||
aarch64-linux
|
||||
@@ -487,22 +492,24 @@ DEPENDENCIES
|
||||
faraday-multipart
|
||||
faraday-retry
|
||||
good_job
|
||||
holidays
|
||||
hotwire-livereload
|
||||
hotwire_combobox
|
||||
i18n-tasks
|
||||
image_processing (>= 1.2)
|
||||
importmap-rails
|
||||
inline_svg
|
||||
intercom-rails
|
||||
jwt
|
||||
letter_opener
|
||||
lucide-rails!
|
||||
mocha
|
||||
octokit
|
||||
pagy
|
||||
pg (~> 1.5)
|
||||
plaid
|
||||
propshaft
|
||||
puma (>= 5.0)
|
||||
rails (~> 7.2.1)
|
||||
rails (~> 7.2.2)
|
||||
rails-settings-cached
|
||||
redcarpet
|
||||
rubocop-rails-omakase
|
||||
@@ -522,7 +529,7 @@ DEPENDENCIES
|
||||
webmock
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.3.4p94
|
||||
ruby 3.3.5p100
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.9
|
||||
2.5.22
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
web: ${DEBUG:+rdbg -O -n -c --} bin/rails server -b 0.0.0.0
|
||||
css: bin/rails tailwindcss:watch
|
||||
web: bundle exec ${DEBUG:+rdbg -O -n -c --} bin/rails server -b 0.0.0.0
|
||||
css: bundle exec bin/rails tailwindcss:watch
|
||||
worker: bundle exec good_job start
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# Maybe: The OS for your personal finances
|
||||
|
||||
<b>Get
|
||||
involved: [Discord](https://link.maybe.co/discord) • [Website](https://maybe.co) • [Issues](https://github.com/maybe-finance/maybe/issues)</b>
|
||||
involved: [Discord](https://link.maybe.co/discord) • [Website](https://maybefinance.com) • [Issues](https://github.com/maybe-finance/maybe/issues)</b>
|
||||
|
||||
_If you're looking for the previous React codebase, you can find it
|
||||
at [maybe-finance/maybe-archive](https://github.com/maybe-finance/maybe-archive)._
|
||||
@@ -42,14 +42,14 @@ The instructions below are for developers to get started with contributing to th
|
||||
|
||||
### Requirements
|
||||
|
||||
- Ruby 3.3.4
|
||||
- See `.ruby-version` file for required Ruby version
|
||||
- PostgreSQL >9.3 (ideally, latest stable version)
|
||||
|
||||
After cloning the repo, the basic setup commands are:
|
||||
|
||||
```sh
|
||||
cd maybe
|
||||
cp .env.example .env
|
||||
cp .env.local.example .env.local
|
||||
bin/setup
|
||||
bin/dev
|
||||
|
||||
|
||||
BIN
app/assets/images/logo-color.png
Normal file
BIN
app/assets/images/logo-color.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.1 KiB |
BIN
app/assets/images/maybe-plus-background.png
Normal file
BIN
app/assets/images/maybe-plus-background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
2811
app/assets/images/maybe-plus-background.svg
Normal file
2811
app/assets/images/maybe-plus-background.svg
Normal file
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 299 KiB |
BIN
app/assets/images/maybe-plus-logo.png
Normal file
BIN
app/assets/images/maybe-plus-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
10
app/assets/images/placeholder-graph.svg
Normal file
10
app/assets/images/placeholder-graph.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="944" height="201" viewBox="0 0 944 201" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 56.5502L14.4845 52.101L28.9689 50.1276L43.4534 51.7926L57.9379 40.2042L72.4224 35.6995L86.9068 35.0612L101.391 51.2218L115.876 73.6398L130.36 65.7562L144.845 64.7572L159.329 78.5795L173.814 81.9833L188.298 71.3186L202.783 80.5112L217.267 86L231.752 84.5697L246.236 83.0772L260.721 78.4002L275.205 77.343L289.689 71.8152L304.174 52.25L318.658 51.5349L333.143 48.185L347.627 47.2522L362.112 45.4586L376.596 49.2356L391.081 47.5566L405.565 31.0549L420.05 28.5641L434.534 36.6352H449.019L463.503 42.7572L477.988 37.7564L492.472 42.3467L506.957 49.3852L521.441 59.4839L535.925 52.7514L550.41 47.1535L564.894 58.6703L579.379 49.8343L593.863 50.5123H608.348L622.832 54.192L637.317 58.4763L651.801 57.2522L666.286 59.3943L677.01 62.8533L688.553 59.3943L709.129 67.4827L724.224 60.8386L738.708 52.27L753.193 58.6965L767.677 37.887L782.162 28.3178L796.646 16.383L811.13 20.9733L825.615 10.2626L840.099 11.7927L854.584 6.59032L869.068 15.771L883.553 8.12043L898.037 6.59032L912.522 2L927.006 14.8529L944 15.771" stroke="#0B0B0B" stroke-opacity="0.25" stroke-width="2" stroke-miterlimit="16"/>
|
||||
<path d="M14.4845 52.5538L0 57.0432V201H944V15.8954L927.006 14.9691L912.522 2L898.037 6.63181L883.553 8.17575L869.068 15.8954L854.584 6.63181L840.099 11.8812L825.615 10.3373L811.13 21.1448L796.646 16.513L782.161 28.5557L767.677 38.2114L753.193 59.2089L738.708 52.7244L724.224 61.3704L709.129 68.0745L688.553 59.9131L677.01 63.4034L666.286 59.9131L651.801 57.7516L637.317 58.9868L622.832 54.6637L608.348 50.9508H593.863L579.379 50.2667L564.894 59.1826L550.41 47.5616L535.925 53.2102L521.441 60.0035L506.957 49.8135L492.472 42.7114L477.988 38.0796L463.503 43.1256L449.019 36.9483H434.534L420.05 28.8042L405.565 31.3175L391.081 47.9684L376.596 49.6626L362.112 45.8514L347.627 47.6612L333.143 48.6024L318.658 51.9826L304.174 52.7042L289.689 72.4463L275.205 78.024L260.721 79.0908L246.236 83.8101L231.752 85.3161L217.267 86.7593L202.783 81.2209L188.298 71.9451L173.814 82.7063L159.329 79.2716L144.845 65.3245L130.36 66.3325L115.876 74.2874L101.391 51.6667L86.9068 35.3601L72.4224 36.0041L57.9379 40.5496L43.4534 52.2427L28.9689 50.5627L14.4845 52.5538Z" fill="url(#paint0_linear_4023_1299)" fill-opacity="0.5"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_4023_1299" x1="445.5" y1="174.496" x2="445.5" y2="51.9672" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#E5E5E5" stop-opacity="0.6"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
6
app/assets/images/stripe-logo.svg
Normal file
6
app/assets/images/stripe-logo.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Frame 1321315963">
|
||||
<rect width="20" height="20" rx="10" fill="#635BFF"/>
|
||||
<path id="Vector" fill-rule="evenodd" clip-rule="evenodd" d="M9.35663 7.69056C9.35663 7.20077 9.75747 7.01238 10.4214 7.01238C11.3734 7.01238 12.5759 7.30124 13.5279 7.81615V4.86482C12.4882 4.45037 11.461 4.28711 10.4214 4.28711C7.87854 4.28711 6.1875 5.61835 6.1875 7.84127C6.1875 11.3075 10.9475 10.7549 10.9475 12.2494C10.9475 12.8271 10.4464 13.0155 9.74495 13.0155C8.70527 13.0155 7.37749 12.5885 6.32529 12.0108V14.9998C7.49023 15.5022 8.66769 15.7157 9.74495 15.7157C12.3504 15.7157 14.1416 14.4221 14.1416 12.1741C14.1291 8.43154 9.35663 9.09716 9.35663 7.69056Z" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 775 B |
@@ -19,7 +19,8 @@
|
||||
@apply focus-within:border-gray-900 focus-within:shadow-none focus-within:ring-4 focus-within:ring-gray-100;
|
||||
}
|
||||
|
||||
.form-field__label {
|
||||
|
||||
.form-field__label, .hw-combobox__label {
|
||||
@apply block text-xs text-gray-500 peer-disabled:text-gray-400;
|
||||
}
|
||||
|
||||
@@ -100,7 +101,7 @@
|
||||
}
|
||||
|
||||
.btn {
|
||||
@apply px-3 py-2 rounded-lg text-sm font-medium cursor-pointer;
|
||||
@apply px-3 py-2 rounded-lg text-sm font-medium cursor-pointer disabled:cursor-not-allowed focus:outline-gray-500;
|
||||
}
|
||||
|
||||
.btn--primary {
|
||||
@@ -112,7 +113,7 @@
|
||||
}
|
||||
|
||||
.btn--outline {
|
||||
@apply border border-alpha-black-200 text-gray-900 hover:bg-gray-50;
|
||||
@apply border border-alpha-black-200 text-gray-900 hover:bg-gray-50 disabled:bg-gray-50 disabled:hover:bg-gray-50 disabled:text-gray-400;
|
||||
}
|
||||
|
||||
.btn--ghost {
|
||||
@@ -120,6 +121,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
.combobox {
|
||||
.hw-combobox__main__wrapper, .hw-combobox__input {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
.hw-combobox__main__wrapper {
|
||||
@apply border-0 p-0 focus:border-0 ring-0 focus:ring-0 shadow-none focus:shadow-none focus-within:shadow-none;
|
||||
}
|
||||
|
||||
.hw-combobox__listbox {
|
||||
@apply absolute top-[160%] right-0 w-full bg-transparent rounded z-30;
|
||||
}
|
||||
|
||||
.hw_combobox__pagination__wrapper {
|
||||
@apply h-px;
|
||||
|
||||
&:only-child {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
}
|
||||
|
||||
--hw-border-color: rgba(0, 0, 0, 0.2);
|
||||
--hw-handle-width: 20px;
|
||||
--hw-handle-height: 20px;
|
||||
--hw-handle-offset-right: 0px;
|
||||
}
|
||||
|
||||
/* Small, single purpose classes that should take precedence over other styles */
|
||||
@layer utilities {
|
||||
.scrollbar::-webkit-scrollbar {
|
||||
@@ -134,4 +162,20 @@
|
||||
.scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: #a6a6a6;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom scrollbar implementation for Windows browsers */
|
||||
.windows {
|
||||
::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #d6d6d6;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #a6a6a6;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
class Account::CashesController < ApplicationController
|
||||
layout :with_sidebar
|
||||
|
||||
before_action :set_account
|
||||
|
||||
def index
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Current.family.accounts.find(params[:account_id])
|
||||
end
|
||||
end
|
||||
@@ -2,47 +2,25 @@ class Account::EntriesController < ApplicationController
|
||||
layout :with_sidebar
|
||||
|
||||
before_action :set_account
|
||||
before_action :set_entry, only: %i[edit update show destroy]
|
||||
|
||||
def edit
|
||||
render entryable_view_path(:edit)
|
||||
end
|
||||
|
||||
def update
|
||||
@entry.update!(entry_params)
|
||||
@entry.sync_account_later
|
||||
|
||||
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
|
||||
|
||||
def show
|
||||
render entryable_view_path(:show)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@entry.destroy!
|
||||
@entry.sync_account_later
|
||||
redirect_to account_url(@entry.account), notice: t(".success")
|
||||
def index
|
||||
@q = search_params
|
||||
@pagy, @entries = pagy(entries_scope.search(@q).reverse_chronological, limit: params[:per_page] || "10")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def entryable_view_path(action)
|
||||
@entry.entryable_type.underscore.pluralize + "/" + action.to_s
|
||||
end
|
||||
|
||||
def set_account
|
||||
@account = Current.family.accounts.find(params[:account_id])
|
||||
end
|
||||
|
||||
def set_entry
|
||||
@entry = @account.entries.find(params[:id])
|
||||
def entries_scope
|
||||
scope = Current.family.entries
|
||||
scope = scope.where(account: @account) if @account
|
||||
scope
|
||||
end
|
||||
|
||||
def entry_params
|
||||
params.require(:account_entry).permit(:name, :date, :amount, :currency)
|
||||
def search_params
|
||||
params.fetch(:q, {})
|
||||
.permit(:search)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
class Account::HoldingsController < ApplicationController
|
||||
layout :with_sidebar
|
||||
|
||||
before_action :set_account
|
||||
before_action :set_holding, only: %i[show destroy]
|
||||
|
||||
def index
|
||||
@holdings = @account.holdings.current
|
||||
@account = Current.family.accounts.find(params[:account_id])
|
||||
end
|
||||
|
||||
def show
|
||||
@@ -13,16 +12,17 @@ class Account::HoldingsController < ApplicationController
|
||||
|
||||
def destroy
|
||||
@holding.destroy_holding_and_entries!
|
||||
redirect_back_or_to account_holdings_path(@account)
|
||||
|
||||
flash[:notice] = t(".success")
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_back_or_to account_path(@holding.account) }
|
||||
format.turbo_stream { render turbo_stream: turbo_stream.action(:redirect, account_path(@holding.account)) }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Current.family.accounts.find(params[:account_id])
|
||||
end
|
||||
|
||||
def set_holding
|
||||
@holding = @account.holdings.current.find(params[:id])
|
||||
@holding = Current.family.holdings.find(params[:id])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
class Account::LogosController < ApplicationController
|
||||
def show
|
||||
@account = Current.family.accounts.find(params[:account_id])
|
||||
render_placeholder
|
||||
end
|
||||
|
||||
def render_placeholder
|
||||
render formats: :svg
|
||||
end
|
||||
end
|
||||
@@ -1,59 +1,37 @@
|
||||
class Account::TradesController < ApplicationController
|
||||
layout :with_sidebar
|
||||
include EntryableResource
|
||||
|
||||
before_action :set_account
|
||||
before_action :set_entry, only: :update
|
||||
|
||||
def new
|
||||
@entry = @account.entries.account_trades.new(entryable_attributes: {})
|
||||
end
|
||||
|
||||
def index
|
||||
@entries = @account.entries.reverse_chronological.where(entryable_type: %w[Account::Trade Account::Transaction])
|
||||
end
|
||||
|
||||
def create
|
||||
@builder = Account::EntryBuilder.new(entry_params)
|
||||
|
||||
if entry = @builder.save
|
||||
entry.sync_account_later
|
||||
redirect_to account_path(@account), notice: t(".success")
|
||||
else
|
||||
flash[:alert] = t(".failure")
|
||||
redirect_back_or_to account_path(@account)
|
||||
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
|
||||
permitted_entryable_attributes :id, :qty, :price
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Current.family.accounts.find(params[:account_id])
|
||||
def build_entry
|
||||
Account::TradeBuilder.new(create_entry_params)
|
||||
end
|
||||
|
||||
def set_entry
|
||||
@entry = @account.entries.find(params[:id])
|
||||
def create_entry_params
|
||||
params.require(:account_entry).permit(
|
||||
:account_id, :date, :amount, :currency, :qty, :price, :ticker, :type, :transfer_account_id
|
||||
).tap do |params|
|
||||
account_id = params.delete(:account_id)
|
||||
params[:account] = Current.family.accounts.find(account_id)
|
||||
end
|
||||
end
|
||||
|
||||
def entry_params
|
||||
params.require(:account_entry)
|
||||
.permit(
|
||||
:type, :date, :qty, :ticker, :price, :amount, :notes, :excluded, :currency, :transfer_account_id, :entryable_type,
|
||||
entryable_attributes: [
|
||||
:id,
|
||||
:qty,
|
||||
:ticker,
|
||||
:price
|
||||
]
|
||||
)
|
||||
.merge(account: @account)
|
||||
def update_entry_params
|
||||
return entry_params unless entry_params[:entryable_attributes].present?
|
||||
|
||||
update_params = entry_params
|
||||
update_params = update_params.merge(entryable_type: "Account::Trade")
|
||||
|
||||
qty = update_params[:entryable_attributes][:qty]
|
||||
price = update_params[:entryable_attributes][:price]
|
||||
|
||||
if qty.present? && price.present?
|
||||
qty = update_params[:nature] == "inflow" ? -qty.to_d : qty.to_d
|
||||
update_params[:entryable_attributes][:qty] = qty
|
||||
update_params[:amount] = qty * price.to_d
|
||||
end
|
||||
|
||||
update_params.except(:nature)
|
||||
end
|
||||
end
|
||||
|
||||
22
app/controllers/account/transaction_categories_controller.rb
Normal file
22
app/controllers/account/transaction_categories_controller.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
class Account::TransactionCategoriesController < ApplicationController
|
||||
def update
|
||||
@entry = Current.family.entries.account_transactions.find(params[:transaction_id])
|
||||
@entry.update!(entry_params)
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_back_or_to account_transaction_path(@entry) }
|
||||
format.turbo_stream do
|
||||
render turbo_stream: turbo_stream.replace(
|
||||
"category_menu_account_transaction_#{@entry.account_transaction_id}",
|
||||
partial: "categories/menu",
|
||||
locals: { transaction: @entry.account_transaction }
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def entry_params
|
||||
params.require(:account_entry).permit(:entryable_type, entryable_attributes: [ :id, :category_id ])
|
||||
end
|
||||
end
|
||||
@@ -1,57 +1,55 @@
|
||||
class Account::TransactionsController < ApplicationController
|
||||
layout :with_sidebar
|
||||
include EntryableResource
|
||||
|
||||
before_action :set_account
|
||||
before_action :set_entry, only: :update
|
||||
permitted_entryable_attributes :id, :category_id, :merchant_id, { tag_ids: [] }
|
||||
|
||||
def index
|
||||
@pagy, @entries = pagy(
|
||||
@account.entries.account_transactions.reverse_chronological,
|
||||
limit: params[:per_page] || "10"
|
||||
)
|
||||
def bulk_delete
|
||||
destroyed = Current.family.entries.destroy_by(id: bulk_delete_params[:entry_ids])
|
||||
destroyed.map(&:account).uniq.each(&:sync_later)
|
||||
redirect_back_or_to transactions_url, notice: t(".success", count: destroyed.count)
|
||||
end
|
||||
|
||||
def update
|
||||
@entry.update!(entry_params)
|
||||
def bulk_edit
|
||||
end
|
||||
|
||||
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
|
||||
def bulk_update
|
||||
updated = Current.family
|
||||
.entries
|
||||
.where(id: bulk_update_params[:entry_ids])
|
||||
.bulk_update!(bulk_update_params)
|
||||
|
||||
redirect_back_or_to transactions_url, notice: t(".success", count: updated)
|
||||
end
|
||||
|
||||
def mark_transfers
|
||||
Current.family
|
||||
.entries
|
||||
.where(id: bulk_update_params[:entry_ids])
|
||||
.mark_transfers!
|
||||
|
||||
redirect_back_or_to transactions_url, notice: t(".success")
|
||||
end
|
||||
|
||||
def unmark_transfers
|
||||
Current.family
|
||||
.entries
|
||||
.where(id: bulk_update_params[:entry_ids])
|
||||
.update_all marked_as_transfer: false
|
||||
|
||||
redirect_back_or_to transactions_url, notice: t(".success")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Current.family.accounts.find(params[:account_id])
|
||||
def bulk_delete_params
|
||||
params.require(:bulk_delete).permit(entry_ids: [])
|
||||
end
|
||||
|
||||
def set_entry
|
||||
@entry = @account.entries.find(params[:id])
|
||||
def bulk_update_params
|
||||
params.require(:bulk_update).permit(:date, :notes, :category_id, :merchant_id, entry_ids: [])
|
||||
end
|
||||
|
||||
def entry_params
|
||||
params.require(:account_entry)
|
||||
.permit(
|
||||
:name, :date, :amount, :currency, :excluded, :notes, :entryable_type, :nature,
|
||||
entryable_attributes: [
|
||||
:id,
|
||||
:category_id,
|
||||
:merchant_id,
|
||||
{ tag_ids: [] }
|
||||
]
|
||||
).tap do |permitted_params|
|
||||
nature = permitted_params.delete(:nature)
|
||||
|
||||
if permitted_params[:amount]
|
||||
amount_value = permitted_params[:amount].to_d
|
||||
|
||||
if nature == "income"
|
||||
amount_value *= -1
|
||||
end
|
||||
|
||||
permitted_params[:amount] = amount_value
|
||||
end
|
||||
end
|
||||
def search_params
|
||||
params.fetch(:q, {})
|
||||
.permit(:start_date, :end_date, :search, :amount, :amount_operator, accounts: [], account_ids: [], categories: [], merchants: [], types: [], tags: [])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
class Account::TransfersController < ApplicationController
|
||||
layout :with_sidebar
|
||||
|
||||
before_action :set_transfer, only: :destroy
|
||||
before_action :set_transfer, only: %i[destroy show update]
|
||||
|
||||
def new
|
||||
@transfer = Account::Transfer.new
|
||||
end
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def create
|
||||
from_account = Current.family.accounts.find(transfer_params[:from_account_id])
|
||||
to_account = Current.family.accounts.find(transfer_params[:to_account_id])
|
||||
|
||||
@transfer = Account::Transfer.build_from_accounts from_account, to_account, \
|
||||
date: transfer_params[:date],
|
||||
amount: transfer_params[:amount].to_d,
|
||||
currency: transfer_params[:currency],
|
||||
name: transfer_params[:name]
|
||||
amount: transfer_params[:amount].to_d
|
||||
|
||||
if @transfer.save
|
||||
@transfer.entries.each(&:sync_account_later)
|
||||
@@ -28,18 +29,33 @@ class Account::TransfersController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@transfer.update_entries!(transfer_update_params)
|
||||
redirect_back_or_to transactions_url, notice: t(".success")
|
||||
end
|
||||
|
||||
def destroy
|
||||
@transfer.destroy_and_remove_marks!
|
||||
@transfer.destroy!
|
||||
redirect_back_or_to transactions_url, notice: t(".success")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_transfer
|
||||
@transfer = Account::Transfer.find(params[:id])
|
||||
record = Account::Transfer.find(params[:id])
|
||||
|
||||
unless record.entries.all? { |entry| Current.family.accounts.include?(entry.account) }
|
||||
raise ActiveRecord::RecordNotFound
|
||||
end
|
||||
|
||||
@transfer = record
|
||||
end
|
||||
|
||||
def transfer_params
|
||||
params.require(:account_transfer).permit(:from_account_id, :to_account_id, :amount, :date, :name)
|
||||
params.require(:account_transfer).permit(:from_account_id, :to_account_id, :amount, :date, :name, :excluded)
|
||||
end
|
||||
|
||||
def transfer_update_params
|
||||
params.require(:account_transfer).permit(:excluded, :notes)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,35 +1,3 @@
|
||||
class Account::ValuationsController < ApplicationController
|
||||
layout :with_sidebar
|
||||
|
||||
before_action :set_account
|
||||
|
||||
def new
|
||||
@entry = @account.entries.account_valuations.new(entryable_attributes: {})
|
||||
end
|
||||
|
||||
def create
|
||||
@entry = @account.entries.account_valuations.new(entry_params.merge(entryable_attributes: {}))
|
||||
|
||||
if @entry.save
|
||||
@entry.sync_account_later
|
||||
redirect_back_or_to account_valuations_path(@account), notice: t(".success")
|
||||
else
|
||||
flash[:alert] = @entry.errors.full_messages.to_sentence
|
||||
redirect_to account_path(@account)
|
||||
end
|
||||
end
|
||||
|
||||
def index
|
||||
@entries = @account.entries.account_valuations.reverse_chronological
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Current.family.accounts.find(params[:account_id])
|
||||
end
|
||||
|
||||
def entry_params
|
||||
params.require(:account_entry).permit(:name, :date, :amount, :currency)
|
||||
end
|
||||
include EntryableResource
|
||||
end
|
||||
|
||||
@@ -1,88 +1,51 @@
|
||||
class AccountsController < ApplicationController
|
||||
layout :with_sidebar
|
||||
|
||||
include Filterable
|
||||
before_action :set_account, only: %i[edit show destroy sync update]
|
||||
before_action :set_account, only: %i[sync]
|
||||
|
||||
def index
|
||||
@institutions = Current.family.institutions
|
||||
@accounts = Current.family.accounts.ungrouped.alphabetically
|
||||
@manual_accounts = Current.family.accounts.where(scheduled_for_deletion: false).manual.alphabetically
|
||||
@plaid_items = Current.family.plaid_items.where(scheduled_for_deletion: false).ordered
|
||||
end
|
||||
|
||||
def summary
|
||||
@period = Period.from_param(params[:period])
|
||||
snapshot = Current.family.snapshot(@period)
|
||||
@net_worth_series = snapshot[:net_worth_series]
|
||||
@asset_series = snapshot[:asset_series]
|
||||
@liability_series = snapshot[:liability_series]
|
||||
@accounts = Current.family.accounts
|
||||
@accounts = Current.family.accounts.active
|
||||
@account_groups = @accounts.by_group(period: @period, currency: Current.family.currency)
|
||||
end
|
||||
|
||||
def list
|
||||
@period = Period.from_param(params[:period])
|
||||
render layout: false
|
||||
end
|
||||
|
||||
def new
|
||||
@account = Account.new(
|
||||
accountable: Accountable.from_type(params[:type])&.new,
|
||||
currency: Current.family.currency
|
||||
)
|
||||
|
||||
@account.accountable.address = Address.new if @account.accountable.is_a?(Property)
|
||||
|
||||
if params[:institution_id]
|
||||
@account.institution = Current.family.institutions.find_by(id: params[:institution_id])
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
@series = @account.series(period: @period)
|
||||
@trend = @series.trend
|
||||
end
|
||||
|
||||
def edit
|
||||
@account.accountable.build_address if @account.accountable.is_a?(Property) && @account.accountable.address.blank?
|
||||
end
|
||||
|
||||
def update
|
||||
@account.update_with_sync!(account_params)
|
||||
redirect_back_or_to account_path(@account), notice: t(".success")
|
||||
end
|
||||
|
||||
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_back_or_to account_path(@account), notice: t(".success")
|
||||
end
|
||||
|
||||
def destroy
|
||||
@account.destroy!
|
||||
redirect_to accounts_path, notice: t(".success")
|
||||
end
|
||||
|
||||
def sync
|
||||
unless @account.syncing?
|
||||
@account.sync_later
|
||||
end
|
||||
|
||||
redirect_to account_path(@account)
|
||||
end
|
||||
|
||||
def chart
|
||||
@account = Current.family.accounts.find(params[:id])
|
||||
render layout: "application"
|
||||
end
|
||||
|
||||
def sync_all
|
||||
Current.family.accounts.active.sync
|
||||
redirect_back_or_to accounts_path, notice: t(".success")
|
||||
unless Current.family.syncing?
|
||||
Current.family.sync_later
|
||||
end
|
||||
|
||||
redirect_to accounts_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Current.family.accounts.find(params[:id])
|
||||
end
|
||||
|
||||
def account_params
|
||||
params.require(:account).permit(:name, :accountable_type, :balance, :start_date, :start_balance, :currency, :subtype, :is_active, :institution_id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,12 +1,41 @@
|
||||
class ApplicationController < ActionController::Base
|
||||
include Localize, AutoSync, Authentication, Invitable, SelfHostable, StoreLocation
|
||||
include Onboardable, Localize, AutoSync, Authentication, Invitable, SelfHostable, StoreLocation, Impersonatable
|
||||
include Pagy::Backend
|
||||
|
||||
helper_method :require_upgrade?, :subscription_pending?
|
||||
|
||||
before_action :detect_os
|
||||
|
||||
private
|
||||
def require_upgrade?
|
||||
return false if self_hosted?
|
||||
return false unless Current.session
|
||||
return false if Current.family.subscribed?
|
||||
return false if subscription_pending? || request.path == settings_billing_path
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def subscription_pending?
|
||||
subscribed_at = Current.session.subscribed_at
|
||||
subscribed_at.present? && subscribed_at <= Time.current && subscribed_at > 1.hour.ago
|
||||
end
|
||||
|
||||
def with_sidebar
|
||||
return "turbo_rails/frame" if turbo_frame_request?
|
||||
|
||||
"with_sidebar"
|
||||
end
|
||||
|
||||
def detect_os
|
||||
user_agent = request.user_agent
|
||||
@os = case user_agent
|
||||
when /Windows/i then "windows"
|
||||
when /Macintosh/i then "mac"
|
||||
when /Linux/i then "linux"
|
||||
when /Android/i then "android"
|
||||
when /iPhone|iPad/i then "ios"
|
||||
else ""
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class CategoriesController < ApplicationController
|
||||
layout :with_sidebar
|
||||
|
||||
before_action :set_category, only: %i[edit update]
|
||||
before_action :set_category, only: %i[edit update destroy]
|
||||
before_action :set_transaction, only: :create
|
||||
|
||||
def index
|
||||
@@ -13,12 +13,14 @@ class CategoriesController < ApplicationController
|
||||
end
|
||||
|
||||
def create
|
||||
Category.transaction do
|
||||
category = Current.family.categories.create!(category_params)
|
||||
@transaction.update!(category_id: category.id) if @transaction
|
||||
end
|
||||
@category = Current.family.categories.new(category_params)
|
||||
|
||||
redirect_back_or_to transactions_path, notice: t(".success")
|
||||
if @category.save
|
||||
@transaction.update(category_id: @category.id) if @transaction
|
||||
redirect_back_or_to transactions_path, notice: t(".success")
|
||||
else
|
||||
redirect_back_or_to transactions_path, alert: t(".failure", error: @category.errors.full_messages.to_sentence)
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
@@ -30,6 +32,12 @@ class CategoriesController < ApplicationController
|
||||
redirect_back_or_to transactions_path, notice: t(".success")
|
||||
end
|
||||
|
||||
def destroy
|
||||
@category.destroy
|
||||
|
||||
redirect_back_or_to categories_path, notice: t(".success")
|
||||
end
|
||||
|
||||
private
|
||||
def set_category
|
||||
@category = Current.family.categories.find(params[:id])
|
||||
|
||||
75
app/controllers/concerns/accountable_resource.rb
Normal file
75
app/controllers/concerns/accountable_resource.rb
Normal file
@@ -0,0 +1,75 @@
|
||||
module AccountableResource
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
layout :with_sidebar
|
||||
before_action :set_account, only: [ :show, :edit, :update, :destroy ]
|
||||
before_action :set_link_token, only: :new
|
||||
end
|
||||
|
||||
class_methods do
|
||||
def permitted_accountable_attributes(*attrs)
|
||||
@permitted_accountable_attributes = attrs if attrs.any?
|
||||
@permitted_accountable_attributes ||= [ :id ]
|
||||
end
|
||||
end
|
||||
|
||||
def new
|
||||
@account = Current.family.accounts.build(
|
||||
currency: Current.family.currency,
|
||||
accountable: accountable_type.new
|
||||
)
|
||||
end
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def create
|
||||
@account = Current.family.accounts.create_and_sync(account_params.except(:return_to))
|
||||
redirect_to account_params[:return_to].presence || @account, notice: t("accounts.create.success", type: accountable_type.name.underscore.humanize)
|
||||
end
|
||||
|
||||
def update
|
||||
@account.update_with_sync!(account_params.except(:return_to))
|
||||
redirect_back_or_to @account, notice: t("accounts.update.success", type: accountable_type.name.underscore.humanize)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@account.destroy_later
|
||||
redirect_to accounts_path, notice: t("accounts.destroy.success", type: accountable_type.name.underscore.humanize)
|
||||
end
|
||||
|
||||
private
|
||||
def set_link_token
|
||||
@link_token = Current.family.get_link_token(
|
||||
webhooks_url: webhooks_url,
|
||||
redirect_url: accounts_url,
|
||||
accountable_type: accountable_type.name
|
||||
)
|
||||
end
|
||||
|
||||
def webhooks_url
|
||||
return webhooks_plaid_url if Rails.env.production?
|
||||
|
||||
base_url = ENV.fetch("DEV_WEBHOOKS_URL", root_url.chomp("/"))
|
||||
base_url + "/webhooks/plaid"
|
||||
end
|
||||
|
||||
def accountable_type
|
||||
controller_name.classify.constantize
|
||||
end
|
||||
|
||||
def set_account
|
||||
@account = Current.family.accounts.find(params[:id])
|
||||
end
|
||||
|
||||
def account_params
|
||||
params.require(:account).permit(
|
||||
:name, :is_active, :balance, :subtype, :currency, :accountable_type, :return_to,
|
||||
accountable_attributes: self.class.permitted_accountable_attributes
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -14,7 +14,7 @@ module Authentication
|
||||
|
||||
private
|
||||
def authenticate_user!
|
||||
if session_record = Session.find_by_id(cookies.signed[:session_token])
|
||||
if session_record = find_session_by_cookie
|
||||
Current.session = session_record
|
||||
else
|
||||
if self_hosted_first_login?
|
||||
@@ -25,6 +25,10 @@ module Authentication
|
||||
end
|
||||
end
|
||||
|
||||
def find_session_by_cookie
|
||||
Session.find_by(id: cookies.signed[:session_token])
|
||||
end
|
||||
|
||||
def create_session_for(user)
|
||||
session = user.sessions.create!
|
||||
cookies.signed.permanent[:session_token] = { value: session.id, httponly: true }
|
||||
|
||||
@@ -2,12 +2,20 @@ module AutoSync
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :sync_family, if: -> { Current.family.present? && Current.family.needs_sync? }
|
||||
before_action :sync_family, if: :family_needs_auto_sync?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sync_family
|
||||
Current.family.sync
|
||||
Current.family.update!(last_synced_at: Time.current)
|
||||
Current.family.sync_later
|
||||
end
|
||||
|
||||
def family_needs_auto_sync?
|
||||
return false unless Current.family.present?
|
||||
return false unless Current.family.accounts.any?
|
||||
|
||||
Current.family.last_synced_at.blank? ||
|
||||
Current.family.last_synced_at.to_date < Date.current
|
||||
end
|
||||
end
|
||||
|
||||
126
app/controllers/concerns/entryable_resource.rb
Normal file
126
app/controllers/concerns/entryable_resource.rb
Normal file
@@ -0,0 +1,126 @@
|
||||
module EntryableResource
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
layout :with_sidebar
|
||||
before_action :set_entry, only: %i[show update destroy]
|
||||
end
|
||||
|
||||
class_methods do
|
||||
def permitted_entryable_attributes(*attrs)
|
||||
@permitted_entryable_attributes = attrs if attrs.any?
|
||||
@permitted_entryable_attributes ||= [ :id ]
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def new
|
||||
account = Current.family.accounts.find_by(id: params[:account_id])
|
||||
|
||||
@entry = Current.family.entries.new(
|
||||
account: account,
|
||||
currency: account ? account.currency : Current.family.currency,
|
||||
entryable: entryable_type.new
|
||||
)
|
||||
end
|
||||
|
||||
def create
|
||||
@entry = build_entry
|
||||
|
||||
if @entry.save
|
||||
@entry.sync_account_later
|
||||
|
||||
flash[:notice] = t("account.entries.create.success")
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_back_or_to account_path(@entry.account) }
|
||||
|
||||
redirect_target_url = request.referer || account_path(@entry.account)
|
||||
format.turbo_stream { render turbo_stream: turbo_stream.action(:redirect, redirect_target_url) }
|
||||
end
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @entry.update(update_entry_params)
|
||||
@entry.sync_account_later
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_back_or_to account_path(@entry.account), notice: t("account.entries.update.success") }
|
||||
format.turbo_stream do
|
||||
render turbo_stream: turbo_stream.replace(
|
||||
"header_account_entry_#{@entry.id}",
|
||||
partial: "#{entryable_type.name.underscore.pluralize}/header",
|
||||
locals: { entry: @entry }
|
||||
)
|
||||
end
|
||||
end
|
||||
else
|
||||
render :show, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
account = @entry.account
|
||||
@entry.destroy!
|
||||
@entry.sync_account_later
|
||||
|
||||
flash[:notice] = t("account.entries.destroy.success")
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_back_or_to account_path(account) }
|
||||
|
||||
redirect_target_url = request.referer || account_path(@entry.account)
|
||||
format.turbo_stream { render turbo_stream: turbo_stream.action(:redirect, redirect_target_url) }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def entryable_type
|
||||
permitted_entryable_types = %w[Account::Transaction Account::Valuation Account::Trade]
|
||||
klass = params[:entryable_type] || "Account::#{controller_name.classify}"
|
||||
klass.constantize if permitted_entryable_types.include?(klass)
|
||||
end
|
||||
|
||||
def set_entry
|
||||
@entry = Current.family.entries.find(params[:id])
|
||||
end
|
||||
|
||||
def build_entry
|
||||
Current.family.entries.new(create_entry_params)
|
||||
end
|
||||
|
||||
def update_entry_params
|
||||
prepared_entry_params
|
||||
end
|
||||
|
||||
def create_entry_params
|
||||
prepared_entry_params.merge({
|
||||
entryable_type: entryable_type.name,
|
||||
entryable_attributes: entry_params[:entryable_attributes] || {}
|
||||
})
|
||||
end
|
||||
|
||||
def prepared_entry_params
|
||||
default_params = entry_params.except(:nature)
|
||||
default_params = default_params.merge(entryable_type: entryable_type.name) if entry_params[:entryable_attributes].present?
|
||||
|
||||
if entry_params[:nature].present? && entry_params[:amount].present?
|
||||
signed_amount = entry_params[:nature] == "inflow" ? -entry_params[:amount].to_d : entry_params[:amount].to_d
|
||||
default_params = default_params.merge(amount: signed_amount)
|
||||
end
|
||||
|
||||
default_params
|
||||
end
|
||||
|
||||
def entry_params
|
||||
params.require(:account_entry).permit(
|
||||
:account_id, :name, :date, :amount, :currency, :excluded, :notes, :nature,
|
||||
entryable_attributes: self.class.permitted_entryable_attributes
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -1,23 +0,0 @@
|
||||
module Filterable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :set_period
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_period
|
||||
@period = Period.find_by_name(params[:period])
|
||||
if @period.nil?
|
||||
start_date = params[:start_date].presence&.to_date
|
||||
end_date = params[:end_date].presence&.to_date
|
||||
if start_date.is_a?(Date) && end_date.is_a?(Date) && start_date <= end_date
|
||||
@period = Period.new(name: "custom", date_range: start_date..end_date)
|
||||
else
|
||||
params[:period] = "last_30_days"
|
||||
@period = Period.find_by_name(params[:period])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
21
app/controllers/concerns/impersonatable.rb
Normal file
21
app/controllers/concerns/impersonatable.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
module Impersonatable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
after_action :create_impersonation_session_log
|
||||
end
|
||||
|
||||
private
|
||||
def create_impersonation_session_log
|
||||
return unless Current.session&.active_impersonator_session.present?
|
||||
|
||||
Current.session.active_impersonator_session.logs.create!(
|
||||
controller: controller_name,
|
||||
action: action_name,
|
||||
path: request.fullpath,
|
||||
method: request.method,
|
||||
ip_address: request.ip,
|
||||
user_agent: request.user_agent
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -7,6 +7,7 @@ module Invitable
|
||||
|
||||
private
|
||||
def invite_code_required?
|
||||
return false if @invitation.present?
|
||||
self_hosted? ? Setting.require_invite_for_signup : ENV["REQUIRE_INVITE_CODE"] == "true"
|
||||
end
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ module Localize
|
||||
|
||||
included do
|
||||
around_action :switch_locale
|
||||
around_action :switch_timezone
|
||||
end
|
||||
|
||||
private
|
||||
@@ -10,4 +11,9 @@ module Localize
|
||||
locale = Current.family.try(:locale) || I18n.default_locale
|
||||
I18n.with_locale(locale, &action)
|
||||
end
|
||||
|
||||
def switch_timezone(&action)
|
||||
timezone = Current.family.try(:timezone) || Time.zone
|
||||
Time.use_zone(timezone, &action)
|
||||
end
|
||||
end
|
||||
|
||||
17
app/controllers/concerns/onboardable.rb
Normal file
17
app/controllers/concerns/onboardable.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
module Onboardable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :redirect_to_onboarding, if: :needs_onboarding?
|
||||
end
|
||||
|
||||
private
|
||||
def redirect_to_onboarding
|
||||
redirect_to onboarding_path
|
||||
end
|
||||
|
||||
def needs_onboarding?
|
||||
Current.user && Current.user.onboarded_at.blank? &&
|
||||
!%w[/users /onboarding /sessions].any? { |path| request.path.start_with?(path) }
|
||||
end
|
||||
end
|
||||
@@ -5,6 +5,8 @@ module StoreLocation
|
||||
helper_method :previous_path
|
||||
before_action :store_return_to
|
||||
after_action :clear_previous_path
|
||||
|
||||
rescue_from ActiveRecord::RecordNotFound, with: :handle_not_found
|
||||
end
|
||||
|
||||
def previous_path
|
||||
@@ -12,6 +14,14 @@ module StoreLocation
|
||||
end
|
||||
|
||||
private
|
||||
def handle_not_found
|
||||
if request.fullpath == session[:return_to]
|
||||
session.delete(:return_to)
|
||||
redirect_to fallback_path
|
||||
else
|
||||
head :not_found
|
||||
end
|
||||
end
|
||||
|
||||
def store_return_to
|
||||
if params[:return_to].present?
|
||||
|
||||
@@ -1,41 +1,12 @@
|
||||
class CreditCardsController < ApplicationController
|
||||
before_action :set_account, only: :update
|
||||
include AccountableResource
|
||||
|
||||
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
|
||||
permitted_accountable_attributes(
|
||||
:id,
|
||||
:available_credit,
|
||||
:minimum_payment,
|
||||
:apr,
|
||||
:annual_fee,
|
||||
:expiration_date
|
||||
)
|
||||
end
|
||||
|
||||
3
app/controllers/cryptos_controller.rb
Normal file
3
app/controllers/cryptos_controller.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
class CryptosController < ApplicationController
|
||||
include AccountableResource
|
||||
end
|
||||
3
app/controllers/depositories_controller.rb
Normal file
3
app/controllers/depositories_controller.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
class DepositoriesController < ApplicationController
|
||||
include AccountableResource
|
||||
end
|
||||
58
app/controllers/impersonation_sessions_controller.rb
Normal file
58
app/controllers/impersonation_sessions_controller.rb
Normal file
@@ -0,0 +1,58 @@
|
||||
class ImpersonationSessionsController < ApplicationController
|
||||
before_action :require_super_admin!, only: [ :create, :join, :leave ]
|
||||
before_action :set_impersonation_session, only: [ :approve, :reject, :complete ]
|
||||
|
||||
def create
|
||||
Current.true_user.request_impersonation_for(session_params[:impersonated_id])
|
||||
redirect_to root_path, notice: t(".success")
|
||||
end
|
||||
|
||||
def join
|
||||
@impersonation_session = Current.true_user.impersonator_support_sessions.find_by(id: params[:impersonation_session_id])
|
||||
Current.session.update!(active_impersonator_session: @impersonation_session)
|
||||
redirect_to root_path, notice: t(".success")
|
||||
end
|
||||
|
||||
def leave
|
||||
Current.session.update!(active_impersonator_session: nil)
|
||||
redirect_to root_path, notice: t(".success")
|
||||
end
|
||||
|
||||
def approve
|
||||
raise_unauthorized! unless @impersonation_session.impersonated == Current.true_user
|
||||
|
||||
@impersonation_session.approve!
|
||||
redirect_to root_path, notice: t(".success")
|
||||
end
|
||||
|
||||
def reject
|
||||
raise_unauthorized! unless @impersonation_session.impersonated == Current.true_user
|
||||
|
||||
@impersonation_session.reject!
|
||||
redirect_to root_path, notice: t(".success")
|
||||
end
|
||||
|
||||
def complete
|
||||
@impersonation_session.complete!
|
||||
redirect_to root_path, notice: t(".success")
|
||||
end
|
||||
|
||||
private
|
||||
def session_params
|
||||
params.require(:impersonation_session).permit(:impersonated_id)
|
||||
end
|
||||
|
||||
def set_impersonation_session
|
||||
@impersonation_session =
|
||||
Current.true_user.impersonated_support_sessions.find_by(id: params[:id]) ||
|
||||
Current.true_user.impersonator_support_sessions.find_by(id: params[:id])
|
||||
end
|
||||
|
||||
def require_super_admin!
|
||||
raise_unauthorized! unless Current.true_user&.super_admin?
|
||||
end
|
||||
|
||||
def raise_unauthorized!
|
||||
raise ActionController::RoutingError.new("Not Found")
|
||||
end
|
||||
end
|
||||
@@ -20,6 +20,21 @@ class Import::ConfigurationsController < ApplicationController
|
||||
end
|
||||
|
||||
def import_params
|
||||
params.require(:import).permit(:date_col_label, :date_format, :name_col_label, :category_col_label, :tags_col_label, :amount_col_label, :signage_convention, :account_col_label, :notes_col_label, :entity_type_col_label)
|
||||
params.require(:import).permit(
|
||||
:date_col_label,
|
||||
:amount_col_label,
|
||||
:name_col_label,
|
||||
:category_col_label,
|
||||
:tags_col_label,
|
||||
:account_col_label,
|
||||
:qty_col_label,
|
||||
:ticker_col_label,
|
||||
:price_col_label,
|
||||
:entity_type_col_label,
|
||||
:notes_col_label,
|
||||
:currency_col_label,
|
||||
:date_format,
|
||||
:signage_convention
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -32,7 +32,7 @@ class Import::UploadsController < ApplicationController
|
||||
require "csv"
|
||||
|
||||
begin
|
||||
csv = CSV.parse(str || "", headers: true)
|
||||
csv = CSV.parse(str || "", headers: true, col_sep: upload_params[:col_sep])
|
||||
return false if csv.headers.empty?
|
||||
return false if csv.count == 0
|
||||
true
|
||||
|
||||
@@ -24,7 +24,11 @@ class ImportsController < ApplicationController
|
||||
end
|
||||
|
||||
def show
|
||||
redirect_to import_confirm_path(@import), alert: "Please finalize your mappings before proceeding." unless @import.publishable?
|
||||
if !@import.uploaded?
|
||||
redirect_to import_upload_path(@import), alert: "Please finalize your file upload."
|
||||
elsif !@import.publishable?
|
||||
redirect_to import_confirm_path(@import), alert: "Please finalize your mappings before proceeding."
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
class InstitutionsController < ApplicationController
|
||||
before_action :set_institution, except: %i[new create]
|
||||
|
||||
def new
|
||||
@institution = Institution.new
|
||||
end
|
||||
|
||||
def create
|
||||
Current.family.institutions.create!(institution_params)
|
||||
redirect_to accounts_path, notice: t(".success")
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
@institution.update!(institution_params)
|
||||
redirect_to accounts_path, notice: t(".success")
|
||||
end
|
||||
|
||||
def destroy
|
||||
@institution.destroy!
|
||||
redirect_to accounts_path, notice: t(".success")
|
||||
end
|
||||
|
||||
def sync
|
||||
@institution.sync
|
||||
redirect_back_or_to accounts_path, notice: t(".success")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def institution_params
|
||||
params.require(:institution).permit(:name, :logo)
|
||||
end
|
||||
|
||||
def set_institution
|
||||
@institution = Current.family.institutions.find(params[:id])
|
||||
end
|
||||
end
|
||||
3
app/controllers/investments_controller.rb
Normal file
3
app/controllers/investments_controller.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
class InvestmentsController < ApplicationController
|
||||
include AccountableResource
|
||||
end
|
||||
42
app/controllers/invitations_controller.rb
Normal file
42
app/controllers/invitations_controller.rb
Normal file
@@ -0,0 +1,42 @@
|
||||
class InvitationsController < ApplicationController
|
||||
skip_authentication only: :accept
|
||||
def new
|
||||
@invitation = Invitation.new
|
||||
end
|
||||
|
||||
def create
|
||||
unless Current.user.admin?
|
||||
flash[:alert] = t(".failure")
|
||||
redirect_to settings_profile_path
|
||||
return
|
||||
end
|
||||
|
||||
@invitation = Current.family.invitations.build(invitation_params)
|
||||
@invitation.inviter = Current.user
|
||||
|
||||
if @invitation.save
|
||||
InvitationMailer.invite_email(@invitation).deliver_later unless self_hosted?
|
||||
flash[:notice] = t(".success")
|
||||
else
|
||||
flash[:alert] = t(".failure")
|
||||
end
|
||||
|
||||
redirect_to settings_profile_path
|
||||
end
|
||||
|
||||
def accept
|
||||
@invitation = Invitation.find_by!(token: params[:id])
|
||||
|
||||
if @invitation.pending?
|
||||
redirect_to new_registration_path(invitation: @invitation.token)
|
||||
else
|
||||
raise ActiveRecord::RecordNotFound
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def invitation_params
|
||||
params.require(:invitation).permit(:email, :role)
|
||||
end
|
||||
end
|
||||
@@ -3,8 +3,9 @@ class Issue::ExchangeRateProviderMissingsController < ApplicationController
|
||||
|
||||
def update
|
||||
Setting.synth_api_key = exchange_rate_params[:synth_api_key]
|
||||
@issue.issuable.sync_later
|
||||
redirect_back_or_to account_path(@issue.issuable)
|
||||
account = @issue.issuable
|
||||
account.sync_later
|
||||
redirect_back_or_to account
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -1,39 +1,7 @@
|
||||
class LoansController < ApplicationController
|
||||
before_action :set_account, only: :update
|
||||
include AccountableResource
|
||||
|
||||
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
|
||||
permitted_accountable_attributes(
|
||||
:id, :rate_type, :interest_rate, :term_months
|
||||
)
|
||||
end
|
||||
|
||||
@@ -12,8 +12,13 @@ class MerchantsController < ApplicationController
|
||||
end
|
||||
|
||||
def create
|
||||
Current.family.merchants.create!(merchant_params)
|
||||
redirect_to merchants_path, notice: t(".success")
|
||||
@merchant = Current.family.merchants.new(merchant_params)
|
||||
|
||||
if @merchant.save
|
||||
redirect_to merchants_path, notice: t(".success")
|
||||
else
|
||||
redirect_to merchants_path, alert: t(".error", error: @merchant.errors.full_messages.to_sentence)
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
|
||||
24
app/controllers/onboardings_controller.rb
Normal file
24
app/controllers/onboardings_controller.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
class OnboardingsController < ApplicationController
|
||||
layout "application"
|
||||
before_action :set_user
|
||||
before_action :load_invitation
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def profile
|
||||
end
|
||||
|
||||
def preferences
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_user
|
||||
@user = Current.user
|
||||
end
|
||||
|
||||
def load_invitation
|
||||
@invitation = Current.family.invitations.accepted.find_by(email: Current.user.email)
|
||||
end
|
||||
end
|
||||
3
app/controllers/other_assets_controller.rb
Normal file
3
app/controllers/other_assets_controller.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
class OtherAssetsController < ApplicationController
|
||||
include AccountableResource
|
||||
end
|
||||
3
app/controllers/other_liabilities_controller.rb
Normal file
3
app/controllers/other_liabilities_controller.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
class OtherLiabilitiesController < ApplicationController
|
||||
include AccountableResource
|
||||
end
|
||||
@@ -2,9 +2,8 @@ class PagesController < ApplicationController
|
||||
skip_before_action :authenticate_user!, only: %i[early_access]
|
||||
layout :with_sidebar, except: %i[early_access]
|
||||
|
||||
include Filterable
|
||||
|
||||
def dashboard
|
||||
@period = Period.from_param(params[:period])
|
||||
snapshot = Current.family.snapshot(@period)
|
||||
@net_worth_series = snapshot[:net_worth_series]
|
||||
@asset_series = snapshot[:asset_series]
|
||||
@@ -20,7 +19,7 @@ class PagesController < ApplicationController
|
||||
@top_earners = snapshot_account_transactions[:top_earners]
|
||||
@top_savers = snapshot_account_transactions[:top_savers]
|
||||
|
||||
@accounts = Current.family.accounts
|
||||
@accounts = Current.family.accounts.active
|
||||
@account_groups = @accounts.by_group(period: @period, currency: Current.family.currency)
|
||||
@transaction_entries = Current.family.entries.account_transactions.limit(6).reverse_chronological
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ class PasswordResetsController < ApplicationController
|
||||
).password_reset.deliver_later
|
||||
end
|
||||
|
||||
redirect_to root_path, notice: t(".requested")
|
||||
redirect_to new_password_reset_path(step: "pending")
|
||||
end
|
||||
|
||||
def edit
|
||||
|
||||
38
app/controllers/plaid_items_controller.rb
Normal file
38
app/controllers/plaid_items_controller.rb
Normal file
@@ -0,0 +1,38 @@
|
||||
class PlaidItemsController < ApplicationController
|
||||
before_action :set_plaid_item, only: %i[destroy sync]
|
||||
|
||||
def create
|
||||
Current.family.plaid_items.create_from_public_token(
|
||||
plaid_item_params[:public_token],
|
||||
item_name: item_name,
|
||||
)
|
||||
|
||||
redirect_to accounts_path, notice: t(".success")
|
||||
end
|
||||
|
||||
def destroy
|
||||
@plaid_item.destroy_later
|
||||
redirect_to accounts_path, notice: t(".success")
|
||||
end
|
||||
|
||||
def sync
|
||||
unless @plaid_item.syncing?
|
||||
@plaid_item.sync_later
|
||||
end
|
||||
|
||||
redirect_to accounts_path
|
||||
end
|
||||
|
||||
private
|
||||
def set_plaid_item
|
||||
@plaid_item = Current.family.plaid_items.find(params[:id])
|
||||
end
|
||||
|
||||
def plaid_item_params
|
||||
params.require(:plaid_item).permit(:public_token, metadata: {})
|
||||
end
|
||||
|
||||
def item_name
|
||||
plaid_item_params.dig(:metadata, :institution, :name)
|
||||
end
|
||||
end
|
||||
@@ -1,40 +1,21 @@
|
||||
class PropertiesController < ApplicationController
|
||||
before_action :set_account, only: :update
|
||||
include AccountableResource
|
||||
|
||||
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]
|
||||
permitted_accountable_attributes(
|
||||
:id, :year_built, :area_unit, :area_value,
|
||||
address_attributes: [ :line1, :line2, :locality, :region, :country, :postal_code ]
|
||||
)
|
||||
|
||||
account.sync_later
|
||||
redirect_to account, notice: t(".success")
|
||||
def new
|
||||
@account = Current.family.accounts.build(
|
||||
currency: Current.family.currency,
|
||||
accountable: Property.new(
|
||||
address: Address.new
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
def update
|
||||
@account.update_with_sync!(account_params)
|
||||
redirect_to @account, notice: t(".success")
|
||||
def edit
|
||||
@account.accountable.address ||= Address.new
|
||||
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,
|
||||
:year_built,
|
||||
:area_unit,
|
||||
:area_value,
|
||||
address_attributes: [ :line1, :line2, :locality, :region, :country, :postal_code ]
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,36 +4,49 @@ class RegistrationsController < ApplicationController
|
||||
layout "auth"
|
||||
|
||||
before_action :set_user, only: :create
|
||||
before_action :set_invitation
|
||||
before_action :claim_invite_code, only: :create, if: :invite_code_required?
|
||||
|
||||
def new
|
||||
@user = User.new
|
||||
@user = User.new(email: @invitation&.email)
|
||||
end
|
||||
|
||||
def create
|
||||
family = Family.new
|
||||
@user.family = family
|
||||
@user.role = :admin
|
||||
if @invitation
|
||||
@user.family = @invitation.family
|
||||
@user.role = @invitation.role
|
||||
@user.email = @invitation.email
|
||||
else
|
||||
family = Family.new
|
||||
@user.family = family
|
||||
@user.role = :admin
|
||||
end
|
||||
|
||||
if @user.save
|
||||
Category.create_default_categories(@user.family)
|
||||
@invitation&.update!(accepted_at: Time.current)
|
||||
Category.create_default_categories(@user.family) unless @invitation
|
||||
@session = create_session_for(@user)
|
||||
flash[:notice] = t(".success")
|
||||
redirect_to root_path
|
||||
redirect_to root_path, notice: t(".success")
|
||||
else
|
||||
flash[:alert] = t(".failure")
|
||||
render :new, status: :unprocessable_entity
|
||||
render :new, status: :unprocessable_entity, alert: t(".failure")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_user
|
||||
@user = User.new user_params.except(:invite_code)
|
||||
def set_invitation
|
||||
token = params[:invitation]
|
||||
token ||= params[:user][:invitation] if params[:user].present?
|
||||
@invitation = Invitation.pending.find_by(token: token)
|
||||
end
|
||||
|
||||
def user_params
|
||||
params.require(:user).permit(:name, :email, :password, :password_confirmation, :invite_code)
|
||||
def set_user
|
||||
@user = User.new user_params.except(:invite_code, :invitation)
|
||||
end
|
||||
|
||||
def user_params(specific_param = nil)
|
||||
params = self.params.require(:user).permit(:name, :email, :password, :password_confirmation, :invite_code, :invitation)
|
||||
specific_param ? params[specific_param] : params
|
||||
end
|
||||
|
||||
def claim_invite_code
|
||||
|
||||
18
app/controllers/securities_controller.rb
Normal file
18
app/controllers/securities_controller.rb
Normal file
@@ -0,0 +1,18 @@
|
||||
class SecuritiesController < ApplicationController
|
||||
def index
|
||||
query = params[:q]
|
||||
return render json: [] if query.blank? || query.length < 2 || query.length > 100
|
||||
|
||||
@securities = Security.search({
|
||||
search: query,
|
||||
country: country_code_filter
|
||||
})
|
||||
end
|
||||
|
||||
private
|
||||
def country_code_filter
|
||||
filter = params[:country_code]
|
||||
filter = "#{filter},US" unless filter == "US"
|
||||
filter
|
||||
end
|
||||
end
|
||||
@@ -19,7 +19,7 @@ class SessionsController < ApplicationController
|
||||
|
||||
def destroy
|
||||
@session.destroy
|
||||
redirect_to root_path, notice: t(".logout_successful")
|
||||
redirect_to new_session_path, notice: t(".logout_successful")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
class Settings::BillingsController < SettingsController
|
||||
def show
|
||||
@user = Current.user
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,26 +1,5 @@
|
||||
class Settings::PreferencesController < SettingsController
|
||||
def edit
|
||||
def show
|
||||
@user = Current.user
|
||||
end
|
||||
|
||||
def update
|
||||
preference_params_with_family = preference_params
|
||||
|
||||
if Current.family && preference_params[:family_attributes]
|
||||
family_attributes = preference_params[:family_attributes].merge({ id: Current.family.id })
|
||||
preference_params_with_family[:family_attributes] = family_attributes
|
||||
end
|
||||
|
||||
if Current.user.update(preference_params_with_family)
|
||||
redirect_to settings_preferences_path, notice: t(".success")
|
||||
else
|
||||
redirect_to settings_preferences_path, notice: t(".success")
|
||||
render :show, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def preference_params
|
||||
params.require(:user).permit(family_attributes: [ :id, :currency, :locale ])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,38 +1,7 @@
|
||||
class Settings::ProfilesController < SettingsController
|
||||
def show
|
||||
@user = Current.user
|
||||
@users = Current.family.users.order(:created_at)
|
||||
@pending_invitations = Current.family.invitations.pending
|
||||
end
|
||||
|
||||
def update
|
||||
user_params_with_family = user_params
|
||||
|
||||
if params[:user][:delete_profile_image] == "true"
|
||||
Current.user.profile_image.purge
|
||||
end
|
||||
|
||||
if Current.family && user_params_with_family[:family_attributes]
|
||||
family_attributes = user_params_with_family[:family_attributes].merge({ id: Current.family.id })
|
||||
user_params_with_family[:family_attributes] = family_attributes
|
||||
end
|
||||
|
||||
if Current.user.update(user_params_with_family)
|
||||
redirect_to settings_profile_path, notice: t(".success")
|
||||
else
|
||||
redirect_to settings_profile_path, alert: Current.user.errors.full_messages.to_sentence
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if Current.user.deactivate
|
||||
Current.session.destroy
|
||||
redirect_to root_path, notice: t(".success")
|
||||
else
|
||||
redirect_to settings_profile_path, alert: Current.user.errors.full_messages.to_sentence
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def user_params
|
||||
params.require(:user).permit(:first_name, :last_name, :profile_image,
|
||||
family_attributes: [ :name, :id ])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
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(
|
||||
customer = stripe_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({
|
||||
session = stripe_client.v1.checkout.sessions.create({
|
||||
customer: Current.family.stripe_customer_id,
|
||||
line_items: [ {
|
||||
price: ENV["STRIPE_PLAN_ID"],
|
||||
@@ -18,7 +16,7 @@ class SubscriptionsController < ApplicationController
|
||||
} ],
|
||||
mode: "subscription",
|
||||
allow_promotion_codes: true,
|
||||
success_url: settings_billing_url,
|
||||
success_url: success_subscription_url + "?session_id={CHECKOUT_SESSION_ID}",
|
||||
cancel_url: settings_billing_url
|
||||
})
|
||||
|
||||
@@ -26,12 +24,24 @@ class SubscriptionsController < ApplicationController
|
||||
end
|
||||
|
||||
def show
|
||||
client = Stripe::StripeClient.new(ENV["STRIPE_SECRET_KEY"])
|
||||
|
||||
portal_session = client.v1.billing_portal.sessions.create(
|
||||
portal_session = stripe_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
|
||||
|
||||
def success
|
||||
checkout_session = stripe_client.v1.checkout.sessions.retrieve(params[:session_id])
|
||||
Current.session.update(subscribed_at: Time.at(checkout_session.created))
|
||||
redirect_to root_path, notice: "You have successfully subscribed to Maybe+."
|
||||
rescue Stripe::InvalidRequestError
|
||||
redirect_to settings_billing_path, alert: "Something went wrong processing your subscription. Please contact us to get this fixed."
|
||||
end
|
||||
|
||||
private
|
||||
def stripe_client
|
||||
@stripe_client ||= Stripe::StripeClient.new(ENV["STRIPE_SECRET_KEY"])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class TagsController < ApplicationController
|
||||
layout :with_sidebar
|
||||
|
||||
before_action :set_tag, only: %i[edit update]
|
||||
before_action :set_tag, only: %i[edit update destroy]
|
||||
|
||||
def index
|
||||
@tags = Current.family.tags.alphabetically
|
||||
@@ -12,8 +12,13 @@ class TagsController < ApplicationController
|
||||
end
|
||||
|
||||
def create
|
||||
Current.family.tags.create!(tag_params)
|
||||
redirect_to tags_path, notice: t(".created")
|
||||
@tag = Current.family.tags.new(tag_params)
|
||||
|
||||
if @tag.save
|
||||
redirect_to tags_path, notice: t(".created")
|
||||
else
|
||||
redirect_to tags_path, alert: t(".error", error: @tag.errors.full_messages.to_sentence)
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
@@ -24,6 +29,11 @@ class TagsController < ApplicationController
|
||||
redirect_to tags_path, notice: t(".updated")
|
||||
end
|
||||
|
||||
def destroy
|
||||
@tag.destroy!
|
||||
redirect_to tags_path, notice: t(".deleted")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_tag
|
||||
|
||||
@@ -13,93 +13,13 @@ class TransactionsController < ApplicationController
|
||||
}
|
||||
end
|
||||
|
||||
def new
|
||||
@entry = Current.family.entries.new(entryable: Account::Transaction.new).tap do |e|
|
||||
if params[:account_id]
|
||||
e.account = Current.family.accounts.find(params[:account_id])
|
||||
e.currency = e.account.currency
|
||||
else
|
||||
e.currency = Current.family.currency
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
@entry = Current.family
|
||||
.accounts
|
||||
.find(params[:account_entry][:account_id])
|
||||
.entries
|
||||
.create!(transaction_entry_params.merge(amount: amount))
|
||||
|
||||
@entry.sync_account_later
|
||||
redirect_back_or_to account_path(@entry.account), notice: t(".success")
|
||||
end
|
||||
|
||||
def bulk_delete
|
||||
destroyed = Current.family.entries.destroy_by(id: bulk_delete_params[:entry_ids])
|
||||
redirect_back_or_to transactions_url, notice: t(".success", count: destroyed.count)
|
||||
end
|
||||
|
||||
def bulk_edit
|
||||
end
|
||||
|
||||
def bulk_update
|
||||
updated = Current.family
|
||||
.entries
|
||||
.where(id: bulk_update_params[:entry_ids])
|
||||
.bulk_update!(bulk_update_params)
|
||||
|
||||
redirect_back_or_to transactions_url, notice: t(".success", count: updated)
|
||||
end
|
||||
|
||||
def mark_transfers
|
||||
Current.family
|
||||
.entries
|
||||
.where(id: bulk_update_params[:entry_ids])
|
||||
.mark_transfers!
|
||||
|
||||
redirect_back_or_to transactions_url, notice: t(".success")
|
||||
end
|
||||
|
||||
def unmark_transfers
|
||||
Current.family
|
||||
.entries
|
||||
.where(id: bulk_update_params[:entry_ids])
|
||||
.update_all marked_as_transfer: false
|
||||
|
||||
redirect_back_or_to transactions_url, notice: t(".success")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def amount
|
||||
if nature.income?
|
||||
transaction_entry_params[:amount].to_d * -1
|
||||
else
|
||||
transaction_entry_params[:amount].to_d
|
||||
end
|
||||
end
|
||||
|
||||
def nature
|
||||
params[:account_entry][:nature].to_s.inquiry
|
||||
end
|
||||
|
||||
def bulk_delete_params
|
||||
params.require(:bulk_delete).permit(entry_ids: [])
|
||||
end
|
||||
|
||||
def bulk_update_params
|
||||
params.require(:bulk_update).permit(:date, :notes, :category_id, :merchant_id, entry_ids: [])
|
||||
end
|
||||
|
||||
def search_params
|
||||
params.fetch(:q, {})
|
||||
.permit(:start_date, :end_date, :search, :amount, :amount_operator, accounts: [], account_ids: [], categories: [], merchants: [], types: [], tags: [])
|
||||
end
|
||||
|
||||
def transaction_entry_params
|
||||
params.require(:account_entry)
|
||||
.permit(:name, :date, :amount, :currency, :entryable_type, entryable_attributes: [ :category_id ])
|
||||
.with_defaults(entryable_type: "Account::Transaction", entryable_attributes: {})
|
||||
.permit(
|
||||
:start_date, :end_date, :search, :amount,
|
||||
:amount_operator, accounts: [], account_ids: [],
|
||||
categories: [], merchants: [], types: [], tags: []
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
51
app/controllers/users_controller.rb
Normal file
51
app/controllers/users_controller.rb
Normal file
@@ -0,0 +1,51 @@
|
||||
class UsersController < ApplicationController
|
||||
before_action :set_user
|
||||
|
||||
def update
|
||||
@user = Current.user
|
||||
|
||||
@user.update!(user_params.except(:redirect_to, :delete_profile_image))
|
||||
@user.profile_image.purge if should_purge_profile_image?
|
||||
|
||||
handle_redirect(t(".success"))
|
||||
end
|
||||
|
||||
def destroy
|
||||
if @user.deactivate
|
||||
Current.session.destroy
|
||||
redirect_to root_path, notice: t(".success")
|
||||
else
|
||||
redirect_to settings_profile_path, alert: @user.errors.full_messages.to_sentence
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def handle_redirect(notice)
|
||||
case user_params[:redirect_to]
|
||||
when "onboarding_preferences"
|
||||
redirect_to preferences_onboarding_path
|
||||
when "home"
|
||||
redirect_to root_path
|
||||
when "preferences"
|
||||
redirect_to settings_preferences_path, notice: notice
|
||||
else
|
||||
redirect_to settings_profile_path, notice: notice
|
||||
end
|
||||
end
|
||||
|
||||
def should_purge_profile_image?
|
||||
user_params[:delete_profile_image] == "1" &&
|
||||
user_params[:profile_image].blank?
|
||||
end
|
||||
|
||||
def user_params
|
||||
params.require(:user).permit(
|
||||
:first_name, :last_name, :profile_image, :redirect_to, :delete_profile_image, :onboarded_at,
|
||||
family_attributes: [ :name, :currency, :country, :locale, :date_format, :timezone, :id ]
|
||||
)
|
||||
end
|
||||
|
||||
def set_user
|
||||
@user = Current.user
|
||||
end
|
||||
end
|
||||
@@ -1,41 +1,7 @@
|
||||
class VehiclesController < ApplicationController
|
||||
before_action :set_account, only: :update
|
||||
include AccountableResource
|
||||
|
||||
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,
|
||||
:make,
|
||||
:model,
|
||||
:year,
|
||||
:mileage_value,
|
||||
:mileage_unit
|
||||
]
|
||||
)
|
||||
end
|
||||
permitted_accountable_attributes(
|
||||
:id, :make, :model, :year, :mileage_value, :mileage_unit
|
||||
)
|
||||
end
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
class WebhooksController < ApplicationController
|
||||
skip_before_action :verify_authenticity_token, only: [ :stripe ]
|
||||
skip_before_action :verify_authenticity_token
|
||||
skip_authentication
|
||||
|
||||
def plaid
|
||||
webhook_body = request.body.read
|
||||
plaid_verification_header = request.headers["Plaid-Verification"]
|
||||
|
||||
Provider::Plaid.validate_webhook!(plaid_verification_header, webhook_body)
|
||||
Provider::Plaid.process_webhook(webhook_body)
|
||||
|
||||
render json: { received: true }, status: :ok
|
||||
rescue => error
|
||||
render json: { error: "Invalid webhook: #{error.message}" }, status: :bad_request
|
||||
end
|
||||
|
||||
def stripe
|
||||
webhook_body = request.body.read
|
||||
sig_header = request.env["HTTP_STRIPE_SIGNATURE"]
|
||||
|
||||
@@ -12,43 +12,13 @@ module Account::EntriesHelper
|
||||
transfers.map(&:transfer).uniq
|
||||
end
|
||||
|
||||
def entry_icon(entry, is_oldest: false)
|
||||
if is_oldest
|
||||
"keyboard"
|
||||
elsif entry.trend.direction.up?
|
||||
"arrow-up"
|
||||
elsif entry.trend.direction.down?
|
||||
"arrow-down"
|
||||
else
|
||||
"minus"
|
||||
end
|
||||
end
|
||||
|
||||
def entry_style(entry, is_oldest: false)
|
||||
color = is_oldest ? "#D444F1" : entry.trend.color
|
||||
|
||||
mixed_hex_styles(color)
|
||||
end
|
||||
|
||||
def entry_name(entry)
|
||||
if entry.account_trade?
|
||||
trade = entry.account_trade
|
||||
prefix = trade.sell? ? "Sell " : "Buy "
|
||||
generated = prefix + "#{trade.qty.abs} shares of #{trade.security.ticker}"
|
||||
name = entry.name || generated
|
||||
name
|
||||
else
|
||||
entry.name || "Transaction"
|
||||
end
|
||||
end
|
||||
|
||||
def entries_by_date(entries, selectable: true)
|
||||
entries.group_by(&:date).map do |date, grouped_entries|
|
||||
def entries_by_date(entries, selectable: true, totals: false)
|
||||
entries.reverse_chronological.group_by(&:date).map do |date, grouped_entries|
|
||||
content = capture do
|
||||
yield grouped_entries
|
||||
end
|
||||
|
||||
render partial: "account/entries/entry_group", locals: { date:, entries: grouped_entries, content:, selectable: }
|
||||
render partial: "account/entries/entry_group", locals: { date:, entries: grouped_entries, content:, selectable:, totals: }
|
||||
end.join.html_safe
|
||||
end
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
module Account::CashesHelper
|
||||
def brokerage_cash(account)
|
||||
module Account::HoldingsHelper
|
||||
def brokerage_cash_holding(account)
|
||||
currency = Money::Currency.new(account.currency)
|
||||
|
||||
account.holdings.build \
|
||||
date: Date.current,
|
||||
qty: account.balance,
|
||||
qty: account.cash_balance,
|
||||
price: 1,
|
||||
amount: account.balance,
|
||||
currency: account.currency,
|
||||
amount: account.cash_balance,
|
||||
currency: currency.iso_code,
|
||||
security: Security.new(ticker: currency.iso_code, name: currency.name)
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,27 @@
|
||||
module AccountsHelper
|
||||
def period_label(period)
|
||||
return "since account creation" if period.date_range.begin.nil?
|
||||
start_date, end_date = period.date_range.first, period.date_range.last
|
||||
|
||||
return "Starting from #{start_date.strftime('%b %d, %Y')}" if end_date.nil?
|
||||
return "Ending at #{end_date.strftime('%b %d, %Y')}" if start_date.nil?
|
||||
|
||||
days_apart = (end_date - start_date).to_i
|
||||
|
||||
case days_apart
|
||||
when 1
|
||||
"vs. yesterday"
|
||||
when 7
|
||||
"vs. last week"
|
||||
when 30, 31
|
||||
"vs. last month"
|
||||
when 365, 366
|
||||
"vs. last year"
|
||||
else
|
||||
"from #{start_date.strftime('%b %d, %Y')} to #{end_date.strftime('%b %d, %Y')}"
|
||||
end
|
||||
end
|
||||
|
||||
def summary_card(title:, &block)
|
||||
content = capture(&block)
|
||||
render "accounts/summary_card", title: title, content: content
|
||||
@@ -28,65 +51,9 @@ module AccountsHelper
|
||||
class_mapping(accountable_type)[:hex]
|
||||
end
|
||||
|
||||
# Eventually, we'll have an accountable form for each type of accountable, so
|
||||
# this helper is a convenience for now to reuse common logic in the accounts controller
|
||||
def new_account_form_url(account)
|
||||
case account.accountable_type
|
||||
when "Property"
|
||||
properties_path
|
||||
when "Vehicle"
|
||||
vehicles_path
|
||||
when "Loan"
|
||||
loans_path
|
||||
when "CreditCard"
|
||||
credit_cards_path
|
||||
else
|
||||
accounts_path
|
||||
end
|
||||
end
|
||||
|
||||
def edit_account_form_url(account)
|
||||
case account.accountable_type
|
||||
when "Property"
|
||||
property_path(account)
|
||||
when "Vehicle"
|
||||
vehicle_path(account)
|
||||
when "Loan"
|
||||
loan_path(account)
|
||||
when "CreditCard"
|
||||
credit_card_path(account)
|
||||
else
|
||||
account_path(account)
|
||||
end
|
||||
end
|
||||
|
||||
def account_tabs(account)
|
||||
overview_tab = { key: "overview", label: t("accounts.show.overview"), path: account_path(account, tab: "overview"), partial_path: "accounts/overview" }
|
||||
holdings_tab = { key: "holdings", label: t("accounts.show.holdings"), path: account_path(account, tab: "holdings"), route: account_holdings_path(account) }
|
||||
cash_tab = { key: "cash", label: t("accounts.show.cash"), path: account_path(account, tab: "cash"), route: account_cashes_path(account) }
|
||||
value_tab = { key: "valuations", label: t("accounts.show.value"), path: account_path(account, tab: "valuations"), route: account_valuations_path(account) }
|
||||
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, value_tab ] if account.investment?
|
||||
return [ overview_tab, value_tab, transactions_tab ] if account.loan? || account.credit_card?
|
||||
|
||||
[ value_tab, transactions_tab ]
|
||||
end
|
||||
|
||||
def selected_account_tab(account)
|
||||
available_tabs = account_tabs(account)
|
||||
|
||||
tab = available_tabs.find { |tab| tab[:key] == params[:tab] }
|
||||
|
||||
tab || available_tabs.first
|
||||
end
|
||||
|
||||
def account_groups(period: nil)
|
||||
assets, liabilities = Current.family.accounts.by_group(currency: Current.family.currency, period: period || Period.last_30_days).values_at(:assets, :liabilities)
|
||||
[ assets.children, liabilities.children ].flatten
|
||||
assets, liabilities = Current.family.accounts.active.by_group(currency: Current.family.currency, period: period || Period.last_30_days).values_at(:assets, :liabilities)
|
||||
[ assets.children.sort_by(&:name), liabilities.children.sort_by(&:name) ].flatten
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
module ApplicationHelper
|
||||
include Pagy::Frontend
|
||||
|
||||
def date_format_options
|
||||
[
|
||||
[ "DD-MM-YYYY", "%d-%m-%Y" ],
|
||||
[ "DD.MM.YY", "%d.%m.%Y" ],
|
||||
[ "MM-DD-YYYY", "%m-%d-%Y" ],
|
||||
[ "YYYY-MM-DD", "%Y-%m-%d" ],
|
||||
[ "DD/MM/YYYY", "%d/%m/%Y" ],
|
||||
[ "YYYY/MM/DD", "%Y/%m/%d" ],
|
||||
[ "MM/DD/YYYY", "%m/%d/%Y" ],
|
||||
[ "D/MM/YYYY", "%e/%m/%Y" ],
|
||||
[ "YYYY.MM.DD", "%Y.%m.%d" ]
|
||||
]
|
||||
end
|
||||
|
||||
def title(page_title)
|
||||
content_for(:title) { page_title }
|
||||
end
|
||||
@@ -9,10 +23,6 @@ module ApplicationHelper
|
||||
content_for(:header_title) { page_title }
|
||||
end
|
||||
|
||||
def permitted_accountable_partial(name)
|
||||
name.underscore
|
||||
end
|
||||
|
||||
def family_notifications_stream
|
||||
turbo_stream_from [ Current.family, :notifications ] if Current.family
|
||||
end
|
||||
@@ -52,9 +62,9 @@ module ApplicationHelper
|
||||
# <div>Content here</div>
|
||||
# <% end %>
|
||||
#
|
||||
def drawer(&block)
|
||||
def drawer(reload_on_close: false, &block)
|
||||
content = capture &block
|
||||
render partial: "shared/drawer", locals: { content: content }
|
||||
render partial: "shared/drawer", locals: { content:, reload_on_close: }
|
||||
end
|
||||
|
||||
def disclosure(title, &block)
|
||||
@@ -80,8 +90,8 @@ module ApplicationHelper
|
||||
color = hex || "#1570EF" # blue-600
|
||||
|
||||
<<-STYLE.strip
|
||||
background-color: color-mix(in srgb, #{color} 5%, white);
|
||||
border-color: color-mix(in srgb, #{color} 10%, white);
|
||||
background-color: color-mix(in srgb, #{color} 10%, white);
|
||||
border-color: color-mix(in srgb, #{color} 30%, white);
|
||||
color: #{color};
|
||||
STYLE
|
||||
end
|
||||
@@ -113,26 +123,16 @@ module ApplicationHelper
|
||||
{ bg_class: bg_class, text_class: text_class, symbol: symbol, icon: icon }
|
||||
end
|
||||
|
||||
def period_label(period)
|
||||
return "since account creation" if period.date_range.begin.nil?
|
||||
start_date, end_date = period.date_range.first, period.date_range.last
|
||||
# Wrapper around I18n.l to support custom date formats
|
||||
def format_date(object, format = :default, options = {})
|
||||
date = object.to_date
|
||||
|
||||
return "Starting from #{start_date.strftime('%b %d, %Y')}" if end_date.nil?
|
||||
return "Ending at #{end_date.strftime('%b %d, %Y')}" if start_date.nil?
|
||||
format_code = options[:format_code] || Current.family&.date_format
|
||||
|
||||
days_apart = (end_date - start_date).to_i
|
||||
|
||||
case days_apart
|
||||
when 1
|
||||
"vs. yesterday"
|
||||
when 7
|
||||
"vs. last week"
|
||||
when 30, 31
|
||||
"vs. last month"
|
||||
when 365, 366
|
||||
"vs. last year"
|
||||
if format_code.present?
|
||||
date.strftime(format_code)
|
||||
else
|
||||
"from #{start_date.strftime('%b %d, %Y')} to #{end_date.strftime('%b %d, %Y')}"
|
||||
I18n.l(date, format: format, **options)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -158,4 +158,12 @@ module ApplicationHelper
|
||||
.map { |_currency, money| format_money(money) }
|
||||
.join(separator)
|
||||
end
|
||||
|
||||
def show_super_admin_bar?
|
||||
if params[:admin].present?
|
||||
cookies.permanent[:admin] = params[:admin]
|
||||
end
|
||||
|
||||
cookies[:admin] == "true"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,4 +4,8 @@ module CategoriesHelper
|
||||
name: "Uncategorized",
|
||||
color: Category::UNCATEGORIZED_COLOR
|
||||
end
|
||||
|
||||
def family_categories
|
||||
[ null_category ].concat(Current.family.categories.alphabetically)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,7 +18,7 @@ module FormsHelper
|
||||
end
|
||||
|
||||
def period_select(form:, selected:, classes: "border border-alpha-black-100 shadow-xs rounded-lg text-sm pr-7 cursor-pointer text-gray-900 focus:outline-none focus:ring-0")
|
||||
periods_for_select = [ [ "7D", "last_7_days" ], [ "1M", "last_30_days" ], [ "1Y", "last_365_days" ], [ "All", "all" ] ]
|
||||
periods_for_select = [ [ "7D", "last_7_days" ], [ "1M", "last_30_days" ], [ "1Y", "last_365_days" ] ]
|
||||
form.select(:period, periods_for_select, { selected: selected }, class: classes, data: { "auto-submit-form-target": "auto" })
|
||||
end
|
||||
|
||||
|
||||
2
app/helpers/impersonation_sessions_helper.rb
Normal file
2
app/helpers/impersonation_sessions_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
module ImpersonationSessionsHelper
|
||||
end
|
||||
@@ -1,5 +0,0 @@
|
||||
module InstitutionsHelper
|
||||
def institution_logo(institution)
|
||||
institution.logo.attached? ? institution.logo : institution.logo_url
|
||||
end
|
||||
end
|
||||
2
app/helpers/invitations_helper.rb
Normal file
2
app/helpers/invitations_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
module InvitationsHelper
|
||||
end
|
||||
370
app/helpers/languages_helper.rb
Normal file
370
app/helpers/languages_helper.rb
Normal file
@@ -0,0 +1,370 @@
|
||||
module LanguagesHelper
|
||||
LANGUAGE_MAPPING = {
|
||||
en: "English",
|
||||
ru: "Russian",
|
||||
ar: "Arabic",
|
||||
bg: "Bulgarian",
|
||||
'ca-CAT': "Catalan (Catalonia)",
|
||||
ca: "Catalan",
|
||||
'da-DK': "Danish (Denmark)",
|
||||
'de-AT': "German (Austria)",
|
||||
'de-CH': "German (Switzerland)",
|
||||
de: "German",
|
||||
ee: "Ewe",
|
||||
'en-AU': "English (Australia)",
|
||||
'en-BORK': "English (Bork)",
|
||||
'en-CA': "English (Canada)",
|
||||
'en-GB': "English (United Kingdom)",
|
||||
'en-IND': "English (India)",
|
||||
'en-KE': "English (Kenya)",
|
||||
'en-MS': "English (Malaysia)",
|
||||
'en-NEP': "English (Nepal)",
|
||||
'en-NG': "English (Nigeria)",
|
||||
'en-NZ': "English (New Zealand)",
|
||||
'en-PAK': "English (Pakistan)",
|
||||
'en-SG': "English (Singapore)",
|
||||
'en-TH': "English (Thailand)",
|
||||
'en-UG': "English (Uganda)",
|
||||
'en-US': "English (United States)",
|
||||
'en-ZA': "English (South Africa)",
|
||||
'en-au-ocker': "English (Australian Ocker)",
|
||||
'es-AR': "Spanish (Argentina)",
|
||||
'es-MX': "Spanish (Mexico)",
|
||||
es: "Spanish",
|
||||
fa: "Persian",
|
||||
'fi-FI': "Finnish (Finland)",
|
||||
fr: "French",
|
||||
'fr-CA': "French (Canada)",
|
||||
'fr-CH': "French (Switzerland)",
|
||||
he: "Hebrew",
|
||||
hy: "Armenian",
|
||||
id: "Indonesian",
|
||||
it: "Italian",
|
||||
ja: "Japanese",
|
||||
ko: "Korean",
|
||||
lt: "Lithuanian",
|
||||
lv: "Latvian",
|
||||
'mi-NZ': "Maori (New Zealand)",
|
||||
'nb-NO': "Norwegian Bokmål (Norway)",
|
||||
nl: "Dutch",
|
||||
'no-NO': "Norwegian (Norway)",
|
||||
pl: "Polish",
|
||||
'pt-BR': "Portuguese (Brazil)",
|
||||
pt: "Portuguese",
|
||||
sk: "Slovak",
|
||||
sv: "Swedish",
|
||||
th: "Thai",
|
||||
tr: "Turkish",
|
||||
uk: "Ukrainian",
|
||||
vi: "Vietnamese",
|
||||
'zh-CN': "Chinese (Simplified)",
|
||||
'zh-TW': "Chinese (Traditional)",
|
||||
af: "Afrikaans",
|
||||
az: "Azerbaijani",
|
||||
be: "Belarusian",
|
||||
bn: "Bengali",
|
||||
bs: "Bosnian",
|
||||
cs: "Czech",
|
||||
cy: "Welsh",
|
||||
da: "Danish",
|
||||
'de-DE': "German (Germany)",
|
||||
dz: "Dzongkha",
|
||||
'el-CY': "Greek (Cyprus)",
|
||||
el: "Greek",
|
||||
'en-CY': "English (Cyprus)",
|
||||
'en-IE': "English (Ireland)",
|
||||
'en-IN': "English (India)",
|
||||
'en-TT': "English (Trinidad and Tobago)",
|
||||
eo: "Esperanto",
|
||||
'es-419': "Spanish (Latin America)",
|
||||
'es-CL': "Spanish (Chile)",
|
||||
'es-CO': "Spanish (Colombia)",
|
||||
'es-CR': "Spanish (Costa Rica)",
|
||||
'es-EC': "Spanish (Ecuador)",
|
||||
'es-ES': "Spanish (Spain)",
|
||||
'es-NI': "Spanish (Nicaragua)",
|
||||
'es-PA': "Spanish (Panama)",
|
||||
'es-PE': "Spanish (Peru)",
|
||||
'es-US': "Spanish (United States)",
|
||||
'es-VE': "Spanish (Venezuela)",
|
||||
et: "Estonian",
|
||||
eu: "Basque",
|
||||
fi: "Finnish",
|
||||
'fr-FR': "French (France)",
|
||||
fy: "Western Frisian",
|
||||
gd: "Scottish Gaelic",
|
||||
gl: "Galician",
|
||||
'hi-IN': "Hindi (India)",
|
||||
hi: "Hindi",
|
||||
hr: "Croatian",
|
||||
hu: "Hungarian",
|
||||
is: "Icelandic",
|
||||
'it-CH': "Italian (Switzerland)",
|
||||
ka: "Georgian",
|
||||
kk: "Kazakh",
|
||||
km: "Khmer",
|
||||
kn: "Kannada",
|
||||
lb: "Luxembourgish",
|
||||
lo: "Lao",
|
||||
mg: "Malagasy",
|
||||
mk: "Macedonian",
|
||||
ml: "Malayalam",
|
||||
mn: "Mongolian",
|
||||
'mr-IN': "Marathi (India)",
|
||||
ms: "Malay",
|
||||
nb: "Norwegian Bokmål",
|
||||
ne: "Nepali",
|
||||
nn: "Norwegian Nynorsk",
|
||||
oc: "Occitan",
|
||||
or: "Odia",
|
||||
pa: "Punjabi",
|
||||
rm: "Romansh",
|
||||
ro: "Romanian",
|
||||
sc: "Sardinian",
|
||||
sl: "Slovenian",
|
||||
sq: "Albanian",
|
||||
sr: "Serbian",
|
||||
st: "Southern Sotho",
|
||||
'sv-FI': "Swedish (Finland)",
|
||||
'sv-SE': "Swedish (Sweden)",
|
||||
sw: "Swahili",
|
||||
ta: "Tamil",
|
||||
te: "Telugu",
|
||||
tl: "Tagalog",
|
||||
tt: "Tatar",
|
||||
ug: "Uyghur",
|
||||
ur: "Urdu",
|
||||
uz: "Uzbek",
|
||||
wo: "Wolof"
|
||||
}.freeze
|
||||
|
||||
EXCLUDED_LOCALES = [
|
||||
# Test locales
|
||||
"en-BORK",
|
||||
"en-au-ocker",
|
||||
# Duplicate locales
|
||||
"fr-FR",
|
||||
"de-DE",
|
||||
"hi-IN",
|
||||
"sv-SE",
|
||||
"ca-CAT",
|
||||
"en-US",
|
||||
"fi-FI",
|
||||
"en-IND"
|
||||
].freeze
|
||||
|
||||
COUNTRY_MAPPING = {
|
||||
AF: "Afghanistan",
|
||||
AL: "Albania",
|
||||
DZ: "Algeria",
|
||||
AD: "Andorra",
|
||||
AO: "Angola",
|
||||
AG: "Antigua and Barbuda",
|
||||
AR: "Argentina",
|
||||
AM: "Armenia",
|
||||
AU: "Australia",
|
||||
AT: "Austria",
|
||||
AZ: "Azerbaijan",
|
||||
BS: "Bahamas",
|
||||
BH: "Bahrain",
|
||||
BD: "Bangladesh",
|
||||
BB: "Barbados",
|
||||
BY: "Belarus",
|
||||
BE: "Belgium",
|
||||
BZ: "Belize",
|
||||
BJ: "Benin",
|
||||
BT: "Bhutan",
|
||||
BO: "Bolivia",
|
||||
BA: "Bosnia and Herzegovina",
|
||||
BW: "Botswana",
|
||||
BR: "Brazil",
|
||||
BN: "Brunei",
|
||||
BG: "Bulgaria",
|
||||
BF: "Burkina Faso",
|
||||
BI: "Burundi",
|
||||
KH: "Cambodia",
|
||||
CM: "Cameroon",
|
||||
CA: "Canada",
|
||||
CV: "Cape Verde",
|
||||
CF: "Central African Republic",
|
||||
TD: "Chad",
|
||||
CL: "Chile",
|
||||
CN: "China",
|
||||
CO: "Colombia",
|
||||
KM: "Comoros",
|
||||
CG: "Congo",
|
||||
CD: "Congo, Democratic Republic of the",
|
||||
CR: "Costa Rica",
|
||||
CI: "Côte d'Ivoire",
|
||||
HR: "Croatia",
|
||||
CU: "Cuba",
|
||||
CY: "Cyprus",
|
||||
CZ: "Czech Republic",
|
||||
DK: "Denmark",
|
||||
DJ: "Djibouti",
|
||||
DM: "Dominica",
|
||||
DO: "Dominican Republic",
|
||||
EC: "Ecuador",
|
||||
EG: "Egypt",
|
||||
SV: "El Salvador",
|
||||
GQ: "Equatorial Guinea",
|
||||
ER: "Eritrea",
|
||||
EE: "Estonia",
|
||||
ET: "Ethiopia",
|
||||
FJ: "Fiji",
|
||||
FI: "Finland",
|
||||
FR: "France",
|
||||
GA: "Gabon",
|
||||
GM: "Gambia",
|
||||
GE: "Georgia",
|
||||
DE: "Germany",
|
||||
GH: "Ghana",
|
||||
GR: "Greece",
|
||||
GD: "Grenada",
|
||||
GT: "Guatemala",
|
||||
GN: "Guinea",
|
||||
GW: "Guinea-Bissau",
|
||||
GY: "Guyana",
|
||||
HT: "Haiti",
|
||||
HN: "Honduras",
|
||||
HU: "Hungary",
|
||||
IS: "Iceland",
|
||||
IN: "India",
|
||||
ID: "Indonesia",
|
||||
IR: "Iran",
|
||||
IQ: "Iraq",
|
||||
IE: "Ireland",
|
||||
IL: "Israel",
|
||||
IT: "Italy",
|
||||
JM: "Jamaica",
|
||||
JP: "Japan",
|
||||
JO: "Jordan",
|
||||
KZ: "Kazakhstan",
|
||||
KE: "Kenya",
|
||||
KI: "Kiribati",
|
||||
KP: "North Korea",
|
||||
KR: "South Korea",
|
||||
KW: "Kuwait",
|
||||
KG: "Kyrgyzstan",
|
||||
LA: "Laos",
|
||||
LV: "Latvia",
|
||||
LB: "Lebanon",
|
||||
LS: "Lesotho",
|
||||
LR: "Liberia",
|
||||
LY: "Libya",
|
||||
LI: "Liechtenstein",
|
||||
LT: "Lithuania",
|
||||
LU: "Luxembourg",
|
||||
MK: "North Macedonia",
|
||||
MG: "Madagascar",
|
||||
MW: "Malawi",
|
||||
MY: "Malaysia",
|
||||
MV: "Maldives",
|
||||
ML: "Mali",
|
||||
MT: "Malta",
|
||||
MH: "Marshall Islands",
|
||||
MR: "Mauritania",
|
||||
MU: "Mauritius",
|
||||
MX: "Mexico",
|
||||
FM: "Micronesia",
|
||||
MD: "Moldova",
|
||||
MC: "Monaco",
|
||||
MN: "Mongolia",
|
||||
ME: "Montenegro",
|
||||
MA: "Morocco",
|
||||
MZ: "Mozambique",
|
||||
MM: "Myanmar",
|
||||
NA: "Namibia",
|
||||
NR: "Nauru",
|
||||
NP: "Nepal",
|
||||
NL: "Netherlands",
|
||||
NZ: "New Zealand",
|
||||
NI: "Nicaragua",
|
||||
NE: "Niger",
|
||||
NG: "Nigeria",
|
||||
NO: "Norway",
|
||||
OM: "Oman",
|
||||
PK: "Pakistan",
|
||||
PW: "Palau",
|
||||
PA: "Panama",
|
||||
PG: "Papua New Guinea",
|
||||
PY: "Paraguay",
|
||||
PE: "Peru",
|
||||
PH: "Philippines",
|
||||
PL: "Poland",
|
||||
PT: "Portugal",
|
||||
QA: "Qatar",
|
||||
RO: "Romania",
|
||||
RU: "Russia",
|
||||
RW: "Rwanda",
|
||||
KN: "Saint Kitts and Nevis",
|
||||
LC: "Saint Lucia",
|
||||
VC: "Saint Vincent and the Grenadines",
|
||||
WS: "Samoa",
|
||||
SM: "San Marino",
|
||||
ST: "Sao Tome and Principe",
|
||||
SA: "Saudi Arabia",
|
||||
SN: "Senegal",
|
||||
RS: "Serbia",
|
||||
SC: "Seychelles",
|
||||
SL: "Sierra Leone",
|
||||
SG: "Singapore",
|
||||
SK: "Slovakia",
|
||||
SI: "Slovenia",
|
||||
SB: "Solomon Islands",
|
||||
SO: "Somalia",
|
||||
ZA: "South Africa",
|
||||
SS: "South Sudan",
|
||||
ES: "Spain",
|
||||
LK: "Sri Lanka",
|
||||
SD: "Sudan",
|
||||
SR: "Suriname",
|
||||
SE: "Sweden",
|
||||
CH: "Switzerland",
|
||||
SY: "Syria",
|
||||
TW: "Taiwan",
|
||||
TJ: "Tajikistan",
|
||||
TZ: "Tanzania",
|
||||
TH: "Thailand",
|
||||
TL: "Timor-Leste",
|
||||
TG: "Togo",
|
||||
TO: "Tonga",
|
||||
TT: "Trinidad and Tobago",
|
||||
TN: "Tunisia",
|
||||
TR: "Turkey",
|
||||
TM: "Turkmenistan",
|
||||
TV: "Tuvalu",
|
||||
UG: "Uganda",
|
||||
UA: "Ukraine",
|
||||
AE: "United Arab Emirates",
|
||||
GB: "United Kingdom",
|
||||
US: "United States",
|
||||
UY: "Uruguay",
|
||||
UZ: "Uzbekistan",
|
||||
VU: "Vanuatu",
|
||||
VA: "Vatican City",
|
||||
VE: "Venezuela",
|
||||
VN: "Vietnam",
|
||||
YE: "Yemen",
|
||||
ZM: "Zambia",
|
||||
ZW: "Zimbabwe"
|
||||
}.freeze
|
||||
|
||||
def country_options
|
||||
COUNTRY_MAPPING.keys.map { |key| [ COUNTRY_MAPPING[key], key ] }
|
||||
end
|
||||
|
||||
def language_options
|
||||
I18n.available_locales
|
||||
.reject { |locale| EXCLUDED_LOCALES.include?(locale.to_s) }
|
||||
.map do |locale|
|
||||
label = LANGUAGE_MAPPING[locale.to_sym] || locale.to_s.humanize
|
||||
[ "#{label} (#{locale})", locale ]
|
||||
end
|
||||
.sort_by { |label, locale| label }
|
||||
end
|
||||
|
||||
def timezone_options
|
||||
ActiveSupport::TimeZone.all.map { |tz| [ tz.name + " (#{tz.tzinfo.identifier})", tz.tzinfo.identifier ] }
|
||||
end
|
||||
end
|
||||
2
app/helpers/securities_helper.rb
Normal file
2
app/helpers/securities_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
module SecuritiesHelper
|
||||
end
|
||||
@@ -5,10 +5,10 @@ module SettingsHelper
|
||||
{ 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.imports_label"), path: :imports_path },
|
||||
{ name: I18n.t("settings.nav.tags_label"), path: :tags_path },
|
||||
{ name: I18n.t("settings.nav.categories_label"), path: :categories_path },
|
||||
{ name: I18n.t("settings.nav.merchants_label"), path: :merchants_path },
|
||||
{ name: I18n.t("settings.nav.imports_label"), path: :imports_path },
|
||||
{ name: I18n.t("settings.nav.whats_new_label"), path: :changelog_path },
|
||||
{ name: I18n.t("settings.nav.feedback_label"), path: :feedback_path }
|
||||
]
|
||||
|
||||
@@ -24,7 +24,7 @@ class StyledFormBuilder < ActionView::Helpers::FormBuilder
|
||||
def select(method, choices, options = {}, html_options = {})
|
||||
merged_html_options = { class: "form-field__input" }.merge(html_options)
|
||||
|
||||
label = build_label(method, options)
|
||||
label = build_label(method, options.merge(required: merged_html_options[:required]))
|
||||
field = super(method, choices, options, merged_html_options)
|
||||
|
||||
build_styled_field(label, field, options, remove_padding_right: true)
|
||||
@@ -33,7 +33,7 @@ class StyledFormBuilder < ActionView::Helpers::FormBuilder
|
||||
def collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
|
||||
merged_html_options = { class: "form-field__input" }.merge(html_options)
|
||||
|
||||
label = build_label(method, options)
|
||||
label = build_label(method, options.merge(required: merged_html_options[:required]))
|
||||
field = super(method, collection, value_method, text_method, options, merged_html_options)
|
||||
|
||||
build_styled_field(label, field, options, remove_padding_right: true)
|
||||
@@ -49,7 +49,12 @@ class StyledFormBuilder < ActionView::Helpers::FormBuilder
|
||||
end
|
||||
|
||||
def submit(value = nil, options = {})
|
||||
merged_options = { class: "btn btn--primary w-full" }.merge(options)
|
||||
default_options = {
|
||||
data: { turbo_submits_with: "Submitting..." },
|
||||
class: "btn btn--primary w-full"
|
||||
}
|
||||
|
||||
merged_options = default_options.merge(options)
|
||||
value, options = nil, value if value.is_a?(Hash)
|
||||
super(value, merged_options)
|
||||
end
|
||||
@@ -68,7 +73,17 @@ class StyledFormBuilder < ActionView::Helpers::FormBuilder
|
||||
|
||||
def build_label(method, options)
|
||||
return "".html_safe unless options[:label]
|
||||
return label(method, class: "form-field__label") if options[:label] == true
|
||||
label(method, options[:label], class: "form-field__label")
|
||||
|
||||
label_text = options[:label]
|
||||
|
||||
if options[:required]
|
||||
label_text = @template.safe_join([
|
||||
label_text == true ? method.to_s.humanize : label_text,
|
||||
@template.tag.span("*", class: "text-red-500 ml-0.5")
|
||||
])
|
||||
end
|
||||
|
||||
return label(method, class: "form-field__label") if label_text == true
|
||||
label(method, label_text, class: "form-field__label")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
|
||||
import "@hotwired/turbo-rails"
|
||||
import "controllers"
|
||||
import "@hotwired/turbo-rails";
|
||||
import "controllers";
|
||||
|
||||
Turbo.StreamActions.redirect = function () {
|
||||
Turbo.visit(this.target);
|
||||
};
|
||||
|
||||
@@ -1,51 +1,51 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import { Controller } from "@hotwired/stimulus";
|
||||
|
||||
// Connects to data-controller="account-collapse"
|
||||
export default class extends Controller {
|
||||
static values = { type: String }
|
||||
initialToggle = false
|
||||
STORAGE_NAME = "accountCollapseStates"
|
||||
static values = { type: String };
|
||||
initialToggle = false;
|
||||
STORAGE_NAME = "accountCollapseStates";
|
||||
|
||||
connect() {
|
||||
this.element.addEventListener("toggle", this.onToggle)
|
||||
this.updateFromLocalStorage()
|
||||
this.element.addEventListener("toggle", this.onToggle);
|
||||
this.updateFromLocalStorage();
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.element.removeEventListener("toggle", this.onToggle)
|
||||
this.element.removeEventListener("toggle", this.onToggle);
|
||||
}
|
||||
|
||||
onToggle = () => {
|
||||
if (this.initialToggle) {
|
||||
this.initialToggle = false
|
||||
return
|
||||
this.initialToggle = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const items = this.getItemsFromLocalStorage()
|
||||
const items = this.getItemsFromLocalStorage();
|
||||
if (items.has(this.typeValue)) {
|
||||
items.delete(this.typeValue)
|
||||
items.delete(this.typeValue);
|
||||
} else {
|
||||
items.add(this.typeValue)
|
||||
items.add(this.typeValue);
|
||||
}
|
||||
localStorage.setItem(this.STORAGE_NAME, JSON.stringify([...items]))
|
||||
}
|
||||
localStorage.setItem(this.STORAGE_NAME, JSON.stringify([...items]));
|
||||
};
|
||||
|
||||
updateFromLocalStorage() {
|
||||
const items = this.getItemsFromLocalStorage()
|
||||
const items = this.getItemsFromLocalStorage();
|
||||
|
||||
if (items.has(this.typeValue)) {
|
||||
this.initialToggle = true
|
||||
this.element.setAttribute("open", "")
|
||||
this.initialToggle = true;
|
||||
this.element.setAttribute("open", "");
|
||||
}
|
||||
}
|
||||
|
||||
getItemsFromLocalStorage() {
|
||||
try {
|
||||
const items = localStorage.getItem(this.STORAGE_NAME)
|
||||
return new Set(items ? JSON.parse(items) : [])
|
||||
const items = localStorage.getItem(this.STORAGE_NAME);
|
||||
return new Set(items ? JSON.parse(items) : []);
|
||||
} catch (error) {
|
||||
console.error("Error parsing items from localStorage:", error)
|
||||
return new Set()
|
||||
console.error("Error parsing items from localStorage:", error);
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Application } from "@hotwired/stimulus"
|
||||
import { Application } from "@hotwired/stimulus";
|
||||
|
||||
const application = Application.start()
|
||||
const application = Application.start();
|
||||
|
||||
// Configure Stimulus development experience
|
||||
application.debug = false
|
||||
window.Stimulus = application
|
||||
application.debug = false;
|
||||
window.Stimulus = application;
|
||||
|
||||
Turbo.setConfirmMethod((message) => {
|
||||
Turbo.config.forms.confirm = (message) => {
|
||||
const dialog = document.getElementById("turbo-confirm");
|
||||
|
||||
try {
|
||||
@@ -34,10 +34,24 @@ Turbo.setConfirmMethod((message) => {
|
||||
dialog.showModal();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
dialog.addEventListener("close", () => {
|
||||
resolve(dialog.returnValue == "confirm")
|
||||
}, { once: true })
|
||||
})
|
||||
})
|
||||
dialog.addEventListener(
|
||||
"close",
|
||||
() => {
|
||||
const confirmed = dialog.returnValue === "confirm";
|
||||
|
||||
export { application }
|
||||
if (!confirmed) {
|
||||
document.getElementById("turbo-confirm-title").innerHTML =
|
||||
"Are you sure?";
|
||||
document.getElementById("turbo-confirm-body").innerHTML =
|
||||
"You will not be able to undo this decision";
|
||||
document.getElementById("turbo-confirm-accept").innerHTML = "Confirm";
|
||||
}
|
||||
|
||||
resolve(confirmed);
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export { application };
|
||||
|
||||
@@ -24,10 +24,31 @@ export default class extends Controller {
|
||||
});
|
||||
}
|
||||
|
||||
handleInput = () => {
|
||||
handleInput = (event) => {
|
||||
const target = event.target
|
||||
|
||||
clearTimeout(this.timeout);
|
||||
this.timeout = setTimeout(() => {
|
||||
this.element.requestSubmit();
|
||||
}, 500);
|
||||
}, this.#debounceTimeout(target));
|
||||
};
|
||||
|
||||
#debounceTimeout(element) {
|
||||
if(element.dataset.autosubmitDebounceTimeout) {
|
||||
return Number.parseInt(element.dataset.autosubmitDebounceTimeout);
|
||||
}
|
||||
|
||||
const type = element.type || element.tagName;
|
||||
|
||||
switch (type.toLowerCase()) {
|
||||
case 'input':
|
||||
case 'textarea':
|
||||
return 500;
|
||||
case 'select-one':
|
||||
case 'select-multiple':
|
||||
return 0;
|
||||
default:
|
||||
return 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,134 +1,158 @@
|
||||
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 targets = [
|
||||
"row",
|
||||
"group",
|
||||
"selectionBar",
|
||||
"selectionBarText",
|
||||
"bulkEditDrawerTitle",
|
||||
];
|
||||
static values = {
|
||||
resource: String,
|
||||
selectedIds: { type: Array, default: [] }
|
||||
}
|
||||
singularLabel: String,
|
||||
pluralLabel: String,
|
||||
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) {
|
||||
element.innerText = `Edit ${this.selectedIdsValue.length} ${this.#pluralizedResourceName()}`
|
||||
element.innerText = `Edit ${
|
||||
this.selectedIdsValue.length
|
||||
} ${this._pluralizedResourceName()}`;
|
||||
}
|
||||
|
||||
submitBulkRequest(e) {
|
||||
const form = e.target.closest("form");
|
||||
const scope = e.params.scope
|
||||
this.#addHiddenFormInputsForSelectedIds(form, `${scope}[entry_ids][]`, this.selectedIdsValue)
|
||||
form.requestSubmit()
|
||||
const scope = e.params.scope;
|
||||
this._addHiddenFormInputsForSelectedIds(
|
||||
form,
|
||||
`${scope}[entry_ids][]`,
|
||||
this.selectedIdsValue,
|
||||
);
|
||||
form.requestSubmit();
|
||||
}
|
||||
|
||||
togglePageSelection(e) {
|
||||
if (e.target.checked) {
|
||||
this.#selectAll()
|
||||
this._selectAll();
|
||||
} else {
|
||||
this.deselectAll()
|
||||
this.deselectAll();
|
||||
}
|
||||
}
|
||||
|
||||
toggleGroupSelection(e) {
|
||||
const group = this.groupTargets.find(group => group.contains(e.target))
|
||||
const group = this.groupTargets.find((group) => group.contains(e.target));
|
||||
|
||||
this.#rowsForGroup(group).forEach(row => {
|
||||
this._rowsForGroup(group).forEach((row) => {
|
||||
if (e.target.checked) {
|
||||
this.#addToSelection(row.dataset.id)
|
||||
this._addToSelection(row.dataset.id);
|
||||
} else {
|
||||
this.#removeFromSelection(row.dataset.id)
|
||||
this._removeFromSelection(row.dataset.id);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
toggleRowSelection(e) {
|
||||
if (e.target.checked) {
|
||||
this.#addToSelection(e.target.dataset.id)
|
||||
this._addToSelection(e.target.dataset.id);
|
||||
} else {
|
||||
this.#removeFromSelection(e.target.dataset.id)
|
||||
this._removeFromSelection(e.target.dataset.id);
|
||||
}
|
||||
}
|
||||
|
||||
deselectAll() {
|
||||
this.selectedIdsValue = []
|
||||
this.element.querySelectorAll('input[type="checkbox"]').forEach(el => el.checked = false)
|
||||
this.selectedIdsValue = [];
|
||||
this.element.querySelectorAll('input[type="checkbox"]').forEach((el) => {
|
||||
el.checked = false;
|
||||
});
|
||||
}
|
||||
|
||||
selectedIdsValueChanged() {
|
||||
this._updateView()
|
||||
this._updateView();
|
||||
}
|
||||
|
||||
#addHiddenFormInputsForSelectedIds(form, paramName, transactionIds) {
|
||||
this.#resetFormInputs(form, paramName);
|
||||
_addHiddenFormInputsForSelectedIds(form, paramName, transactionIds) {
|
||||
this._resetFormInputs(form, paramName);
|
||||
|
||||
transactionIds.forEach(id => {
|
||||
transactionIds.forEach((id) => {
|
||||
const input = document.createElement("input");
|
||||
input.type = 'hidden'
|
||||
input.name = paramName
|
||||
input.value = id
|
||||
form.appendChild(input)
|
||||
})
|
||||
input.type = "hidden";
|
||||
input.name = paramName;
|
||||
input.value = id;
|
||||
form.appendChild(input);
|
||||
});
|
||||
}
|
||||
|
||||
#resetFormInputs(form, paramName) {
|
||||
_resetFormInputs(form, paramName) {
|
||||
const existingInputs = form.querySelectorAll(`input[name='${paramName}']`);
|
||||
existingInputs.forEach((input) => input.remove());
|
||||
}
|
||||
|
||||
#rowsForGroup(group) {
|
||||
return this.rowTargets.filter(row => group.contains(row))
|
||||
_rowsForGroup(group) {
|
||||
return this.rowTargets.filter((row) => group.contains(row));
|
||||
}
|
||||
|
||||
#addToSelection(idToAdd) {
|
||||
_addToSelection(idToAdd) {
|
||||
this.selectedIdsValue = Array.from(
|
||||
new Set([...this.selectedIdsValue, idToAdd])
|
||||
)
|
||||
new Set([...this.selectedIdsValue, idToAdd]),
|
||||
);
|
||||
}
|
||||
|
||||
#removeFromSelection(idToRemove) {
|
||||
this.selectedIdsValue = this.selectedIdsValue.filter(id => id !== idToRemove)
|
||||
_removeFromSelection(idToRemove) {
|
||||
this.selectedIdsValue = this.selectedIdsValue.filter(
|
||||
(id) => id !== idToRemove,
|
||||
);
|
||||
}
|
||||
|
||||
#selectAll() {
|
||||
this.selectedIdsValue = this.rowTargets.map(t => t.dataset.id)
|
||||
_selectAll() {
|
||||
this.selectedIdsValue = this.rowTargets.map((t) => t.dataset.id);
|
||||
}
|
||||
|
||||
_updateView = () => {
|
||||
this.#updateSelectionBar()
|
||||
this.#updateGroups()
|
||||
this.#updateRows()
|
||||
this._updateSelectionBar();
|
||||
this._updateGroups();
|
||||
this._updateRows();
|
||||
};
|
||||
|
||||
_updateSelectionBar() {
|
||||
const count = this.selectedIdsValue.length;
|
||||
this.selectionBarTextTarget.innerText = `${count} ${this._pluralizedResourceName()} selected`;
|
||||
this.selectionBarTarget.classList.toggle("hidden", count === 0);
|
||||
this.selectionBarTarget.querySelector("input[type='checkbox']").checked =
|
||||
count > 0;
|
||||
}
|
||||
|
||||
#updateSelectionBar() {
|
||||
const count = this.selectedIdsValue.length
|
||||
this.selectionBarTextTarget.innerText = `${count} ${this.#pluralizedResourceName()} selected`
|
||||
this.selectionBarTarget.hidden = count === 0
|
||||
this.selectionBarTarget.querySelector("input[type='checkbox']").checked = count > 0
|
||||
_pluralizedResourceName() {
|
||||
if (this.selectedIdsValue.length === 1) {
|
||||
return this.singularLabelValue;
|
||||
}
|
||||
|
||||
return this.pluralLabelValue;
|
||||
}
|
||||
|
||||
#pluralizedResourceName() {
|
||||
return `${this.resourceValue}${this.selectedIdsValue.length === 1 ? "" : "s"}`
|
||||
_updateGroups() {
|
||||
this.groupTargets.forEach((group) => {
|
||||
const rows = this.rowTargets.filter((row) => group.contains(row));
|
||||
const groupSelected =
|
||||
rows.length > 0 &&
|
||||
rows.every((row) => this.selectedIdsValue.includes(row.dataset.id));
|
||||
group.querySelector("input[type='checkbox']").checked = groupSelected;
|
||||
});
|
||||
}
|
||||
|
||||
#updateGroups() {
|
||||
this.groupTargets.forEach(group => {
|
||||
const rows = this.rowTargets.filter(row => group.contains(row))
|
||||
const groupSelected = rows.length > 0 && rows.every(row => this.selectedIdsValue.includes(row.dataset.id))
|
||||
group.querySelector("input[type='checkbox']").checked = groupSelected
|
||||
})
|
||||
}
|
||||
|
||||
#updateRows() {
|
||||
this.rowTargets.forEach(row => {
|
||||
row.checked = this.selectedIdsValue.includes(row.dataset.id)
|
||||
})
|
||||
_updateRows() {
|
||||
this.rowTargets.forEach((row) => {
|
||||
row.checked = this.selectedIdsValue.includes(row.dataset.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
import { Controller } from "@hotwired/stimulus";
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["source", "iconDefault", "iconSuccess"]
|
||||
static targets = ["source", "iconDefault", "iconSuccess"];
|
||||
|
||||
copy(event) {
|
||||
event.preventDefault();
|
||||
if (this.sourceTarget && this.sourceTarget.textContent) {
|
||||
navigator.clipboard.writeText(this.sourceTarget.textContent)
|
||||
if (this.sourceTarget?.textContent) {
|
||||
navigator.clipboard
|
||||
.writeText(this.sourceTarget.textContent)
|
||||
.then(() => {
|
||||
this.showSuccess();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to copy text: ', error);
|
||||
console.error("Failed to copy text: ", error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
showSuccess() {
|
||||
this.iconDefaultTarget.classList.add('hidden');
|
||||
this.iconSuccessTarget.classList.remove('hidden');
|
||||
this.iconDefaultTarget.classList.add("hidden");
|
||||
this.iconSuccessTarget.classList.remove("hidden");
|
||||
setTimeout(() => {
|
||||
this.iconDefaultTarget.classList.remove('hidden');
|
||||
this.iconSuccessTarget.classList.add('hidden');
|
||||
this.iconDefaultTarget.classList.remove("hidden");
|
||||
this.iconSuccessTarget.classList.add("hidden");
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,7 @@ import { Controller } from "@hotwired/stimulus";
|
||||
// Connects to data-controller="color-avatar"
|
||||
// Used by the transaction merchant form to show a preview of what the avatar will look like
|
||||
export default class extends Controller {
|
||||
static targets = [
|
||||
"name",
|
||||
"avatar"
|
||||
];
|
||||
static targets = ["name", "avatar"];
|
||||
|
||||
connect() {
|
||||
this.nameTarget.addEventListener("input", this.handleNameChange);
|
||||
@@ -17,8 +14,10 @@ export default class extends Controller {
|
||||
}
|
||||
|
||||
handleNameChange = (e) => {
|
||||
this.avatarTarget.textContent = (e.currentTarget.value?.[0] || "?").toUpperCase();
|
||||
}
|
||||
this.avatarTarget.textContent = (
|
||||
e.currentTarget.value?.[0] || "?"
|
||||
).toUpperCase();
|
||||
};
|
||||
|
||||
handleColorChange(e) {
|
||||
const color = e.currentTarget.value;
|
||||
@@ -26,4 +25,4 @@ export default class extends Controller {
|
||||
this.avatarTarget.style.borderColor = `color-mix(in srgb, ${color} 10%, white)`;
|
||||
this.avatarTarget.style.color = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,59 +1,65 @@
|
||||
import { Controller } from "@hotwired/stimulus";
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = [ "input", "decoration" ]
|
||||
static values = { selection: String }
|
||||
static targets = ["input", "decoration"];
|
||||
static values = { selection: String };
|
||||
|
||||
connect() {
|
||||
this.#renderOptions()
|
||||
this.#renderOptions();
|
||||
}
|
||||
|
||||
select({ target }) {
|
||||
this.selectionValue = target.dataset.value
|
||||
this.selectionValue = target.dataset.value;
|
||||
}
|
||||
|
||||
selectionValueChanged() {
|
||||
this.#options.forEach(option => {
|
||||
this.#options.forEach((option) => {
|
||||
if (option.dataset.value === this.selectionValue) {
|
||||
this.#check(option)
|
||||
this.inputTarget.value = this.selectionValue
|
||||
this.#check(option);
|
||||
this.inputTarget.value = this.selectionValue;
|
||||
} else {
|
||||
this.#uncheck(option)
|
||||
this.#uncheck(option);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
#renderOptions() {
|
||||
this.#options.forEach(option => option.style.backgroundColor = option.dataset.value)
|
||||
this.#options.forEach((option) => {
|
||||
option.style.backgroundColor = option.dataset.value;
|
||||
});
|
||||
}
|
||||
|
||||
#check(option) {
|
||||
option.setAttribute("aria-checked", "true")
|
||||
option.style.boxShadow = `0px 0px 0px 4px ${hexToRGBA(option.dataset.value, 0.2)}`
|
||||
this.decorationTarget.style.backgroundColor = option.dataset.value
|
||||
option.setAttribute("aria-checked", "true");
|
||||
option.style.boxShadow = `0px 0px 0px 4px ${hexToRGBA(
|
||||
option.dataset.value,
|
||||
0.2,
|
||||
)}`;
|
||||
this.decorationTarget.style.backgroundColor = option.dataset.value;
|
||||
}
|
||||
|
||||
#uncheck(option) {
|
||||
option.setAttribute("aria-checked", "false")
|
||||
option.style.boxShadow = "none"
|
||||
option.setAttribute("aria-checked", "false");
|
||||
option.style.boxShadow = "none";
|
||||
}
|
||||
|
||||
get #options() {
|
||||
return Array.from(this.element.querySelectorAll("[role='radio']"))
|
||||
return Array.from(this.element.querySelectorAll("[role='radio']"));
|
||||
}
|
||||
}
|
||||
|
||||
function hexToRGBA(hex, alpha = 1) {
|
||||
hex = hex.replace(/^#/, '');
|
||||
let hexCode = hex.replace(/^#/, "");
|
||||
let calculatedAlpha = alpha;
|
||||
|
||||
if (hex.length === 8) {
|
||||
alpha = parseInt(hex.slice(6, 8), 16) / 255;
|
||||
hex = hex.slice(0, 6);
|
||||
if (hexCode.length === 8) {
|
||||
calculatedAlpha = Number.parseInt(hexCode.slice(6, 8), 16) / 255;
|
||||
hexCode = hexCode.slice(0, 6);
|
||||
}
|
||||
|
||||
let r = parseInt(hex.slice(0, 2), 16);
|
||||
let g = parseInt(hex.slice(2, 4), 16);
|
||||
let b = parseInt(hex.slice(4, 6), 16);
|
||||
const r = Number.parseInt(hexCode.slice(0, 2), 16);
|
||||
const g = Number.parseInt(hexCode.slice(2, 4), 16);
|
||||
const b = Number.parseInt(hexCode.slice(4, 6), 16);
|
||||
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
return `rgba(${r}, ${g}, ${b}, ${calculatedAlpha})`;
|
||||
}
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import { Controller } from "@hotwired/stimulus";
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["replacementField", "submitButton"]
|
||||
static classes = [ "dangerousAction", "safeAction" ]
|
||||
static targets = ["replacementField", "submitButton"];
|
||||
static classes = ["dangerousAction", "safeAction"];
|
||||
static values = {
|
||||
submitTextWhenReplacing: String,
|
||||
submitTextWhenNotReplacing: String
|
||||
}
|
||||
submitTextWhenNotReplacing: String,
|
||||
};
|
||||
|
||||
updateSubmitButton() {
|
||||
if (this.replacementFieldTarget.value) {
|
||||
this.submitButtonTarget.value = this.submitTextWhenReplacingValue
|
||||
this.#markSafe()
|
||||
this.submitButtonTarget.value = this.submitTextWhenReplacingValue;
|
||||
this.#markSafe();
|
||||
} else {
|
||||
this.submitButtonTarget.value = this.submitTextWhenNotReplacingValue
|
||||
this.#markDangerous()
|
||||
this.submitButtonTarget.value = this.submitTextWhenNotReplacingValue;
|
||||
this.#markDangerous();
|
||||
}
|
||||
}
|
||||
|
||||
#markSafe() {
|
||||
this.submitButtonTarget.classList.remove(...this.dangerousActionClasses)
|
||||
this.submitButtonTarget.classList.add(...this.safeActionClasses)
|
||||
this.submitButtonTarget.classList.remove(...this.dangerousActionClasses);
|
||||
this.submitButtonTarget.classList.add(...this.safeActionClasses);
|
||||
}
|
||||
|
||||
#markDangerous() {
|
||||
this.submitButtonTarget.classList.remove(...this.safeActionClasses)
|
||||
this.submitButtonTarget.classList.add(...this.dangerousActionClasses)
|
||||
this.submitButtonTarget.classList.remove(...this.safeActionClasses);
|
||||
this.submitButtonTarget.classList.add(...this.dangerousActionClasses);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Controller } from '@hotwired/stimulus'
|
||||
import { Controller } from "@hotwired/stimulus";
|
||||
|
||||
// Connects to data-controller="element-removal"
|
||||
export default class extends Controller {
|
||||
remove() {
|
||||
this.element.remove()
|
||||
this.element.remove();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user