Compare commits
314 Commits
1077-bug-e
...
v0.2.0
| 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 | ||
|
|
d9f11e002a | ||
|
|
c744237b55 | ||
|
|
7dfd7408c7 | ||
|
|
8f8988c03a | ||
|
|
a21061fb56 | ||
|
|
c5bf1db230 | ||
|
|
3610c6cae7 | ||
|
|
79ca7e2039 | ||
|
|
34ebd96c4c | ||
|
|
3399b74849 | ||
|
|
77fc5caecf | ||
|
|
a20809eee3 | ||
|
|
cd9f20747c | ||
|
|
1746533842 | ||
|
|
6b46831199 | ||
|
|
aa16807c6c | ||
|
|
dce9adb534 | ||
|
|
26bd655e4c | ||
|
|
5c7d2f2b01 | ||
|
|
90278630ed | ||
|
|
977da34efc | ||
|
|
6288139a41 | ||
|
|
ff5408c131 | ||
|
|
0a303ccbd5 | ||
|
|
a2ab217925 | ||
|
|
b4d0fdbe0d | ||
|
|
4bfe47540d | ||
|
|
f5cb13b42f | ||
|
|
3893060f8e | ||
|
|
54596d51f7 | ||
|
|
7758f51be9 | ||
|
|
40c09279f3 | ||
|
|
ad52207a25 | ||
|
|
a33ba11ce9 | ||
|
|
47a43a888c | ||
|
|
0afab5296c | ||
|
|
0d7164af9b | ||
|
|
597079dc8d | ||
|
|
fc91a34691 | ||
|
|
fd941d714d | ||
|
|
9263dd3bbe | ||
|
|
31f3ff6a16 | ||
|
|
41dff228e8 | ||
|
|
78b0674052 | ||
|
|
3461182725 | ||
|
|
59e4eff24a | ||
|
|
e70d3d1902 | ||
|
|
2f6479f058 | ||
|
|
ffd54e4065 | ||
|
|
591d149da9 | ||
|
|
c397f1bd2b | ||
|
|
d2a6ab1e45 | ||
|
|
5e3a3b0b38 | ||
|
|
563db0f8eb | ||
|
|
2dda598e8a | ||
|
|
388f8e4197 | ||
|
|
1d56c67b4f | ||
|
|
f6619aa4e5 | ||
|
|
9453313f68 | ||
|
|
9ebcb6fc41 | ||
|
|
24d3c0243f | ||
|
|
e8d7ee3270 | ||
|
|
73d61fc990 | ||
|
|
1ffa13f3b3 | ||
|
|
82c298307d | ||
|
|
ab40289eb4 | ||
|
|
7fabca4679 | ||
|
|
cb75c537fe | ||
|
|
b1d2dc5e97 | ||
|
|
c3c0ab3530 | ||
|
|
fa3b1e016c | ||
|
|
398b246965 | ||
|
|
23786b444a | ||
|
|
edbf4eb3d6 | ||
|
|
367073f046 | ||
|
|
2cb3d806d8 | ||
|
|
3dd0aa2f37 | ||
|
|
2b9a7fdef3 | ||
|
|
cb14ef7655 | ||
|
|
73ceebccc2 | ||
|
|
17f29de773 | ||
|
|
60fadc1d68 | ||
|
|
5eaf335c49 | ||
|
|
be8f74b093 | ||
|
|
b4b4e5df31 | ||
|
|
5942ce7e3c | ||
|
|
730e58d763 | ||
|
|
e06f0c76f9 | ||
|
|
aa6d755402 | ||
|
|
fd40111264 | ||
|
|
fc0bc1ac96 | ||
|
|
b7e3c61d09 | ||
|
|
8181781570 | ||
|
|
5a5e27685a | ||
|
|
cc1954b33b | ||
|
|
9bb9a062ac | ||
|
|
52d3528361 | ||
|
|
d3d9af8bce | ||
|
|
0149ca4ea1 | ||
|
|
30f7c120e1 | ||
|
|
949d3d80fa | ||
|
|
c28dd8f940 | ||
|
|
277e4476d9 | ||
|
|
b9341ac302 | ||
|
|
86741401c3 | ||
|
|
edf44bec03 | ||
|
|
5178928b68 | ||
|
|
ac0ff35360 | ||
|
|
04037b8943 | ||
|
|
cb13fd2245 | ||
|
|
eebc07d75e | ||
|
|
c30c1b9698 | ||
|
|
d3971f9cee | ||
|
|
9e4b931612 | ||
|
|
b44da70836 | ||
|
|
0db75a019b | ||
|
|
ee572d8d1f | ||
|
|
33d007a07b | ||
|
|
3673ab8f03 | ||
|
|
fb42c2ad43 | ||
|
|
9172eb931b | ||
|
|
0c8cf7e217 | ||
|
|
0bbf7f82b7 | ||
|
|
c05ee9b572 | ||
|
|
38c2b4670c | ||
|
|
f82ce59dad | ||
|
|
166ed4b1ea | ||
|
|
0c0db44b7f | ||
|
|
cd254fd19b | ||
|
|
0d20be4905 | ||
|
|
cf861ccff9 | ||
|
|
525439e44d | ||
|
|
e1efe97e6f | ||
|
|
52c729dc33 | ||
|
|
de9723d63a | ||
|
|
eef4c2643b | ||
|
|
359bceb58e | ||
|
|
e856691c86 | ||
|
|
4433488562 | ||
|
|
37ae51f68a | ||
|
|
793a6027a3 | ||
|
|
4d20b5f2d4 | ||
|
|
7966c44d7f | ||
|
|
30b2ff7aa6 | ||
|
|
f85fdba366 | ||
|
|
0cb4e968a0 | ||
|
|
8ebf18e04d | ||
|
|
0c1ff00c1e | ||
|
|
e6528bafec | ||
|
|
1b6ce6af45 | ||
|
|
4527482aa2 | ||
|
|
707c5ca0ca | ||
|
|
c70a08aca2 | ||
|
|
9dda2606d5 | ||
|
|
acf3564a86 | ||
|
|
1f6f55c4a8 |
64
.ai/cursorrules.md
Normal file
@@ -0,0 +1,64 @@
|
||||
<!-- Copy this file to .cursorrules in the root of the project on your local machine if you'd like to use these rules with Cursor. -->
|
||||
|
||||
You are an expert in Ruby, Ruby on Rails, Postgres, Tailwind, Stimulus, Hotwire and Turbo and always use the latest stable versions of those technologies.
|
||||
|
||||
**Code Style and Structure**
|
||||
- Write concise, technical Ruby code with accurate examples.
|
||||
- Prefer iteration and modularization over code duplication.
|
||||
- Use descriptive variable names with auxiliary verbs (e.g., is_loading, has_error).
|
||||
- Structure files: models, controllers, views, helpers, services, jobs, mailers.
|
||||
|
||||
**Naming Conventions**
|
||||
- Use snake_case for file names and directories (e.g., app/models/user_profile.rb).
|
||||
- Use CamelCase for classes and modules (e.g., UserProfile).
|
||||
|
||||
**Ruby on Rails Usage**
|
||||
- Use Rails conventions for MVC structure.
|
||||
- Favor scopes over class methods for queries.
|
||||
- Use strong parameters for mass assignment protection.
|
||||
- Use partials to DRY up views.
|
||||
|
||||
**Syntax and Formatting**
|
||||
- Use two spaces for indentation.
|
||||
- Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements.
|
||||
- Use descriptive method names and keep methods short.
|
||||
|
||||
**Commenting Code**
|
||||
- Write clear, concise comments to explain the purpose of individual functions and methods.
|
||||
- Use comments to describe the intent and functionality of complex logic.
|
||||
- Avoid redundant comments that state the obvious.
|
||||
|
||||
**UI and Styling**
|
||||
- Use Tailwind CSS for styling.
|
||||
- Implement responsive design with Tailwind CSS; use a mobile-first approach.
|
||||
- Use Stimulus for JavaScript behavior.
|
||||
- Use Turbo for asynchronous actions and updates.
|
||||
|
||||
**Performance Optimization**
|
||||
- Use eager loading to avoid N+1 queries.
|
||||
- Cache expensive queries and partials where appropriate.
|
||||
- Use background jobs for long-running tasks.
|
||||
- Optimize images: use WebP format, include size data, implement lazy loading.
|
||||
|
||||
**Database Querying & Data Model Creation**
|
||||
- Use ActiveRecord for data querying and model creation.
|
||||
- Favor database constraints and indexes for data integrity and performance.
|
||||
- Use migrations to manage schema changes.
|
||||
|
||||
**Key Conventions**
|
||||
- Follow Rails best practices for RESTful routing.
|
||||
- Optimize for performance and security.
|
||||
- Use environment variables for configuration.
|
||||
- Write tests for models, controllers, and features.
|
||||
|
||||
**AI Guidelines**
|
||||
- Follow the user’s requirements carefully & to the letter.
|
||||
- Confirm, then write code!
|
||||
- Suggest solutions that I didn't think about—anticipate my needs
|
||||
- Focus on readability over being performant.
|
||||
- Fully implement all requested functionality.
|
||||
- Leave NO todo’s, placeholders or missing pieces.
|
||||
- Don't say things like "additional logic can be added here" — instead, add the logic.
|
||||
- Be concise. Minimize any other prose.
|
||||
- Consider new technologies and contrarian ideas, not just the conventional wisdom
|
||||
- If I ask for adjustments to code, do not repeat all of my code unnecessarily. Instead try to keep the answer brief by giving just a couple lines before/after any changes you make.
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
7
.editorconfig
Normal file
@@ -0,0 +1,7 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
45
.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=
|
||||
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
|
||||
@@ -15,7 +24,7 @@ SMTP_USERNAME=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_TLS_ENABLED=true
|
||||
|
||||
# Email Configuration
|
||||
# Address that emails are sent from
|
||||
EMAIL_SENDER=
|
||||
|
||||
# Database Configuration
|
||||
@@ -36,8 +45,8 @@ SENTRY_DSN=
|
||||
# This is useful for controlling who can sign up for your Maybe instance.
|
||||
REQUIRE_INVITE_CODE=false
|
||||
|
||||
# Enables self hosting features
|
||||
SELF_HOSTING_ENABLED=false
|
||||
# Enables self hosting features (should be set to true for most folks)
|
||||
SELF_HOSTED=true
|
||||
|
||||
# The hosting platform used to deploy the app (e.g. "render")
|
||||
# `localhost` (or unset) is used for local development and testing
|
||||
@@ -86,3 +95,27 @@ GITHUB_REPO_BRANCH=main
|
||||
# S3_SECRET_ACCESS_KEY=
|
||||
# S3_REGION= # defaults to `us-east-1` if not set
|
||||
# S3_BUCKET=
|
||||
#
|
||||
# Cloudflare R2
|
||||
# =============
|
||||
# ACTIVE_STORAGE_SERVICE=cloudflare
|
||||
# CLOUDFLARE_ACCOUNT_ID=
|
||||
# CLOUDFLARE_ACCESS_KEY_ID=
|
||||
# CLOUDFLARE_SECRET_ACCESS_KEY=
|
||||
# CLOUDFLARE_BUCKET=
|
||||
|
||||
# ======================================================================================================
|
||||
# Billing Module - responsible for handling billing
|
||||
# ======================================================================================================
|
||||
#
|
||||
STRIPE_PUBLISHABLE_KEY=
|
||||
STRIPE_SECRET_KEY=
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
|
||||
# ======================================================================================================
|
||||
# Plaid Configuration
|
||||
# ======================================================================================================
|
||||
#
|
||||
PLAID_CLIENT_ID=
|
||||
PLAID_SECRET=
|
||||
PLAID_ENV=
|
||||
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
@@ -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
@@ -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.
|
||||
|
||||
|
||||
21
.github/workflows/ci.yml
vendored
@@ -52,8 +52,25 @@ jobs:
|
||||
- name: Lint code for consistent style
|
||||
run: bin/rubocop -f github
|
||||
|
||||
- name: Lint templates for consistent style
|
||||
run: ./bin/erblint ./app/**/*.erb
|
||||
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
|
||||
|
||||
13
.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,3 +62,7 @@ compose-dev.yaml
|
||||
gcp-storage-keyfile.json
|
||||
|
||||
coverage
|
||||
.cursorrules
|
||||
|
||||
# Ignore node related files
|
||||
node_modules
|
||||
25
.rubocop.yml
@@ -1,12 +1,15 @@
|
||||
# Omakase Ruby styling for Rails
|
||||
inherit_gem: { rubocop-rails-omakase: rubocop.yml }
|
||||
inherit_gem:
|
||||
rubocop-rails-omakase: rubocop.yml
|
||||
|
||||
Layout/IndentationWidth:
|
||||
Enabled: true
|
||||
|
||||
# Overwrite or add rules to create your own house style
|
||||
#
|
||||
# # Use `[a, [b, c]]` not `[ a, [ b, c ] ]`
|
||||
# Layout/SpaceInsideArrayLiteralBrackets:
|
||||
# Enabled: false
|
||||
Layout/ElseAlignment:
|
||||
Enabled: false
|
||||
Layout/EndAlignment:
|
||||
Enabled: false
|
||||
Layout/IndentationStyle:
|
||||
EnforcedStyle: spaces
|
||||
IndentationWidth: 2
|
||||
|
||||
Layout/IndentationConsistency:
|
||||
Enabled: true
|
||||
|
||||
Layout/SpaceInsidePercentLiteralDelimiters:
|
||||
Enabled: true
|
||||
@@ -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.1
|
||||
ARG RUBY_VERSION=3.3.5
|
||||
FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim as base
|
||||
|
||||
# Rails app lives here
|
||||
|
||||
12
Gemfile
@@ -3,7 +3,7 @@ source "https://rubygems.org"
|
||||
ruby file: ".ruby-version"
|
||||
|
||||
# Rails
|
||||
gem "rails", github: "rails/rails", branch: "7-2-stable"
|
||||
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,18 +37,23 @@ gem "image_processing", ">= 1.2"
|
||||
|
||||
# Other
|
||||
gem "bcrypt", "~> 3.1"
|
||||
gem "jwt"
|
||||
gem "faraday"
|
||||
gem "faraday-retry"
|
||||
gem "faraday-multipart"
|
||||
gem "inline_svg"
|
||||
gem "octokit"
|
||||
gem "pagy"
|
||||
gem "rails-settings-cached"
|
||||
gem "tzinfo-data", platforms: %i[ windows jruby ]
|
||||
gem "tzinfo-data", platforms: %i[windows jruby]
|
||||
gem "csv"
|
||||
gem "redcarpet"
|
||||
gem "stripe"
|
||||
gem "intercom-rails"
|
||||
gem "plaid"
|
||||
|
||||
group :development, :test do
|
||||
gem "debug", platforms: %i[ mri windows ]
|
||||
gem "debug", platforms: %i[mri windows]
|
||||
gem "brakeman", require: false
|
||||
gem "rubocop-rails-omakase", require: false
|
||||
gem "i18n-tasks"
|
||||
|
||||
365
Gemfile.lock
@@ -1,38 +1,36 @@
|
||||
GIT
|
||||
remote: https://github.com/maybe-finance/lucide-rails.git
|
||||
revision: 79d989593ee4ac6c50106ec5e4d2bd4ec8f5af87
|
||||
revision: 272e5fb8418ea458da3995d6abe0ba0ceee9c9f0
|
||||
specs:
|
||||
lucide-rails (0.2.0)
|
||||
railties (>= 4.1.0)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/rails/rails.git
|
||||
revision: f6d62b5f214471f6af0fc0535fe70e4e13b19ed4
|
||||
branch: 7-2-stable
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (7.2.0)
|
||||
actionpack (= 7.2.0)
|
||||
activesupport (= 7.2.0)
|
||||
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.0)
|
||||
actionpack (= 7.2.0)
|
||||
activejob (= 7.2.0)
|
||||
activerecord (= 7.2.0)
|
||||
activestorage (= 7.2.0)
|
||||
activesupport (= 7.2.0)
|
||||
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.0)
|
||||
actionpack (= 7.2.0)
|
||||
actionview (= 7.2.0)
|
||||
activejob (= 7.2.0)
|
||||
activesupport (= 7.2.0)
|
||||
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.0)
|
||||
actionview (= 7.2.0)
|
||||
activesupport (= 7.2.0)
|
||||
actionpack (7.2.2)
|
||||
actionview (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
nokogiri (>= 1.8.5)
|
||||
racc
|
||||
rack (>= 2.2.4, < 3.2)
|
||||
@@ -41,36 +39,37 @@ GIT
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
useragent (~> 0.16)
|
||||
actiontext (7.2.0)
|
||||
actionpack (= 7.2.0)
|
||||
activerecord (= 7.2.0)
|
||||
activestorage (= 7.2.0)
|
||||
activesupport (= 7.2.0)
|
||||
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.0)
|
||||
activesupport (= 7.2.0)
|
||||
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.0)
|
||||
activesupport (= 7.2.0)
|
||||
activejob (7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.2.0)
|
||||
activesupport (= 7.2.0)
|
||||
activerecord (7.2.0)
|
||||
activemodel (= 7.2.0)
|
||||
activesupport (= 7.2.0)
|
||||
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.0)
|
||||
actionpack (= 7.2.0)
|
||||
activejob (= 7.2.0)
|
||||
activerecord (= 7.2.0)
|
||||
activesupport (= 7.2.0)
|
||||
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.0)
|
||||
activesupport (7.2.2)
|
||||
base64
|
||||
benchmark (>= 0.3)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||
connection_pool (>= 2.2.5)
|
||||
@@ -80,53 +79,28 @@ GIT
|
||||
minitest (>= 5.1)
|
||||
securerandom (>= 0.3)
|
||||
tzinfo (~> 2.0, >= 2.0.5)
|
||||
rails (7.2.0)
|
||||
actioncable (= 7.2.0)
|
||||
actionmailbox (= 7.2.0)
|
||||
actionmailer (= 7.2.0)
|
||||
actionpack (= 7.2.0)
|
||||
actiontext (= 7.2.0)
|
||||
actionview (= 7.2.0)
|
||||
activejob (= 7.2.0)
|
||||
activemodel (= 7.2.0)
|
||||
activerecord (= 7.2.0)
|
||||
activestorage (= 7.2.0)
|
||||
activesupport (= 7.2.0)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 7.2.0)
|
||||
railties (7.2.0)
|
||||
actionpack (= 7.2.0)
|
||||
activesupport (= 7.2.0)
|
||||
irb (~> 1.13)
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
zeitwerk (~> 2.6)
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
addressable (2.8.6)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
ast (2.4.2)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.961.0)
|
||||
aws-sdk-core (3.201.3)
|
||||
aws-partitions (1.1018.0)
|
||||
aws-sdk-core (3.214.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.8)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.88.0)
|
||||
aws-sdk-core (~> 3, >= 3.201.0)
|
||||
aws-sdk-kms (1.96.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.157.0)
|
||||
aws-sdk-core (~> 3, >= 3.201.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.9.1)
|
||||
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)
|
||||
@@ -138,7 +112,7 @@ GEM
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.18.4)
|
||||
msgpack (~> 1.2)
|
||||
brakeman (6.1.2)
|
||||
brakeman (6.2.2)
|
||||
racc
|
||||
builder (3.3.0)
|
||||
capybara (3.40.0)
|
||||
@@ -159,17 +133,17 @@ 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)
|
||||
docile (1.4.0)
|
||||
dotenv (3.1.2)
|
||||
dotenv-rails (3.1.2)
|
||||
dotenv (= 3.1.2)
|
||||
dotenv (3.1.4)
|
||||
dotenv-rails (3.1.4)
|
||||
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)
|
||||
@@ -179,13 +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.10.1)
|
||||
faraday-net_http (>= 2.0, < 3.2)
|
||||
faraday (2.12.1)
|
||||
faraday-net_http (>= 2.0, < 3.5)
|
||||
json
|
||||
logger
|
||||
faraday-net_http (3.1.1)
|
||||
net-http
|
||||
faraday-multipart (1.0.4)
|
||||
multipart-post (~> 2)
|
||||
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)
|
||||
@@ -194,25 +171,29 @@ GEM
|
||||
ffi (1.17.0-x86-linux-gnu)
|
||||
ffi (1.17.0-x86_64-darwin)
|
||||
ffi (1.17.0-x86_64-linux-gnu)
|
||||
fugit (1.11.0)
|
||||
fugit (1.11.1)
|
||||
et-orbi (~> 1, >= 1.2.11)
|
||||
raabro (~> 1.4)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
good_job (4.1.1)
|
||||
good_job (4.5.1)
|
||||
activejob (>= 6.1.0)
|
||||
activerecord (>= 6.1.0)
|
||||
concurrent-ruby (>= 1.3.1)
|
||||
fugit (>= 1.11.0)
|
||||
railties (>= 6.1.0)
|
||||
thor (>= 1.0.0)
|
||||
hashdiff (1.1.0)
|
||||
hashdiff (1.1.1)
|
||||
highline (3.0.1)
|
||||
hotwire-livereload (1.4.0)
|
||||
hotwire-livereload (1.4.1)
|
||||
actioncable (>= 6.0.0)
|
||||
listen (>= 3.0.0)
|
||||
railties (>= 6.0.0)
|
||||
i18n (1.14.5)
|
||||
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)
|
||||
activesupport (>= 4.0.2)
|
||||
@@ -227,19 +208,23 @@ GEM
|
||||
image_processing (1.13.0)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
importmap-rails (2.0.1)
|
||||
importmap-rails (2.0.3)
|
||||
actionpack (>= 6.0.0)
|
||||
activesupport (>= 6.0.0)
|
||||
railties (>= 6.0.0)
|
||||
inline_svg (1.9.0)
|
||||
inline_svg (1.10.0)
|
||||
activesupport (>= 3.0)
|
||||
nokogiri (>= 1.6)
|
||||
io-console (0.7.2)
|
||||
irb (1.14.0)
|
||||
intercom-rails (1.0.1)
|
||||
activesupport (> 4.0)
|
||||
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)
|
||||
@@ -249,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.0)
|
||||
loofah (2.22.0)
|
||||
logger (1.6.2)
|
||||
loofah (2.23.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
mail (2.8.1)
|
||||
@@ -262,13 +247,14 @@ GEM
|
||||
matrix (0.4.2)
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
minitest (5.24.1)
|
||||
mocha (2.4.5)
|
||||
minitest (5.25.4)
|
||||
mocha (2.7.0)
|
||||
ruby2_keywords (>= 0.0.5)
|
||||
msgpack (1.7.2)
|
||||
net-http (0.4.1)
|
||||
multipart-post (2.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)
|
||||
@@ -277,89 +263,112 @@ 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.0.5)
|
||||
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.7)
|
||||
prism (0.30.0)
|
||||
propshaft (0.9.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 (5.1.0)
|
||||
puma (6.4.2)
|
||||
public_suffix (6.0.1)
|
||||
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.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.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.2)
|
||||
actionpack (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
irb (~> 1.13)
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.2.1)
|
||||
rb-fsevent (0.11.2)
|
||||
rb-inotify (0.10.1)
|
||||
rb-inotify (0.11.1)
|
||||
ffi (~> 1.0)
|
||||
rbs (3.5.2)
|
||||
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.9)
|
||||
reline (0.5.12)
|
||||
io-console (~> 0.5)
|
||||
rexml (3.3.4)
|
||||
strscan
|
||||
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.17.12)
|
||||
ruby-lsp (0.22.1)
|
||||
language_server-protocol (~> 3.17.0)
|
||||
prism (>= 0.29.0, < 0.31)
|
||||
prism (>= 1.2, < 2.0)
|
||||
rbs (>= 3, < 4)
|
||||
sorbet-runtime (>= 0.5.10782)
|
||||
ruby-lsp-rails (0.3.12)
|
||||
ruby-lsp (>= 0.17.12, < 0.18.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.23.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.18.2)
|
||||
sentry-rails (5.22.0)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby (~> 5.18.2)
|
||||
sentry-ruby (5.18.2)
|
||||
sentry-ruby (~> 5.22.0)
|
||||
sentry-ruby (5.22.0)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
simplecov (0.22.0)
|
||||
@@ -413,55 +422,51 @@ GEM
|
||||
simplecov-html (0.12.3)
|
||||
simplecov_json_formatter (0.1.4)
|
||||
smart_properties (1.17.0)
|
||||
sorbet-runtime (0.5.11518)
|
||||
sorbet-runtime (0.5.11663)
|
||||
stackprof (0.2.26)
|
||||
stimulus-rails (1.3.3)
|
||||
stimulus-rails (1.3.4)
|
||||
railties (>= 6.0.0)
|
||||
stringio (3.1.1)
|
||||
strscan (3.1.0)
|
||||
tailwindcss-rails (2.7.2)
|
||||
railties (>= 7.0.0)
|
||||
tailwindcss-rails (2.7.2-aarch64-linux)
|
||||
railties (>= 7.0.0)
|
||||
tailwindcss-rails (2.7.2-arm-linux)
|
||||
railties (>= 7.0.0)
|
||||
tailwindcss-rails (2.7.2-arm64-darwin)
|
||||
railties (>= 7.0.0)
|
||||
tailwindcss-rails (2.7.2-x86_64-darwin)
|
||||
railties (>= 7.0.0)
|
||||
tailwindcss-rails (2.7.2-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.1)
|
||||
timeout (0.4.1)
|
||||
turbo-rails (2.0.6)
|
||||
thor (1.3.2)
|
||||
timeout (0.4.2)
|
||||
turbo-rails (2.0.11)
|
||||
actionpack (>= 6.0.0)
|
||||
activejob (>= 6.0.0)
|
||||
railties (>= 6.0.0)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
unicode-display_width (2.5.0)
|
||||
uri (0.13.0)
|
||||
useragent (0.16.10)
|
||||
vcr (6.2.0)
|
||||
unicode-display_width (2.6.0)
|
||||
uri (1.0.2)
|
||||
useragent (0.16.11)
|
||||
vcr (6.3.1)
|
||||
base64
|
||||
web-console (4.2.1)
|
||||
actionview (>= 6.0.0)
|
||||
activemodel (>= 6.0.0)
|
||||
bindex (>= 0.4.0)
|
||||
railties (>= 6.0.0)
|
||||
webmock (3.23.1)
|
||||
webmock (3.24.0)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webrick (1.8.1)
|
||||
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.17)
|
||||
zeitwerk (2.7.1)
|
||||
|
||||
PLATFORMS
|
||||
aarch64-linux
|
||||
@@ -484,22 +489,27 @@ DEPENDENCIES
|
||||
erb_lint
|
||||
faker
|
||||
faraday
|
||||
faraday-multipart
|
||||
faraday-retry
|
||||
good_job
|
||||
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!
|
||||
rails (~> 7.2.2)
|
||||
rails-settings-cached
|
||||
redcarpet
|
||||
rubocop-rails-omakase
|
||||
@@ -510,6 +520,7 @@ DEPENDENCIES
|
||||
simplecov
|
||||
stackprof
|
||||
stimulus-rails
|
||||
stripe
|
||||
tailwindcss-rails
|
||||
turbo-rails
|
||||
tzinfo-data
|
||||
@@ -518,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: 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/bg-grid.png
Normal file
|
After Width: | Height: | Size: 326 KiB |
1
app/assets/images/discord-icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"><path fill="#5865f2" d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"/></svg>
|
||||
|
After Width: | Height: | Size: 764 B |
1
app/assets/images/github-icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#24292f"/></svg>
|
||||
|
After Width: | Height: | Size: 963 B |
BIN
app/assets/images/logo-color.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
160
app/assets/images/logo-squircle.svg
Normal file
@@ -0,0 +1,160 @@
|
||||
<svg width="70" height="70" viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_iii_4725_68011)">
|
||||
<path d="M1.66199 28.3573C3.33915 4.37286 8.83917 -0.408237 32.8236 1.26892L41.868 1.90136C65.8524 3.57851 70.6335 9.07854 68.9563 33.063L68.3239 42.1073C66.6467 66.0917 61.1467 70.8728 37.1623 69.1957L28.1179 68.5632C4.13349 66.8861 -0.647606 61.3861 1.02955 37.4016L1.66199 28.3573Z" fill="url(#paint0_linear_4725_68011)"/>
|
||||
<path d="M1.66199 28.3573C3.33915 4.37286 8.83917 -0.408237 32.8236 1.26892L41.868 1.90136C65.8524 3.57851 70.6335 9.07854 68.9563 33.063L68.3239 42.1073C66.6467 66.0917 61.1467 70.8728 37.1623 69.1957L28.1179 68.5632C4.13349 66.8861 -0.647606 61.3861 1.02955 37.4016L1.66199 28.3573Z" fill="black" fill-opacity="0.7"/>
|
||||
</g>
|
||||
<path d="M2.82179 28.4384C3.23922 22.4687 3.89051 17.7733 4.98031 14.1012C6.06625 10.4421 7.56711 7.86966 9.64032 6.06745C11.7135 4.26524 14.4698 3.13701 18.2445 2.57088C22.0324 2.00274 26.7729 2.01127 32.7425 2.42871L41.7868 3.06115C47.7565 3.47859 52.452 4.12988 56.124 5.21968C59.7831 6.30562 62.3556 7.80648 64.1578 9.87969C65.96 11.9529 67.0882 14.7092 67.6544 18.4838C68.2225 22.2718 68.214 27.0122 67.7965 32.9819L67.1641 42.0262C66.7466 47.9959 66.0953 52.6913 65.0056 56.3634C63.9196 60.0225 62.4188 62.5949 60.3455 64.3971C58.2723 66.1994 55.516 67.3276 51.7414 67.8937C47.9534 68.4619 43.213 68.4533 37.2434 68.0359L28.199 67.4034C22.2294 66.986 17.5339 66.3347 13.8619 65.2449C10.2028 64.159 7.6303 62.6581 5.82808 60.5849C4.02587 58.5117 2.89764 55.7554 2.33151 51.9808C1.76337 48.1928 1.77191 43.4524 2.18934 37.4827L2.82179 28.4384Z" stroke="white" stroke-width="2.32525"/>
|
||||
<path d="M2.82179 28.4384C3.23922 22.4687 3.89051 17.7733 4.98031 14.1012C6.06625 10.4421 7.56711 7.86966 9.64032 6.06745C11.7135 4.26524 14.4698 3.13701 18.2445 2.57088C22.0324 2.00274 26.7729 2.01127 32.7425 2.42871L41.7868 3.06115C47.7565 3.47859 52.452 4.12988 56.124 5.21968C59.7831 6.30562 62.3556 7.80648 64.1578 9.87969C65.96 11.9529 67.0882 14.7092 67.6544 18.4838C68.2225 22.2718 68.214 27.0122 67.7965 32.9819L67.1641 42.0262C66.7466 47.9959 66.0953 52.6913 65.0056 56.3634C63.9196 60.0225 62.4188 62.5949 60.3455 64.3971C58.2723 66.1994 55.516 67.3276 51.7414 67.8937C47.9534 68.4619 43.213 68.4533 37.2434 68.0359L28.199 67.4034C22.2294 66.986 17.5339 66.3347 13.8619 65.2449C10.2028 64.159 7.6303 62.6581 5.82808 60.5849C4.02587 58.5117 2.89764 55.7554 2.33151 51.9808C1.76337 48.1928 1.77191 43.4524 2.18934 37.4827L2.82179 28.4384Z" stroke="url(#paint1_linear_4725_68011)" stroke-width="2.32525"/>
|
||||
<path d="M3.66933 28.4976C4.08541 22.5474 4.73164 17.9253 5.79481 14.343C6.85131 10.7831 8.28392 8.3723 10.1977 6.70866C12.1115 5.04503 14.6982 3.96188 18.3705 3.4111C22.0659 2.85684 26.7329 2.86017 32.6832 3.27625L41.7276 3.9087C47.6779 4.32478 52.2999 4.97101 55.8823 6.03418C59.4422 7.09068 61.8529 8.52329 63.5166 10.4371C65.1802 12.3509 66.2634 14.9376 66.8141 18.6098C67.3684 22.3053 67.3651 26.9723 66.949 32.9226L66.3165 41.967C65.9005 47.9172 65.2542 52.5393 64.1911 56.1216C63.1345 59.6815 61.7019 62.0923 59.7882 63.7559C57.8744 65.4196 55.2877 66.5027 51.6154 67.0535C47.92 67.6078 43.2529 67.6044 37.3026 67.1883L28.2583 66.5559C22.308 66.1398 17.6859 65.4936 14.1036 64.4304C10.5437 63.3739 8.13293 61.9413 6.4693 60.0275C4.80566 58.1137 3.72251 55.527 3.17173 51.8548C2.61747 48.1593 2.6208 43.4923 3.03688 37.542L3.66933 28.4976Z" stroke="url(#paint2_linear_4725_68011)" stroke-opacity="0.05" stroke-width="4.02448"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.20106 14.4635C5.15118 18.0011 4.50747 22.5866 4.09206 28.5272L3.45962 37.5716C3.04421 43.5122 3.04348 48.1426 3.59081 51.7919C4.13394 55.4131 5.1946 57.9152 6.78912 59.7495C8.38363 61.5838 10.7138 62.9824 14.2242 64.0242C17.7617 65.0741 22.3472 65.7178 28.2878 66.1332L37.3322 66.7656C43.2728 67.181 47.9033 67.1818 51.5525 66.6344C55.1738 66.0913 57.6759 65.0306 59.5101 63.4361C61.3444 61.8416 62.743 59.5115 63.7848 56.0011C64.8347 52.4635 65.4784 47.878 65.8938 41.9374L66.5262 32.893C66.9417 26.9524 66.9424 22.322 66.3951 18.6727C65.8519 15.0515 64.7913 12.5494 63.1968 10.7151C61.6022 8.88082 59.2721 7.48225 55.7617 6.44043C52.2241 5.39055 47.6387 4.74684 41.698 4.33143L32.6537 3.69899C26.713 3.28358 22.0826 3.28284 18.4333 3.83018C14.8121 4.3733 12.31 5.43397 10.4757 7.02849C8.64145 8.623 7.24288 10.9531 6.20106 14.4635ZM32.8236 1.26892C8.83917 -0.408237 3.33915 4.37286 1.66199 28.3573L1.02955 37.4016C-0.647606 61.3861 4.13349 66.8861 28.1179 68.5632L37.1623 69.1957C61.1467 70.8728 66.6467 66.0917 68.3239 42.1073L68.9563 33.063C70.6335 9.07854 65.8524 3.57851 41.868 1.90136L32.8236 1.26892Z" fill="url(#paint3_linear_4725_68011)"/>
|
||||
<g filter="url(#filter1_ddii_4725_68011)">
|
||||
<path d="M20.8165 43.8927L14.5692 43.4559C13.0888 43.3523 11.8006 44.5292 11.6919 46.0845C11.5831 47.6398 12.695 48.9845 14.1753 49.088L20.4227 49.5248C21.903 49.6284 23.1912 48.4515 23.3 46.8962C23.4087 45.3409 22.2968 43.9962 20.8165 43.8927Z" fill="#F23E94"/>
|
||||
<path d="M14.5574 43.6244L20.8047 44.0612C22.1842 44.1577 23.2343 45.4143 23.1315 46.8844C23.0287 48.3546 21.814 49.4528 20.4344 49.3564L14.1871 48.9195C12.8076 48.823 11.7576 47.5664 11.8604 46.0963C11.9632 44.6261 13.1779 43.5279 14.5574 43.6244Z" stroke="url(#paint4_linear_4725_68011)" stroke-opacity="0.1" stroke-width="0.33782" stroke-linejoin="round"/>
|
||||
<path d="M48.3652 51.4775L54.6125 51.9144C56.0928 52.0179 57.381 50.841 57.4898 49.2857C57.5985 47.7305 56.4866 46.3858 55.0063 46.2823L48.759 45.8454C47.2787 45.7419 45.9905 46.9188 45.8817 48.474C45.7729 50.0293 46.8848 51.374 48.3652 51.4775Z" fill="#F23E94"/>
|
||||
<path d="M54.6243 51.7459L48.3769 51.309C46.9974 51.2126 45.9474 49.956 46.0502 48.4858C46.153 47.0157 47.3677 45.9174 48.7472 46.0139L54.9945 46.4508C56.3741 46.5472 57.4241 47.8038 57.3213 49.274C57.2185 50.7441 56.0038 51.8423 54.6243 51.7459Z" stroke="url(#paint5_linear_4725_68011)" stroke-opacity="0.1" stroke-width="0.33782" stroke-linejoin="round"/>
|
||||
<path d="M37.3154 45.0271L32.2298 44.6714C30.7495 44.5679 29.4613 45.7448 29.3525 47.3001C29.2438 48.8553 30.3556 50.2001 31.836 50.3036L36.9215 50.6592C38.4019 50.7627 39.6901 49.5858 39.7988 48.0306C39.9076 46.4753 38.7957 45.1306 37.3154 45.0271Z" fill="#F23E94"/>
|
||||
<path d="M32.218 44.8399L37.3036 45.1956C38.6831 45.292 39.7331 46.5486 39.6303 48.0188C39.5275 49.4889 38.3128 50.5872 36.9333 50.4907L31.8478 50.1351C30.4682 50.0386 29.4182 48.782 29.521 47.3119C29.6238 45.8417 30.8385 44.7435 32.218 44.8399Z" stroke="url(#paint6_linear_4725_68011)" stroke-opacity="0.1" stroke-width="0.33782" stroke-linejoin="round"/>
|
||||
<path d="M46.5039 43.2046L52.5198 43.6253C54.0001 43.7288 55.2884 42.5519 55.3971 40.9967C55.5059 39.4414 54.394 38.0967 52.9136 37.9932L46.8977 37.5725C45.4174 37.469 44.1292 38.6459 44.0204 40.2011C43.9116 41.7564 45.0235 43.1011 46.5039 43.2046Z" fill="#6927DA"/>
|
||||
<path d="M52.5316 43.4568L46.5156 43.0361C45.1361 42.9397 44.0861 41.6831 44.1889 40.2129C44.2917 38.7428 45.5064 37.6445 46.8859 37.741L52.9019 38.1617C54.2814 38.2582 55.3314 39.5148 55.2286 40.9849C55.1258 42.455 53.9111 43.5533 52.5316 43.4568Z" stroke="url(#paint7_linear_4725_68011)" stroke-opacity="0.1" stroke-width="0.33782" stroke-linejoin="round"/>
|
||||
<path d="M23.7094 35.95L17.6934 35.5293C16.2131 35.4258 14.9249 36.6027 14.8161 38.158C14.7074 39.7133 15.8193 41.058 17.2996 41.1615L23.3155 41.5822C24.7959 41.6857 26.0841 40.5088 26.1928 38.9535C26.3016 37.3983 25.1897 36.0535 23.7094 35.95Z" fill="#6927DA"/>
|
||||
<path d="M17.6817 35.6978L23.6976 36.1185C25.0771 36.215 26.1272 37.4716 26.0244 38.9417C25.9215 40.4119 24.7069 41.5101 23.3273 41.4137L17.3114 40.993C15.9319 40.8965 14.8818 39.6399 14.9846 38.1698C15.0874 36.6996 16.3021 35.6014 17.6817 35.6978Z" stroke="url(#paint8_linear_4725_68011)" stroke-opacity="0.1" stroke-width="0.33782" stroke-linejoin="round"/>
|
||||
<path d="M39.8134 37.0582L30.8613 36.4322C29.381 36.3287 28.0927 37.5055 27.984 39.0608C27.8752 40.6161 28.9871 41.9608 30.4675 42.0643L39.4195 42.6903C40.8999 42.7938 42.1881 41.6169 42.2968 40.0617C42.4056 38.5064 41.2937 37.1617 39.8134 37.0582Z" fill="#6927DA"/>
|
||||
<path d="M30.8495 36.6007L39.8016 37.2267C41.1811 37.3231 42.2311 38.5797 42.1283 40.0499C42.0255 41.52 40.8108 42.6183 39.4313 42.5218L30.4792 41.8958C29.0997 41.7994 28.0497 40.5428 28.1525 39.0726C28.2553 37.6025 29.47 36.5042 30.8495 36.6007Z" stroke="url(#paint9_linear_4725_68011)" stroke-opacity="0.1" stroke-width="0.33782" stroke-linejoin="round"/>
|
||||
<path d="M32.3636 28.1666L20.9406 27.3679C19.4603 27.2643 18.1721 28.4412 18.0633 29.9965C17.9546 31.5518 19.0665 32.8965 20.5468 33L31.9698 33.7988C33.4501 33.9023 34.7383 32.7254 34.8471 31.1701C34.9558 29.6148 33.8439 28.2701 32.3636 28.1666Z" fill="#1570EF"/>
|
||||
<path d="M20.9289 27.5363L32.3518 28.3351C33.7314 28.4316 34.7814 29.6882 34.6786 31.1583C34.5758 32.6285 33.3611 33.7267 31.9816 33.6303L20.5586 32.8315C19.179 32.735 18.129 31.4784 18.2318 30.0083C18.3346 28.5381 19.5493 27.4399 20.9289 27.5363Z" stroke="url(#paint10_linear_4725_68011)" stroke-opacity="0.1" stroke-width="0.33782" stroke-linejoin="round"/>
|
||||
<path d="M39.7417 34.3403L50.4352 35.0881C51.9156 35.1916 53.2038 34.0147 53.3125 32.4594C53.4213 30.9042 52.3094 29.5595 50.8291 29.456L40.1356 28.7082C38.6552 28.6047 37.367 29.7816 37.2583 31.3368C37.1495 32.8921 38.2614 34.2368 39.7417 34.3403Z" fill="#1570EF"/>
|
||||
<path d="M50.447 34.9196L39.7535 34.1718C38.374 34.0754 37.324 32.8188 37.4268 31.3486C37.5296 29.8785 38.7442 28.7802 40.1238 28.8767L50.8173 29.6245C52.1968 29.7209 53.2468 30.9775 53.144 32.4477C53.0412 33.9178 51.8265 35.0161 50.447 34.9196Z" stroke="url(#paint11_linear_4725_68011)" stroke-opacity="0.1" stroke-width="0.33782" stroke-linejoin="round"/>
|
||||
<path d="M48.8251 21.1728L42.7957 20.7512C41.3154 20.6476 40.0272 21.8245 39.9184 23.3798C39.8097 24.9351 40.9216 26.2798 42.4019 26.3833L48.4312 26.8049C49.9116 26.9084 51.1998 25.7315 51.3085 24.1763C51.4173 22.621 50.3054 21.2763 48.8251 21.1728Z" fill="#22CCEE"/>
|
||||
<path d="M42.784 20.9196L48.8133 21.3413C50.1928 21.4377 51.2428 22.6943 51.14 24.1645C51.0372 25.6346 49.8226 26.7329 48.443 26.6364L42.4137 26.2148C41.0342 26.1183 39.9841 24.8617 40.0869 23.3916C40.1897 21.9214 41.4044 20.8232 42.784 20.9196Z" stroke="url(#paint12_linear_4725_68011)" stroke-opacity="0.1" stroke-width="0.33782" stroke-linejoin="round"/>
|
||||
<path d="M30.0984 19.8627L24.0691 19.4411C22.5887 19.3376 21.3005 20.5145 21.1918 22.0697C21.083 23.625 22.1949 24.9697 23.6752 25.0732L29.7046 25.4948C31.1849 25.5983 32.4731 24.4215 32.5819 22.8662C32.6906 21.3109 31.5787 19.9662 30.0984 19.8627Z" fill="#22CCEE"/>
|
||||
<path d="M24.0573 19.6096L30.0866 20.0312C31.4661 20.1277 32.5162 21.3843 32.4134 22.8544C32.3106 24.3246 31.0959 25.4228 29.7163 25.3263L23.687 24.9047C22.3075 24.8083 21.2574 23.5517 21.3603 22.0815C21.4631 20.6114 22.6777 19.5131 24.0573 19.6096Z" stroke="url(#paint13_linear_4725_68011)" stroke-opacity="0.1" stroke-width="0.33782" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<g opacity="0.23" filter="url(#filter2_f_4725_68011)">
|
||||
<path d="M2.69258 40.4874L12.7122 44.9449L39.098 57.1213L66.9412 61.8859L45.5294 72.5984L-0.202764 68.4613L2.69258 40.4874Z" fill="#F24396"/>
|
||||
</g>
|
||||
<g opacity="0.23" filter="url(#filter3_f_4725_68011)">
|
||||
<path d="M2.56821 1.97031L52.6272 -2.04293L69.5358 11.3492L56.9932 16.1074L23.2807 14.6892L2.56821 1.97031Z" fill="#22CCEE"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_iii_4725_68011" x="0.72644" y="-2.61149" width="68.533" height="75.6876" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1.27762"/>
|
||||
<feGaussianBlur stdDeviation="1.59703"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.196078 0 0 0 0 0.188235 0 0 0 0 0.219608 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_4725_68011"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="3.57731"/>
|
||||
<feGaussianBlur stdDeviation="2.68298"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.321569 0 0 0 0 0.905882 0 0 0 0 1 0 0 0 0.6 0"/>
|
||||
<feBlend mode="normal" in2="effect1_innerShadow_4725_68011" result="effect2_innerShadow_4725_68011"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-3.57731"/>
|
||||
<feGaussianBlur stdDeviation="1.78866"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.933333 0 0 0 0 0.160784 0 0 0 0 0.509804 0 0 0 0.5 0"/>
|
||||
<feBlend mode="normal" in2="effect2_innerShadow_4725_68011" result="effect3_innerShadow_4725_68011"/>
|
||||
</filter>
|
||||
<filter id="filter1_ddii_4725_68011" x="1.54998" y="10.9893" width="66.0817" height="52.755" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1.6891"/>
|
||||
<feGaussianBlur stdDeviation="5.0673"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.29135 0 0 0 0 0.0895476 0 0 0 0 0.654593 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4725_68011"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1.6891"/>
|
||||
<feGaussianBlur stdDeviation="4.22275"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_4725_68011" result="effect2_dropShadow_4725_68011"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_4725_68011" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-1.6891"/>
|
||||
<feGaussianBlur stdDeviation="0.844549"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect3_innerShadow_4725_68011"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1.6891"/>
|
||||
<feGaussianBlur stdDeviation="0.844549"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.05 0"/>
|
||||
<feBlend mode="normal" in2="effect3_innerShadow_4725_68011" result="effect4_innerShadow_4725_68011"/>
|
||||
</filter>
|
||||
<filter id="filter2_f_4725_68011" x="-23.4553" y="17.2348" width="113.649" height="78.6161" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="11.6263" result="effect1_foregroundBlur_4725_68011"/>
|
||||
</filter>
|
||||
<filter id="filter3_f_4725_68011" x="-20.6843" y="-25.2955" width="113.473" height="64.6555" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="11.6263" result="effect1_foregroundBlur_4725_68011"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_4725_68011" x1="53.7772" y1="8.36942" x2="38.7315" y2="35.4937" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#363636"/>
|
||||
<stop offset="1" stop-color="#141414"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_4725_68011" x1="37.3458" y1="1.58514" x2="32.6401" y2="68.8795" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#52EDFF"/>
|
||||
<stop offset="0.274483" stop-color="#4361EE"/>
|
||||
<stop offset="0.629793" stop-color="#7209B7"/>
|
||||
<stop offset="1" stop-color="#F12980"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_4725_68011" x1="37.019" y1="6.25836" x2="33.4897" y2="56.7291" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_4725_68011" x1="37.3458" y1="1.58514" x2="32.6401" y2="68.8795" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#52EDFF"/>
|
||||
<stop offset="0.274483" stop-color="#4361EE"/>
|
||||
<stop offset="0.629793" stop-color="#7209B7"/>
|
||||
<stop offset="1" stop-color="#F12980"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_4725_68011" x1="17.6928" y1="43.6743" x2="17.299" y2="49.3064" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_4725_68011" x1="51.4888" y1="51.6959" x2="51.8826" y2="46.0638" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint6_linear_4725_68011" x1="34.7726" y1="44.8492" x2="34.3788" y2="50.4814" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint7_linear_4725_68011" x1="49.5118" y1="43.415" x2="49.9057" y2="37.7829" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint8_linear_4725_68011" x1="20.7014" y1="35.7397" x2="20.3076" y2="41.3718" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint9_linear_4725_68011" x1="35.3373" y1="36.7452" x2="34.9435" y2="42.3773" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint10_linear_4725_68011" x1="26.6521" y1="27.7672" x2="26.2583" y2="33.3994" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint11_linear_4725_68011" x1="45.0885" y1="34.7142" x2="45.4823" y2="29.0821" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint12_linear_4725_68011" x1="45.8104" y1="20.962" x2="45.4166" y2="26.5941" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint13_linear_4725_68011" x1="27.0837" y1="19.6519" x2="26.6899" y2="25.284" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 19 KiB |
BIN
app/assets/images/maybe-plus-background.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
2811
app/assets/images/maybe-plus-background.svg
Normal file
|
After Width: | Height: | Size: 299 KiB |
BIN
app/assets/images/maybe-plus-logo.png
Normal file
|
After Width: | Height: | Size: 91 KiB |
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
@@ -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,15 +19,16 @@
|
||||
@apply focus-within:border-gray-900 focus-within:shadow-none focus-within:ring-4 focus-within:ring-gray-100;
|
||||
}
|
||||
|
||||
.form-field__label {
|
||||
@apply block text-xs text-gray-500;
|
||||
|
||||
.form-field__label, .hw-combobox__label {
|
||||
@apply block text-xs text-gray-500 peer-disabled:text-gray-400;
|
||||
}
|
||||
|
||||
.form-field__input {
|
||||
@apply border-none bg-transparent text-sm opacity-100 w-full p-0;
|
||||
@apply focus:opacity-100 focus:outline-none focus:ring-0;
|
||||
@apply placeholder-shown:opacity-50;
|
||||
@apply disabled:opacity-50;
|
||||
@apply disabled:text-gray-400;
|
||||
}
|
||||
|
||||
.form-field__radio {
|
||||
@@ -35,7 +36,7 @@
|
||||
}
|
||||
|
||||
.form-field__submit {
|
||||
@apply w-full cursor-pointer rounded-lg bg-black p-3 text-center text-white hover:bg-gray-700;
|
||||
@apply cursor-pointer rounded-lg bg-black p-3 text-center text-white hover:bg-gray-700;
|
||||
}
|
||||
|
||||
input:checked+label+.toggle-switch-dot {
|
||||
@@ -63,12 +64,16 @@
|
||||
}
|
||||
|
||||
select[multiple="multiple"] option {
|
||||
@apply p-2 rounded-md;
|
||||
@apply py-2 rounded-md;
|
||||
}
|
||||
|
||||
select[multiple="multiple"] option:checked {
|
||||
@apply bg-gray-50;
|
||||
@apply after:content-['\2713'] after:float-right after:text-gray-500;
|
||||
@apply after:content-['\2713'] bg-white after:text-gray-500 after:ml-2;
|
||||
}
|
||||
|
||||
select[multiple="multiple"] option:active,
|
||||
select[multiple="multiple"] option:focus {
|
||||
@apply bg-white;
|
||||
}
|
||||
|
||||
.maybe-switch {
|
||||
@@ -94,6 +99,53 @@
|
||||
.tooltip {
|
||||
@apply hidden absolute;
|
||||
}
|
||||
|
||||
.btn {
|
||||
@apply px-3 py-2 rounded-lg text-sm font-medium cursor-pointer disabled:cursor-not-allowed focus:outline-gray-500;
|
||||
}
|
||||
|
||||
.btn--primary {
|
||||
@apply bg-gray-900 text-white hover:bg-gray-700 disabled:bg-gray-50 disabled:hover:bg-gray-50 disabled:text-gray-400;
|
||||
}
|
||||
|
||||
.btn--secondary {
|
||||
@apply bg-gray-50 hover:bg-gray-100 text-gray-900;
|
||||
}
|
||||
|
||||
.btn--outline {
|
||||
@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 {
|
||||
@apply border border-transparent text-gray-900 hover:bg-gray-50;
|
||||
}
|
||||
}
|
||||
|
||||
.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 */
|
||||
@@ -111,3 +163,19 @@
|
||||
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_back_or_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,23 +1,28 @@
|
||||
class Account::HoldingsController < ApplicationController
|
||||
layout :with_sidebar
|
||||
|
||||
before_action :set_account
|
||||
before_action :set_holding, only: :show
|
||||
before_action :set_holding, only: %i[show destroy]
|
||||
|
||||
def index
|
||||
@holdings = @account.holdings.current
|
||||
@account = Current.family.accounts.find(params[:account_id])
|
||||
end
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
private
|
||||
def destroy
|
||||
@holding.destroy_holding_and_entries!
|
||||
|
||||
def set_account
|
||||
@account = Current.family.accounts.find(params[:account_id])
|
||||
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_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,37 +1,37 @@
|
||||
class Account::TradesController < ApplicationController
|
||||
layout :with_sidebar
|
||||
include EntryableResource
|
||||
|
||||
before_action :set_account
|
||||
|
||||
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
|
||||
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 entry_params
|
||||
params.require(:account_entry)
|
||||
.permit(:type, :date, :qty, :ticker, :price, :amount, :currency, :transfer_account_id)
|
||||
.merge(account: @account)
|
||||
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 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
@@ -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,53 +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
|
||||
@entries = @account.entries.account_transactions.reverse_chronological
|
||||
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.merge(amount: amount))
|
||||
@entry.sync_account_later
|
||||
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, :entryable_type,
|
||||
entryable_attributes: [
|
||||
:id,
|
||||
:notes,
|
||||
:excluded,
|
||||
:category_id,
|
||||
:merchant_id,
|
||||
{ tag_ids: [] }
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
def amount
|
||||
if params[:account_entry][:nature] == "income"
|
||||
entry_params[:amount].to_d * -1
|
||||
else
|
||||
entry_params[:amount].to_d
|
||||
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, :currency, :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_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)
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
def update
|
||||
Account.transaction do
|
||||
@account.update! account_params.except(:accountable_type, :balance)
|
||||
@account.update_balance!(account_params[:balance]) if account_params[:balance]
|
||||
end
|
||||
@account.sync_later
|
||||
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")
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
redirect_back_or_to accounts_path, alert: e.record.errors.full_messages.to_sentence
|
||||
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,15 +1,41 @@
|
||||
class ApplicationController < ActionController::Base
|
||||
include AutoSync, Authentication, Invitable, SelfHostable
|
||||
include Onboardable, Localize, AutoSync, Authentication, Invitable, SelfHostable, StoreLocation, Impersonatable
|
||||
include Pagy::Backend
|
||||
|
||||
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
|
||||
allow_browser versions: :modern
|
||||
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])
|
||||
|
||||
@@ -6,17 +6,17 @@ class Category::DropdownsController < ApplicationController
|
||||
end
|
||||
|
||||
private
|
||||
def set_from_params
|
||||
if params[:category_id]
|
||||
@selected_category = categories_scope.find(params[:category_id])
|
||||
def set_from_params
|
||||
if params[:category_id]
|
||||
@selected_category = categories_scope.find(params[:category_id])
|
||||
end
|
||||
|
||||
if params[:transaction_id]
|
||||
@transaction = Current.family.transactions.find(params[:transaction_id])
|
||||
end
|
||||
end
|
||||
|
||||
if params[:transaction_id]
|
||||
@transaction = Current.family.transactions.find(params[:transaction_id])
|
||||
def categories_scope
|
||||
Current.family.categories.alphabetically
|
||||
end
|
||||
end
|
||||
|
||||
def categories_scope
|
||||
Current.family.categories.alphabetically
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
@@ -2,6 +2,7 @@ module Authentication
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :set_request_details
|
||||
before_action :authenticate_user!
|
||||
end
|
||||
|
||||
@@ -12,28 +13,34 @@ module Authentication
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authenticate_user!
|
||||
if user = User.find_by(id: session[:user_id])
|
||||
Current.user = user
|
||||
else
|
||||
redirect_to new_session_url
|
||||
def authenticate_user!
|
||||
if session_record = find_session_by_cookie
|
||||
Current.session = session_record
|
||||
else
|
||||
if self_hosted_first_login?
|
||||
redirect_to new_registration_url
|
||||
else
|
||||
redirect_to new_session_url
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def login(user)
|
||||
Current.user = user
|
||||
reset_session
|
||||
session[:user_id] = user.id
|
||||
set_last_login_at
|
||||
end
|
||||
def find_session_by_cookie
|
||||
Session.find_by(id: cookies.signed[:session_token])
|
||||
end
|
||||
|
||||
def logout
|
||||
Current.user = nil
|
||||
reset_session
|
||||
end
|
||||
def create_session_for(user)
|
||||
session = user.sessions.create!
|
||||
cookies.signed.permanent[:session_token] = { value: session.id, httponly: true }
|
||||
session
|
||||
end
|
||||
|
||||
def set_last_login_at
|
||||
Current.user.update(last_login_at: DateTime.now)
|
||||
end
|
||||
def self_hosted_first_login?
|
||||
Rails.application.config.app_mode.self_hosted? && User.count.zero?
|
||||
end
|
||||
|
||||
def set_request_details
|
||||
Current.user_agent = request.user_agent
|
||||
Current.ip_address = request.ip
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
@@ -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
@@ -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,11 @@ module Invitable
|
||||
|
||||
private
|
||||
def invite_code_required?
|
||||
ENV["REQUIRE_INVITE_CODE"] == "true"
|
||||
return false if @invitation.present?
|
||||
self_hosted? ? Setting.require_invite_for_signup : ENV["REQUIRE_INVITE_CODE"] == "true"
|
||||
end
|
||||
|
||||
def self_hosted?
|
||||
Rails.application.config.app_mode.self_hosted?
|
||||
end
|
||||
end
|
||||
|
||||
19
app/controllers/concerns/localize.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
module Localize
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
around_action :switch_locale
|
||||
around_action :switch_timezone
|
||||
end
|
||||
|
||||
private
|
||||
def switch_locale(&action)
|
||||
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
@@ -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
|
||||
@@ -2,11 +2,15 @@ module SelfHostable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
helper_method :self_hosted?
|
||||
helper_method :self_hosted?, :self_hosted_first_login?
|
||||
end
|
||||
|
||||
private
|
||||
def self_hosted?
|
||||
Rails.configuration.app_mode.self_hosted?
|
||||
end
|
||||
|
||||
def self_hosted_first_login?
|
||||
self_hosted? && User.count.zero?
|
||||
end
|
||||
end
|
||||
|
||||
41
app/controllers/concerns/store_location.rb
Normal file
@@ -0,0 +1,41 @@
|
||||
module StoreLocation
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
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
|
||||
session[:return_to] || fallback_path
|
||||
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?
|
||||
session[:return_to] = params[:return_to]
|
||||
end
|
||||
end
|
||||
|
||||
def clear_previous_path
|
||||
if request.fullpath == session[:return_to]
|
||||
session.delete(:return_to)
|
||||
end
|
||||
end
|
||||
|
||||
def fallback_path
|
||||
root_path
|
||||
end
|
||||
end
|
||||
12
app/controllers/credit_cards_controller.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
class CreditCardsController < ApplicationController
|
||||
include AccountableResource
|
||||
|
||||
permitted_accountable_attributes(
|
||||
:id,
|
||||
:available_credit,
|
||||
:minimum_payment,
|
||||
:apr,
|
||||
:annual_fee,
|
||||
:expiration_date
|
||||
)
|
||||
end
|
||||
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
@@ -0,0 +1,3 @@
|
||||
class DepositoriesController < ApplicationController
|
||||
include AccountableResource
|
||||
end
|
||||
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
|
||||
22
app/controllers/import/cleans_controller.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
class Import::CleansController < ApplicationController
|
||||
layout "imports"
|
||||
|
||||
before_action :set_import
|
||||
|
||||
def show
|
||||
redirect_to import_configuration_path(@import), alert: "Please configure your import before proceeding." unless @import.configured?
|
||||
|
||||
rows = @import.rows.ordered
|
||||
|
||||
if params[:view] == "errors"
|
||||
rows = rows.reject { |row| row.valid? }
|
||||
end
|
||||
|
||||
@pagy, @rows = pagy_array(rows, limit: params[:per_page] || "10")
|
||||
end
|
||||
|
||||
private
|
||||
def set_import
|
||||
@import = Current.family.imports.find(params[:import_id])
|
||||
end
|
||||
end
|
||||
40
app/controllers/import/configurations_controller.rb
Normal file
@@ -0,0 +1,40 @@
|
||||
class Import::ConfigurationsController < ApplicationController
|
||||
layout "imports"
|
||||
|
||||
before_action :set_import
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def update
|
||||
@import.update!(import_params)
|
||||
@import.generate_rows_from_csv
|
||||
@import.reload.sync_mappings
|
||||
|
||||
redirect_to import_clean_path(@import), notice: "Import configured successfully."
|
||||
end
|
||||
|
||||
private
|
||||
def set_import
|
||||
@import = Current.family.imports.find(params[:import_id])
|
||||
end
|
||||
|
||||
def import_params
|
||||
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
|
||||
14
app/controllers/import/confirms_controller.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
class Import::ConfirmsController < ApplicationController
|
||||
layout "imports"
|
||||
|
||||
before_action :set_import
|
||||
|
||||
def show
|
||||
redirect_to import_clean_path(@import), alert: "You have invalid data, please edit until all errors are resolved" unless @import.cleaned?
|
||||
end
|
||||
|
||||
private
|
||||
def set_import
|
||||
@import = Current.family.imports.find(params[:import_id])
|
||||
end
|
||||
end
|
||||
43
app/controllers/import/mappings_controller.rb
Normal file
@@ -0,0 +1,43 @@
|
||||
class Import::MappingsController < ApplicationController
|
||||
before_action :set_import
|
||||
|
||||
def update
|
||||
mapping = @import.mappings.find(params[:id])
|
||||
|
||||
mapping.update! \
|
||||
create_when_empty: create_when_empty,
|
||||
mappable: mappable,
|
||||
value: mapping_params[:value]
|
||||
|
||||
redirect_back_or_to import_confirm_path(@import)
|
||||
end
|
||||
|
||||
private
|
||||
def mapping_params
|
||||
params.require(:import_mapping).permit(:type, :key, :mappable_id, :mappable_type, :value)
|
||||
end
|
||||
|
||||
def set_import
|
||||
@import = Current.family.imports.find(params[:import_id])
|
||||
end
|
||||
|
||||
def mappable
|
||||
return nil unless mappable_class.present?
|
||||
|
||||
@mappable ||= mappable_class.find_by(id: mapping_params[:mappable_id], family: Current.family)
|
||||
end
|
||||
|
||||
def create_when_empty
|
||||
return false unless mapping_class.present?
|
||||
|
||||
mapping_params[:mappable_id] == mapping_class::CREATE_NEW_KEY
|
||||
end
|
||||
|
||||
def mappable_class
|
||||
mapping_params[:mappable_type]&.constantize
|
||||
end
|
||||
|
||||
def mapping_class
|
||||
mapping_params[:type]&.constantize
|
||||
end
|
||||
end
|
||||
24
app/controllers/import/rows_controller.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
class Import::RowsController < ApplicationController
|
||||
before_action :set_import_row
|
||||
|
||||
def update
|
||||
@row.assign_attributes(row_params)
|
||||
@row.save!(validate: false)
|
||||
@row.sync_mappings
|
||||
|
||||
redirect_to import_row_path(@row.import, @row)
|
||||
end
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
private
|
||||
def row_params
|
||||
params.require(:import_row).permit(:type, :account, :date, :qty, :ticker, :price, :amount, :currency, :name, :category, :tags, :entity_type, :notes)
|
||||
end
|
||||
|
||||
def set_import_row
|
||||
@import = Current.family.imports.find(params[:import_id])
|
||||
@row = @import.rows.find(params[:id])
|
||||
end
|
||||
end
|
||||
47
app/controllers/import/uploads_controller.rb
Normal file
@@ -0,0 +1,47 @@
|
||||
class Import::UploadsController < ApplicationController
|
||||
layout "imports"
|
||||
|
||||
before_action :set_import
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def update
|
||||
if csv_valid?(csv_str)
|
||||
@import.assign_attributes(raw_file_str: csv_str, col_sep: upload_params[:col_sep])
|
||||
@import.save!(validate: false)
|
||||
|
||||
redirect_to import_configuration_path(@import), notice: "CSV uploaded successfully."
|
||||
else
|
||||
flash.now[:alert] = "Must be valid CSV with headers and at least one row of data"
|
||||
|
||||
render :show, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def set_import
|
||||
@import = Current.family.imports.find(params[:import_id])
|
||||
end
|
||||
|
||||
def csv_str
|
||||
@csv_str ||= upload_params[:csv_file]&.read || upload_params[:raw_file_str]
|
||||
end
|
||||
|
||||
def csv_valid?(str)
|
||||
require "csv"
|
||||
|
||||
begin
|
||||
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
|
||||
rescue CSV::MalformedCSVError
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def upload_params
|
||||
params.require(:import).permit(:raw_file_str, :csv_file, :col_sep)
|
||||
end
|
||||
end
|
||||
@@ -1,118 +1,48 @@
|
||||
require "ostruct"
|
||||
|
||||
class ImportsController < ApplicationController
|
||||
before_action :set_import, except: %i[ index new create ]
|
||||
before_action :set_import, only: %i[show publish destroy]
|
||||
|
||||
def publish
|
||||
@import.publish_later
|
||||
|
||||
redirect_to import_path(@import), notice: "Your import has started in the background."
|
||||
end
|
||||
|
||||
def index
|
||||
@imports = Current.family.imports
|
||||
render layout: "with_sidebar"
|
||||
|
||||
render layout: with_sidebar
|
||||
end
|
||||
|
||||
def new
|
||||
account = Current.family.accounts.find_by(id: params[:account_id])
|
||||
@import = Import.new account: account
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
account = Current.family.accounts.find(params[:import][:account_id])
|
||||
|
||||
@import.update! account: account
|
||||
redirect_to load_import_path(@import), notice: t(".import_updated")
|
||||
@pending_import = Current.family.imports.ordered.pending.first
|
||||
end
|
||||
|
||||
def create
|
||||
account = Current.family.accounts.find(params[:import][:account_id])
|
||||
@import = Import.create!(account: account)
|
||||
import = Current.family.imports.create! import_params
|
||||
|
||||
redirect_to load_import_path(@import), notice: t(".import_created")
|
||||
redirect_to import_upload_path(import)
|
||||
end
|
||||
|
||||
def show
|
||||
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
|
||||
@import.destroy!
|
||||
redirect_to imports_url, notice: t(".import_destroyed"), status: :see_other
|
||||
end
|
||||
@import.destroy
|
||||
|
||||
def load
|
||||
end
|
||||
|
||||
def upload_csv
|
||||
begin
|
||||
@import.raw_csv_str = import_params[:raw_csv_str].read
|
||||
rescue NoMethodError
|
||||
flash.now[:alert] = "Please select a file to upload"
|
||||
render :load, status: :unprocessable_entity and return
|
||||
end
|
||||
if @import.save
|
||||
redirect_to configure_import_path(@import), notice: t(".import_loaded")
|
||||
else
|
||||
flash.now[:alert] = @import.errors.full_messages.to_sentence
|
||||
render :load, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def load_csv
|
||||
if @import.update(import_params)
|
||||
redirect_to configure_import_path(@import), notice: t(".import_loaded")
|
||||
else
|
||||
flash.now[:alert] = @import.errors.full_messages.to_sentence
|
||||
render :load, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def configure
|
||||
unless @import.loaded?
|
||||
redirect_to load_import_path(@import), alert: t(".invalid_csv")
|
||||
end
|
||||
end
|
||||
|
||||
def update_mappings
|
||||
@import.update! import_params(@import.expected_fields.map(&:key))
|
||||
redirect_to clean_import_path(@import), notice: t(".column_mappings_saved")
|
||||
end
|
||||
|
||||
def clean
|
||||
unless @import.loaded?
|
||||
redirect_to load_import_path(@import), alert: t(".invalid_csv")
|
||||
end
|
||||
end
|
||||
|
||||
def update_csv
|
||||
update_params = import_params[:csv_update]
|
||||
|
||||
@import.update_csv! \
|
||||
row_idx: update_params[:row_idx],
|
||||
col_idx: update_params[:col_idx],
|
||||
value: update_params[:value]
|
||||
|
||||
render :clean
|
||||
end
|
||||
|
||||
def confirm
|
||||
unless @import.cleaned?
|
||||
redirect_to clean_import_path(@import), alert: t(".invalid_data")
|
||||
end
|
||||
end
|
||||
|
||||
def publish
|
||||
if @import.valid?
|
||||
@import.publish_later
|
||||
redirect_to imports_path, notice: t(".import_published")
|
||||
else
|
||||
flash.now[:error] = t(".invalid_data")
|
||||
render :confirm, status: :unprocessable_entity
|
||||
end
|
||||
redirect_to imports_path, notice: "Your import has been deleted."
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_import
|
||||
@import = Current.family.imports.find(params[:id])
|
||||
end
|
||||
|
||||
def import_params(permitted_mappings = nil)
|
||||
params.require(:import).permit(:raw_csv_str, column_mappings: permitted_mappings, csv_update: [ :row_idx, :col_idx, :value ])
|
||||
def import_params
|
||||
params.require(:import).permit(:type)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,35 +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
|
||||
|
||||
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
@@ -0,0 +1,3 @@
|
||||
class InvestmentsController < ApplicationController
|
||||
include AccountableResource
|
||||
end
|
||||
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
|
||||
18
app/controllers/invite_codes_controller.rb
Normal file
@@ -0,0 +1,18 @@
|
||||
class InviteCodesController < ApplicationController
|
||||
before_action :ensure_self_hosted
|
||||
|
||||
def index
|
||||
@invite_codes = InviteCode.all
|
||||
end
|
||||
|
||||
def create
|
||||
InviteCode.generate!
|
||||
redirect_back_or_to invite_codes_path, notice: "Code generated"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_self_hosted
|
||||
redirect_to root_path unless self_hosted?
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,20 @@
|
||||
class Issue::ExchangeRateProviderMissingsController < ApplicationController
|
||||
before_action :set_issue, only: :update
|
||||
|
||||
def update
|
||||
Setting.synth_api_key = exchange_rate_params[:synth_api_key]
|
||||
account = @issue.issuable
|
||||
account.sync_later
|
||||
redirect_back_or_to account
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_issue
|
||||
@issue = Current.family.issues.find(params[:id])
|
||||
end
|
||||
|
||||
def exchange_rate_params
|
||||
params.require(:issue_exchange_rate_provider_missing).permit(:synth_api_key)
|
||||
end
|
||||
end
|
||||
13
app/controllers/issues_controller.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
class IssuesController < ApplicationController
|
||||
before_action :set_issue, only: :show
|
||||
|
||||
def show
|
||||
render template: "#{@issue.class.name.underscore.pluralize}/show", layout: "issues"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_issue
|
||||
@issue = Current.family.issues.find(params[:id])
|
||||
end
|
||||
end
|
||||
7
app/controllers/loans_controller.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
class LoansController < ApplicationController
|
||||
include AccountableResource
|
||||
|
||||
permitted_accountable_attributes(
|
||||
:id, :rate_type, :interest_rate, :term_months
|
||||
)
|
||||
end
|
||||
@@ -1,7 +1,7 @@
|
||||
class MerchantsController < ApplicationController
|
||||
layout :with_sidebar
|
||||
|
||||
before_action :set_merchant, only: %i[ edit update destroy ]
|
||||
before_action :set_merchant, only: %i[edit update destroy]
|
||||
|
||||
def index
|
||||
@merchants = Current.family.merchants.alphabetically
|
||||
@@ -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
|
||||
@@ -31,11 +36,11 @@ class MerchantsController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def set_merchant
|
||||
@merchant = Current.family.merchants.find(params[:id])
|
||||
end
|
||||
def set_merchant
|
||||
@merchant = Current.family.merchants.find(params[:id])
|
||||
end
|
||||
|
||||
def merchant_params
|
||||
params.require(:merchant).permit(:name, :color)
|
||||
end
|
||||
def merchant_params
|
||||
params.require(:merchant).permit(:name, :color)
|
||||
end
|
||||
end
|
||||
|
||||
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
@@ -0,0 +1,3 @@
|
||||
class OtherAssetsController < ApplicationController
|
||||
include AccountableResource
|
||||
end
|
||||
3
app/controllers/other_liabilities_controller.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
class OtherLiabilitiesController < ApplicationController
|
||||
include AccountableResource
|
||||
end
|
||||
@@ -1,9 +1,9 @@
|
||||
class PagesController < ApplicationController
|
||||
layout :with_sidebar
|
||||
|
||||
include Filterable
|
||||
skip_before_action :authenticate_user!, only: %i[early_access]
|
||||
layout :with_sidebar, except: %i[early_access]
|
||||
|
||||
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]
|
||||
@@ -19,24 +19,29 @@ 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
|
||||
|
||||
# TODO: Placeholders for trendlines
|
||||
placeholder_series_data = 10.times.map do |i|
|
||||
{ date: Date.current - i.days, value: Money.new(0) }
|
||||
{ date: Date.current - i.days, value: Money.new(0, Current.family.currency) }
|
||||
end
|
||||
@investing_series = TimeSeries.new(placeholder_series_data)
|
||||
end
|
||||
|
||||
def changelog
|
||||
@releases_notes = Provider::Github.new.fetch_latest_releases_notes
|
||||
@release_notes = Provider::Github.new.fetch_latest_release_notes
|
||||
end
|
||||
|
||||
def feedback
|
||||
end
|
||||
|
||||
def invites
|
||||
def early_access
|
||||
redirect_to root_path if self_hosted?
|
||||
|
||||
@invite_codes_count = InviteCode.count
|
||||
@invite_code = InviteCode.order("RANDOM()").limit(1).first
|
||||
render layout: false
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,7 +3,7 @@ class PasswordResetsController < ApplicationController
|
||||
|
||||
layout "auth"
|
||||
|
||||
before_action :set_user_by_token, only: %i[ edit update ]
|
||||
before_action :set_user_by_token, only: %i[edit update]
|
||||
|
||||
def new
|
||||
end
|
||||
@@ -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
|
||||
@@ -33,12 +33,12 @@ class PasswordResetsController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def set_user_by_token
|
||||
@user = User.find_by_token_for(:password_reset, params[:token])
|
||||
redirect_to new_password_reset_path, alert: t("password_resets.update.invalid_token") unless @user.present?
|
||||
end
|
||||
def set_user_by_token
|
||||
@user = User.find_by_token_for(:password_reset, params[:token])
|
||||
redirect_to new_password_reset_path, alert: t("password_resets.update.invalid_token") unless @user.present?
|
||||
end
|
||||
|
||||
def password_params
|
||||
params.require(:user).permit(:password, :password_confirmation)
|
||||
end
|
||||
def password_params
|
||||
params.require(:user).permit(:password, :password_confirmation)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,7 +12,7 @@ class PasswordsController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def password_params
|
||||
params.require(:user).permit(:password, :password_confirmation, :password_challenge).with_defaults(password_challenge: "")
|
||||
end
|
||||
def password_params
|
||||
params.require(:user).permit(:password, :password_confirmation, :password_challenge).with_defaults(password_challenge: "")
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
21
app/controllers/properties_controller.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
class PropertiesController < ApplicationController
|
||||
include AccountableResource
|
||||
|
||||
permitted_accountable_attributes(
|
||||
:id, :year_built, :area_unit, :area_value,
|
||||
address_attributes: [ :line1, :line2, :locality, :region, :country, :postal_code ]
|
||||
)
|
||||
|
||||
def new
|
||||
@account = Current.family.accounts.build(
|
||||
currency: Current.family.currency,
|
||||
accountable: Property.new(
|
||||
address: Address.new
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
def edit
|
||||
@account.accountable.address ||= Address.new
|
||||
end
|
||||
end
|
||||
@@ -4,41 +4,54 @@ 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)
|
||||
login @user
|
||||
flash[:notice] = t(".success")
|
||||
redirect_to root_path
|
||||
@invitation&.update!(accepted_at: Time.current)
|
||||
Category.create_default_categories(@user.family) unless @invitation
|
||||
@session = create_session_for(@user)
|
||||
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)
|
||||
end
|
||||
|
||||
def user_params
|
||||
params.require(:user).permit(:name, :email, :password, :password_confirmation, :invite_code)
|
||||
end
|
||||
|
||||
def claim_invite_code
|
||||
unless InviteCode.claim! params[:user][:invite_code]
|
||||
redirect_to new_registration_path, alert: t("registrations.create.invalid_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 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
|
||||
unless InviteCode.claim! params[:user][:invite_code]
|
||||
redirect_to new_registration_path, alert: t("registrations.create.invalid_invite_code")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
@@ -1,4 +1,5 @@
|
||||
class SessionsController < ApplicationController
|
||||
before_action :set_session, only: :destroy
|
||||
skip_authentication only: %i[new create]
|
||||
|
||||
layout "auth"
|
||||
@@ -8,7 +9,7 @@ class SessionsController < ApplicationController
|
||||
|
||||
def create
|
||||
if user = User.authenticate_by(email: params[:email], password: params[:password])
|
||||
login user
|
||||
@session = create_session_for(user)
|
||||
redirect_to root_path
|
||||
else
|
||||
flash.now[:alert] = t(".invalid_credentials")
|
||||
@@ -17,7 +18,12 @@ class SessionsController < ApplicationController
|
||||
end
|
||||
|
||||
def destroy
|
||||
logout
|
||||
redirect_to root_path, notice: t(".logout_successful")
|
||||
@session.destroy
|
||||
redirect_to new_session_path, notice: t(".logout_successful")
|
||||
end
|
||||
|
||||
private
|
||||
def set_session
|
||||
@session = Current.user.sessions.find(params[:id])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
class Settings::BillingsController < SettingsController
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
def show
|
||||
@user = Current.user
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,71 +1,43 @@
|
||||
class Settings::HostingsController < SettingsController
|
||||
before_action :verify_hosting_mode
|
||||
before_action :raise_if_not_self_hosted
|
||||
|
||||
def show
|
||||
@synth_usage = Current.family.synth_usage
|
||||
end
|
||||
|
||||
def update
|
||||
if all_updates_valid?
|
||||
hosting_params.keys.each do |key|
|
||||
Setting.send("#{key}=", hosting_params[key].strip)
|
||||
end
|
||||
if hosting_params[:upgrades_setting].present?
|
||||
mode = hosting_params[:upgrades_setting] == "manual" ? "manual" : "auto"
|
||||
target = hosting_params[:upgrades_setting] == "commit" ? "commit" : "release"
|
||||
|
||||
redirect_to settings_hosting_path, notice: t(".success")
|
||||
else
|
||||
flash.now[:error] = @errors.first.message
|
||||
render :show, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def send_test_email
|
||||
unless Setting.smtp_settings_populated?
|
||||
flash[:alert] = t(".missing_smtp_setting_error")
|
||||
render(:show, status: :unprocessable_entity)
|
||||
return
|
||||
Setting.upgrades_mode = mode
|
||||
Setting.upgrades_target = target
|
||||
end
|
||||
|
||||
begin
|
||||
NotificationMailer.with(user: Current.user).test_email.deliver_now
|
||||
rescue => _e
|
||||
flash[:alert] = t(".error")
|
||||
render :show, status: :unprocessable_entity
|
||||
return
|
||||
if hosting_params.key?(:render_deploy_hook)
|
||||
Setting.render_deploy_hook = hosting_params[:render_deploy_hook]
|
||||
end
|
||||
|
||||
if hosting_params.key?(:require_invite_for_signup)
|
||||
Setting.require_invite_for_signup = hosting_params[:require_invite_for_signup]
|
||||
end
|
||||
|
||||
if hosting_params.key?(:synth_api_key)
|
||||
Setting.synth_api_key = hosting_params[:synth_api_key]
|
||||
end
|
||||
|
||||
redirect_to settings_hosting_path, notice: t(".success")
|
||||
rescue ActiveRecord::RecordInvalid => error
|
||||
flash.now[:alert] = t(".failure")
|
||||
render :show, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
private
|
||||
def all_updates_valid?
|
||||
@errors = ActiveModel::Errors.new(Setting)
|
||||
hosting_params.keys.each do |key|
|
||||
setting = Setting.new(var: key)
|
||||
setting.value = hosting_params[key].strip
|
||||
|
||||
unless setting.valid?
|
||||
@errors.merge!(setting.errors)
|
||||
end
|
||||
end
|
||||
|
||||
if hosting_params[:upgrades_mode] == "auto" && hosting_params[:render_deploy_hook].blank?
|
||||
@errors.add(:render_deploy_hook, t("settings.hostings.update.render_deploy_hook_error"))
|
||||
end
|
||||
|
||||
@errors.empty?
|
||||
end
|
||||
|
||||
def hosting_params
|
||||
permitted_params = params.require(:setting).permit(:render_deploy_hook, :upgrades_mode, :email_sender, :app_domain, :smtp_host, :smtp_port, :smtp_username, :smtp_password)
|
||||
|
||||
result = {}
|
||||
result[:upgrades_mode] = permitted_params[:upgrades_mode] == "manual" ? "manual" : "auto" if permitted_params.key?(:upgrades_mode)
|
||||
result[:render_deploy_hook] = permitted_params[:render_deploy_hook] if permitted_params.key?(:render_deploy_hook)
|
||||
result[:upgrades_target] = permitted_params[:upgrades_mode] unless permitted_params[:upgrades_mode] == "manual" if permitted_params.key?(:upgrades_mode)
|
||||
result.merge!(permitted_params.slice(:email_sender, :app_domain, :smtp_host, :smtp_port, :smtp_username, :smtp_password))
|
||||
result
|
||||
params.require(:setting).permit(:render_deploy_hook, :upgrades_setting, :require_invite_for_signup, :synth_api_key)
|
||||
end
|
||||
|
||||
def verify_hosting_mode
|
||||
head :not_found unless self_hosted?
|
||||
def raise_if_not_self_hosted
|
||||
raise "Settings not available on non-self-hosted instance" unless self_hosted?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
class Settings::NotificationsController < SettingsController
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
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 ])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,39 +1,7 @@
|
||||
class Settings::ProfilesController < SettingsController
|
||||
def show
|
||||
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
|
||||
logout
|
||||
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 ])
|
||||
@user = Current.user
|
||||
@users = Current.family.users.order(:created_at)
|
||||
@pending_invitations = Current.family.invitations.pending
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
class Settings::SecuritiesController < SettingsController
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
end
|
||||
end
|
||||
47
app/controllers/subscriptions_controller.rb
Normal file
@@ -0,0 +1,47 @@
|
||||
class SubscriptionsController < ApplicationController
|
||||
def new
|
||||
if Current.family.stripe_customer_id.blank?
|
||||
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 = stripe_client.v1.checkout.sessions.create({
|
||||
customer: Current.family.stripe_customer_id,
|
||||
line_items: [ {
|
||||
price: ENV["STRIPE_PLAN_ID"],
|
||||
quantity: 1
|
||||
} ],
|
||||
mode: "subscription",
|
||||
allow_promotion_codes: true,
|
||||
success_url: success_subscription_url + "?session_id={CHECKOUT_SESSION_ID}",
|
||||
cancel_url: settings_billing_url
|
||||
})
|
||||
|
||||
redirect_to session.url, allow_other_host: true, status: :see_other
|
||||
end
|
||||
|
||||
def show
|
||||
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,92 +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])
|
||||
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
|
||||
|
||||
def rules
|
||||
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, accounts: [], account_ids: [], categories: [], merchants: [])
|
||||
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: {})
|
||||
params.fetch(:q, {})
|
||||
.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
@@ -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
|
||||
7
app/controllers/vehicles_controller.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
class VehiclesController < ApplicationController
|
||||
include AccountableResource
|
||||
|
||||
permitted_accountable_attributes(
|
||||
:id, :make, :model, :year, :mileage_value, :mileage_unit
|
||||
)
|
||||
end
|
||||
73
app/controllers/webhooks_controller.rb
Normal file
@@ -0,0 +1,73 @@
|
||||
class WebhooksController < ApplicationController
|
||||
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"]
|
||||
client = Stripe::StripeClient.new(ENV["STRIPE_SECRET_KEY"])
|
||||
|
||||
begin
|
||||
thin_event = client.parse_thin_event(webhook_body, sig_header, ENV["STRIPE_WEBHOOK_SECRET"])
|
||||
|
||||
event = client.v1.events.retrieve(thin_event.id)
|
||||
|
||||
case event.type
|
||||
when /^customer\.subscription\./
|
||||
handle_subscription_event(event)
|
||||
when "customer.created", "customer.updated", "customer.deleted"
|
||||
handle_customer_event(event)
|
||||
else
|
||||
Rails.logger.info "Unhandled event type: #{event.type}"
|
||||
end
|
||||
|
||||
rescue JSON::ParserError
|
||||
render json: { error: "Invalid payload" }, status: :bad_request
|
||||
return
|
||||
rescue Stripe::SignatureVerificationError
|
||||
render json: { error: "Invalid signature" }, status: :bad_request
|
||||
return
|
||||
end
|
||||
|
||||
render json: { received: true }, status: :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def handle_subscription_event(event)
|
||||
subscription = event.data.object
|
||||
family = Family.find_by(stripe_customer_id: subscription.customer)
|
||||
|
||||
if family
|
||||
family.update(
|
||||
stripe_plan_id: subscription.plan.id,
|
||||
stripe_subscription_status: subscription.status
|
||||
)
|
||||
else
|
||||
Rails.logger.error "Family not found for Stripe customer ID: #{subscription.customer}"
|
||||
end
|
||||
end
|
||||
|
||||
def handle_customer_event(event)
|
||||
customer = event.data.object
|
||||
family = Family.find_by(stripe_customer_id: customer.id)
|
||||
|
||||
if family
|
||||
family.update(stripe_customer_id: customer.id)
|
||||
else
|
||||
Rails.logger.error "Family not found for Stripe customer ID: #{customer.id}"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -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,32 @@
|
||||
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
|
||||
end
|
||||
|
||||
def to_accountable_title(accountable)
|
||||
accountable.model_name.human
|
||||
end
|
||||
@@ -23,24 +51,9 @@ module AccountsHelper
|
||||
class_mapping(accountable_type)[:hex]
|
||||
end
|
||||
|
||||
def account_tabs(account)
|
||||
holdings_tab = { key: "holdings", label: t("accounts.show.holdings"), path: account_path(account, tab: "holdings"), content_path: account_holdings_path(account) }
|
||||
cash_tab = { key: "cash", label: t("accounts.show.cash"), path: account_path(account, tab: "cash"), content_path: account_cashes_path(account) }
|
||||
value_tab = { key: "valuations", label: t("accounts.show.value"), path: account_path(account, tab: "valuations"), content_path: account_valuations_path(account) }
|
||||
transactions_tab = { key: "transactions", label: t("accounts.show.transactions"), path: account_path(account, tab: "transactions"), content_path: account_transactions_path(account) }
|
||||
trades_tab = { key: "trades", label: t("accounts.show.trades"), path: account_path(account, tab: "trades"), content_path: account_trades_path(account) }
|
||||
|
||||
return [ holdings_tab, cash_tab, trades_tab ] if account.investment?
|
||||
|
||||
[ 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
|
||||
def account_groups(period: nil)
|
||||
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,14 +62,14 @@ 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 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
|
||||
def disclosure(title, &block)
|
||||
content = capture &block
|
||||
render partial: "shared/disclosure", locals: { title: title, content: content }
|
||||
end
|
||||
|
||||
def sidebar_link_to(name, path, options = {})
|
||||
@@ -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,38 +123,32 @@ 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
|
||||
|
||||
def format_money(number_or_money, options = {})
|
||||
return nil unless number_or_money
|
||||
|
||||
money = Money.new(number_or_money)
|
||||
options.reverse_merge!(money.default_format_options)
|
||||
options.reverse_merge!(money.format_options(I18n.locale))
|
||||
number_to_currency(money.amount, options)
|
||||
end
|
||||
|
||||
def format_money_without_symbol(number_or_money, options = {})
|
||||
return nil unless number_or_money
|
||||
|
||||
money = Money.new(number_or_money)
|
||||
options.reverse_merge!(money.default_format_options)
|
||||
options.reverse_merge!(money.format_options(I18n.locale))
|
||||
ActiveSupport::NumberHelper.number_to_delimited(money.amount.round(options[:precision] || 0), { delimiter: options[:delimiter], separator: options[:separator] })
|
||||
end
|
||||
|
||||
@@ -154,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
|
||||
|
||||