mirror of
https://github.com/we-promise/sure.git
synced 2026-05-29 15:34:58 +00:00
feat(i18n): extract hardcoded English strings to locale files (#1806)
* Extract hardcoded strings to i18n
Replace numerous hardcoded English strings with I18n lookups (t / I18n.t) across controllers, views, helpers, and components, and convert model validation error messages to symbol keys. Added multiple locale files under config/locales for models and views. This centralizes user-facing notices/alerts, UI text, import/validation messages, and prepares the app for localization and easier translation maintenance.
* Update en.yml
* Update preview-cleanup.yml
* Revert "Update preview-cleanup.yml"
This reverts commit 1ba6d3c34c.
* test: align i18n assertions with translated messages
* Standardize balance error key and tweak locales
Replace SophtronAccount's :requires_balance error key with :no_balance and update related locale strings for sophtron, plaid, and simplefin accounts to use the new key and clearer copy. Also switch the QIF upload redirect notice to use a relative translation key (t('.qif_uploaded')), remove an unused SSO providers help line, and fix a trailing-newline/whitespace issue in the subscriptions locale. These changes standardize validation keys and improve translation consistency and messaging.
---------
Co-authored-by: KiloClaw <kiloclaw@openclaw.ai>
This commit is contained in:
@@ -21,7 +21,7 @@
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium privacy-sensitive"><%= end_balance_money.format %></span>
|
||||
<%= render DS::Tooltip.new(text: "The end of day balance, after all transactions and adjustments", placement: "left", size: "sm") %>
|
||||
<%= render DS::Tooltip.new(text: t(".balance_tooltip"), placement: "left", size: "sm") %>
|
||||
</div>
|
||||
<%= helpers.icon "chevron-down", class: "group-open:rotate-180" %>
|
||||
</div>
|
||||
@@ -32,7 +32,7 @@
|
||||
<% if balance %>
|
||||
<%= render UI::Account::BalanceReconciliation.new(balance: balance, account: account) %>
|
||||
<% else %>
|
||||
<p class="text-sm text-secondary">No balance data available for this date</p>
|
||||
<p class="text-sm text-secondary"><%= t(".no_balance_data") %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
data-time-series-chart-data-value="<%= series.to_json %>"></div>
|
||||
<% else %>
|
||||
<div class="w-full h-full flex items-center justify-center">
|
||||
<p class="text-secondary text-sm">No data available</p>
|
||||
<p class="text-secondary text-sm"><%= t(".no_data_available") %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -58,7 +58,7 @@ class CategoriesController < ApplicationController
|
||||
|
||||
def destroy_all
|
||||
Current.family.categories.destroy_all
|
||||
redirect_back_or_to categories_path, notice: "All categories deleted"
|
||||
redirect_back_or_to categories_path, notice: t(".success")
|
||||
end
|
||||
|
||||
def bootstrap
|
||||
|
||||
@@ -29,7 +29,7 @@ class ChatsController < ApplicationController
|
||||
@chat.update!(chat_params)
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_back_or_to chat_path(@chat), notice: "Chat updated" }
|
||||
format.html { redirect_back_or_to chat_path(@chat), notice: t(".success") }
|
||||
format.turbo_stream { render turbo_stream: turbo_stream.replace(dom_id(@chat, :title), partial: "chats/chat_title", locals: { chat: @chat }) }
|
||||
end
|
||||
end
|
||||
@@ -38,7 +38,7 @@ class ChatsController < ApplicationController
|
||||
@chat.destroy
|
||||
clear_last_viewed_chat
|
||||
|
||||
redirect_to chats_path, notice: "Chat was successfully deleted"
|
||||
redirect_to chats_path, notice: t(".notice")
|
||||
end
|
||||
|
||||
def retry
|
||||
|
||||
@@ -23,7 +23,7 @@ module SelfHostable
|
||||
if controller_name == "pages" && action_name == "redis_configuration_error"
|
||||
# If Redis is now working, redirect to home
|
||||
if redis_connected?
|
||||
redirect_to root_path, notice: "Redis is now configured properly! You can now setup your Sure application."
|
||||
redirect_to root_path, notice: t("concerns.self_hostable.redis_configured")
|
||||
end
|
||||
|
||||
return
|
||||
|
||||
@@ -42,7 +42,7 @@ class HoldingsController < ApplicationController
|
||||
@holding.destroy_holding_and_entries!
|
||||
flash[:notice] = t(".success")
|
||||
else
|
||||
flash[:alert] = "You cannot delete this holding"
|
||||
flash[:alert] = t(".cannot_delete")
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
|
||||
@@ -6,7 +6,7 @@ class Import::CleansController < ApplicationController
|
||||
def show
|
||||
unless @import.configured?
|
||||
redirect_path = @import.is_a?(PdfImport) ? import_path(@import) : import_configuration_path(@import)
|
||||
return redirect_to redirect_path, alert: "Please configure your import before proceeding."
|
||||
return redirect_to redirect_path, alert: t(".not_configured")
|
||||
end
|
||||
|
||||
rows = @import.rows_ordered
|
||||
|
||||
@@ -8,7 +8,7 @@ class Import::ConfirmsController < ApplicationController
|
||||
return redirect_to import_path(@import)
|
||||
end
|
||||
|
||||
redirect_to import_clean_path(@import), alert: "You have invalid data, please edit until all errors are resolved" unless @import.cleaned?
|
||||
redirect_to import_clean_path(@import), alert: t(".invalid_data") unless @import.cleaned?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -54,7 +54,7 @@ class Import::QifCategorySelectionsController < ApplicationController
|
||||
@import.sync_mappings unless format_changed
|
||||
end
|
||||
|
||||
redirect_to import_clean_path(@import), notice: "Categories and tags saved."
|
||||
redirect_to import_clean_path(@import), notice: t(".success")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -85,7 +85,7 @@ class Import::UploadsController < ApplicationController
|
||||
@import.sync_mappings
|
||||
end
|
||||
|
||||
redirect_to import_qif_category_selection_path(@import), notice: "QIF file uploaded successfully."
|
||||
redirect_to import_qif_category_selection_path(@import), notice: t(".qif_uploaded")
|
||||
end
|
||||
|
||||
def csv_str
|
||||
|
||||
@@ -22,9 +22,9 @@ class ImportsController < ApplicationController
|
||||
def publish
|
||||
@import.publish_later
|
||||
|
||||
redirect_to import_path(@import), notice: "Your import has started in the background."
|
||||
redirect_to import_path(@import), notice: t(".started")
|
||||
rescue Import::MaxRowCountExceededError
|
||||
redirect_back_or_to import_path(@import), alert: "Your import exceeds the maximum row count of #{@import.max_row_count}."
|
||||
redirect_back_or_to import_path(@import), alert: t(".max_rows_exceeded", max: @import.max_row_count)
|
||||
end
|
||||
|
||||
def index
|
||||
@@ -112,22 +112,22 @@ class ImportsController < ApplicationController
|
||||
|
||||
def revert
|
||||
@import.revert_later
|
||||
redirect_to imports_path, notice: "Import is reverting in the background."
|
||||
redirect_to imports_path, notice: t(".started")
|
||||
end
|
||||
|
||||
def apply_template
|
||||
if @import.suggested_template
|
||||
@import.apply_template!(@import.suggested_template)
|
||||
redirect_to import_configuration_path(@import), notice: "Template applied."
|
||||
redirect_to import_configuration_path(@import), notice: t(".template_applied")
|
||||
else
|
||||
redirect_to import_configuration_path(@import), alert: "No template found, please manually configure your import."
|
||||
redirect_to import_configuration_path(@import), alert: t(".no_template_found")
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@import.destroy
|
||||
|
||||
redirect_to imports_path, notice: "Your import has been deleted."
|
||||
redirect_to imports_path, notice: t(".deleted")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -8,13 +8,13 @@ class InviteCodesController < ApplicationController
|
||||
|
||||
def create
|
||||
InviteCode.generate!
|
||||
redirect_back_or_to invite_codes_path, notice: "Code generated"
|
||||
redirect_back_or_to invite_codes_path, notice: t(".success")
|
||||
end
|
||||
|
||||
def destroy
|
||||
code = InviteCode.find(params[:id])
|
||||
code.destroy
|
||||
redirect_back_or_to invite_codes_path, notice: "Code deleted"
|
||||
redirect_back_or_to invite_codes_path, notice: t(".success")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -7,7 +7,7 @@ class OidcAccountsController < ApplicationController
|
||||
@pending_auth = session[:pending_oidc_auth]
|
||||
|
||||
if @pending_auth.nil?
|
||||
redirect_to new_session_path, alert: "No pending OIDC authentication found"
|
||||
redirect_to new_session_path, alert: t(".no_pending_oidc")
|
||||
return
|
||||
end
|
||||
|
||||
@@ -26,7 +26,7 @@ class OidcAccountsController < ApplicationController
|
||||
@pending_auth = session[:pending_oidc_auth]
|
||||
|
||||
if @pending_auth.nil?
|
||||
redirect_to new_session_path, alert: "No pending OIDC authentication found"
|
||||
redirect_to new_session_path, alert: t(".no_pending_oidc")
|
||||
return
|
||||
end
|
||||
|
||||
@@ -75,7 +75,7 @@ class OidcAccountsController < ApplicationController
|
||||
@pending_auth = session[:pending_oidc_auth]
|
||||
|
||||
if @pending_auth.nil?
|
||||
redirect_to new_session_path, alert: "No pending OIDC authentication found"
|
||||
redirect_to new_session_path, alert: t(".no_pending_oidc")
|
||||
return
|
||||
end
|
||||
|
||||
@@ -91,7 +91,7 @@ class OidcAccountsController < ApplicationController
|
||||
@pending_auth = session[:pending_oidc_auth]
|
||||
|
||||
if @pending_auth.nil?
|
||||
redirect_to new_session_path, alert: "No pending OIDC authentication found"
|
||||
redirect_to new_session_path, alert: t(".no_pending_oidc")
|
||||
return
|
||||
end
|
||||
|
||||
@@ -104,7 +104,7 @@ class OidcAccountsController < ApplicationController
|
||||
# domain is not allowed, block JIT account creation—unless there's a
|
||||
# pending invitation for this user.
|
||||
unless invitation.present? || (!AuthConfig.jit_link_only? && AuthConfig.allowed_oidc_domain?(email))
|
||||
redirect_to new_session_path, alert: "SSO account creation is disabled. Please contact an administrator."
|
||||
redirect_to new_session_path, alert: t(".account_creation_disabled")
|
||||
return
|
||||
end
|
||||
|
||||
@@ -164,7 +164,7 @@ class OidcAccountsController < ApplicationController
|
||||
elsif accept_pending_invitation_for(@user)
|
||||
t("invitations.accept_choice.joined_household")
|
||||
else
|
||||
"Welcome! Your account has been created."
|
||||
t(".account_created")
|
||||
end
|
||||
redirect_to root_path, notice: notice
|
||||
else
|
||||
|
||||
@@ -21,7 +21,7 @@ class PendingDuplicateMergesController < ApplicationController
|
||||
|
||||
# Manually merge the pending transaction with the selected posted transaction
|
||||
unless merge_params[:posted_entry_id].present?
|
||||
redirect_back_or_to transactions_path, alert: "Please select a posted transaction to merge with"
|
||||
redirect_back_or_to transactions_path, alert: t(".no_posted_selected")
|
||||
return
|
||||
end
|
||||
|
||||
@@ -29,7 +29,7 @@ class PendingDuplicateMergesController < ApplicationController
|
||||
posted_entry = find_eligible_posted_entry(merge_params[:posted_entry_id])
|
||||
|
||||
unless posted_entry
|
||||
redirect_back_or_to transactions_path, alert: "Invalid transaction selected for merge"
|
||||
redirect_back_or_to transactions_path, alert: t(".invalid_transaction")
|
||||
return
|
||||
end
|
||||
|
||||
@@ -48,9 +48,9 @@ class PendingDuplicateMergesController < ApplicationController
|
||||
|
||||
# Immediately merge
|
||||
if @transaction.merge_with_duplicate!
|
||||
redirect_back_or_to transactions_path, notice: "Pending transaction merged with posted transaction"
|
||||
redirect_back_or_to transactions_path, notice: t(".merge_success")
|
||||
else
|
||||
redirect_back_or_to transactions_path, alert: "Could not merge transactions"
|
||||
redirect_back_or_to transactions_path, alert: t(".merge_failed")
|
||||
end
|
||||
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotDestroyed,
|
||||
ActiveRecord::Deadlocked, ActiveRecord::LockWaitTimeout => e
|
||||
@@ -64,7 +64,7 @@ class PendingDuplicateMergesController < ApplicationController
|
||||
@transaction = entry.entryable
|
||||
|
||||
unless @transaction.is_a?(Transaction) && @transaction.pending?
|
||||
redirect_to transactions_path, alert: "This feature is only available for pending transactions"
|
||||
redirect_to transactions_path, alert: t("pending_duplicate_merges.set_transaction.pending_only")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ class PlaidItemsController < ApplicationController
|
||||
.select { |pa| pa.account_provider.nil? && pa.account.nil? } # Not linked via new or legacy system
|
||||
|
||||
if @available_plaid_accounts.empty?
|
||||
redirect_to account_path(@account), alert: "No available Plaid accounts to link. Please connect a new Plaid account first."
|
||||
redirect_to account_path(@account), alert: t(".no_available_accounts")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -72,13 +72,13 @@ class PlaidItemsController < ApplicationController
|
||||
|
||||
# Verify the Plaid account belongs to this family's Plaid items
|
||||
unless Current.family.plaid_items.include?(plaid_account.plaid_item)
|
||||
redirect_to account_path(@account), alert: "Invalid Plaid account selected"
|
||||
redirect_to account_path(@account), alert: t(".invalid_account")
|
||||
return
|
||||
end
|
||||
|
||||
# Verify the Plaid account is not already linked
|
||||
if plaid_account.account_provider.present? || plaid_account.account.present?
|
||||
redirect_to account_path(@account), alert: "This Plaid account is already linked"
|
||||
redirect_to account_path(@account), alert: t(".already_linked")
|
||||
return
|
||||
end
|
||||
|
||||
@@ -88,7 +88,7 @@ class PlaidItemsController < ApplicationController
|
||||
provider: plaid_account
|
||||
)
|
||||
|
||||
redirect_to accounts_path, notice: "Account successfully linked to Plaid"
|
||||
redirect_to accounts_path, notice: t(".success")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -86,8 +86,8 @@ class RulesController < ApplicationController
|
||||
def update
|
||||
if @rule.update(rule_params)
|
||||
respond_to do |format|
|
||||
format.html { redirect_back_or_to rules_path, notice: "Rule updated" }
|
||||
format.turbo_stream { stream_redirect_back_or_to rules_path, notice: "Rule updated" }
|
||||
format.html { redirect_back_or_to rules_path, notice: t(".success") }
|
||||
format.turbo_stream { stream_redirect_back_or_to rules_path, notice: t(".success") }
|
||||
end
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
@@ -96,12 +96,12 @@ class RulesController < ApplicationController
|
||||
|
||||
def destroy
|
||||
@rule.destroy
|
||||
redirect_to rules_path, notice: "Rule deleted"
|
||||
redirect_to rules_path, notice: t(".success")
|
||||
end
|
||||
|
||||
def destroy_all
|
||||
Current.family.rules.destroy_all
|
||||
redirect_to rules_path, notice: "All rules deleted"
|
||||
redirect_to rules_path, notice: t(".success")
|
||||
end
|
||||
|
||||
def confirm_all
|
||||
|
||||
@@ -31,7 +31,7 @@ class Settings::ApiKeysController < ApplicationController
|
||||
existing_keys.each { |key| key.update_column(:revoked_at, Time.current) }
|
||||
|
||||
if @api_key.save
|
||||
flash[:notice] = "Your API key has been created successfully"
|
||||
flash[:notice] = t(".success")
|
||||
redirect_to settings_api_key_path
|
||||
else
|
||||
# Restore existing keys if new key creation failed
|
||||
@@ -42,13 +42,13 @@ class Settings::ApiKeysController < ApplicationController
|
||||
|
||||
def destroy
|
||||
if @api_key.nil?
|
||||
flash[:alert] = "API key not found"
|
||||
flash[:alert] = t(".not_found")
|
||||
elsif @api_key.demo_monitoring_key?
|
||||
flash[:alert] = "This API key cannot be revoked"
|
||||
flash[:alert] = t(".cannot_revoke")
|
||||
elsif @api_key.revoke!
|
||||
flash[:notice] = "API key has been revoked successfully"
|
||||
flash[:notice] = t(".revoked_successfully")
|
||||
else
|
||||
flash[:alert] = "Failed to revoke API key"
|
||||
flash[:alert] = t(".revoke_failed")
|
||||
end
|
||||
redirect_to settings_api_key_path
|
||||
end
|
||||
|
||||
@@ -29,9 +29,9 @@ class Settings::ProfilesController < ApplicationController
|
||||
if @user.destroy
|
||||
# Also destroy the invitation associated with this user for this family
|
||||
Current.family.invitations.find_by(email: @user.email)&.destroy
|
||||
flash[:notice] = "Member removed successfully."
|
||||
flash[:notice] = t(".member_removed")
|
||||
else
|
||||
flash[:alert] = "Failed to remove member."
|
||||
flash[:alert] = t(".member_removal_failed")
|
||||
end
|
||||
|
||||
redirect_to settings_profile_path
|
||||
|
||||
@@ -66,9 +66,9 @@ class Settings::ProvidersController < ApplicationController
|
||||
# Reload provider configurations if needed
|
||||
reload_provider_configs(updated_fields)
|
||||
|
||||
redirect_to settings_providers_path, notice: "Provider settings updated successfully"
|
||||
redirect_to settings_providers_path, notice: t(".updated_successfully")
|
||||
else
|
||||
redirect_to settings_providers_path, notice: "No changes were made"
|
||||
redirect_to settings_providers_path, notice: t(".no_changes")
|
||||
end
|
||||
rescue => error
|
||||
Rails.logger.error("Failed to update provider settings: #{error.class} - #{error.message}")
|
||||
|
||||
@@ -380,7 +380,7 @@ class SnaptradeItemsController < ApplicationController
|
||||
end
|
||||
|
||||
def link_accounts
|
||||
redirect_to settings_providers_path, alert: "Use the account setup flow instead"
|
||||
redirect_to settings_providers_path, alert: t(".use_setup_flow")
|
||||
end
|
||||
|
||||
def select_existing_account
|
||||
|
||||
@@ -8,7 +8,7 @@ class SubscriptionsController < ApplicationController
|
||||
# Upgrade page for unsubscribed users
|
||||
def upgrade
|
||||
if Current.family.subscription&.active?
|
||||
redirect_to root_path, notice: "You are already contributing. Thank you!"
|
||||
redirect_to root_path, notice: t(".already_contributing")
|
||||
else
|
||||
@plan = params[:plan] || "annual"
|
||||
render layout: "onboardings"
|
||||
@@ -33,9 +33,9 @@ class SubscriptionsController < ApplicationController
|
||||
def create
|
||||
if Current.family.can_start_trial?
|
||||
Current.family.start_trial_subscription!
|
||||
redirect_to root_path, notice: "Welcome to Sure!"
|
||||
redirect_to root_path, notice: t(".welcome")
|
||||
else
|
||||
redirect_to root_path, alert: "You have already started or completed a trial. Please upgrade to continue."
|
||||
redirect_to root_path, alert: t(".trial_already_used")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -54,9 +54,9 @@ class SubscriptionsController < ApplicationController
|
||||
|
||||
if checkout_result.success?
|
||||
Current.family.start_subscription!(checkout_result.subscription_id)
|
||||
redirect_to root_path, notice: "Welcome to Sure! Your contribution is appreciated."
|
||||
redirect_to root_path, notice: t(".welcome_with_contribution")
|
||||
else
|
||||
redirect_to root_path, alert: "Something went wrong processing your contribution. Please try again."
|
||||
redirect_to root_path, alert: t(".contribution_failed")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class TagsController < ApplicationController
|
||||
|
||||
def destroy_all
|
||||
Current.family.tags.destroy_all
|
||||
redirect_back_or_to tags_path, notice: "All tags deleted"
|
||||
redirect_back_or_to tags_path, notice: t(".all_deleted")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -107,7 +107,7 @@ class TransactionsController < ApplicationController
|
||||
@entry.mark_user_modified!
|
||||
@entry.transaction.lock_attr!(:tag_ids) if @entry.transaction.tags.any?
|
||||
|
||||
flash[:notice] = "Transaction created"
|
||||
flash[:notice] = t(".created")
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_back_or_to account_path(@entry.account) }
|
||||
@@ -141,7 +141,7 @@ class TransactionsController < ApplicationController
|
||||
@entry.reload
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_back_or_to account_path(@entry.account), notice: "Transaction updated" }
|
||||
format.html { redirect_back_or_to account_path(@entry.account), notice: t(".updated") }
|
||||
format.turbo_stream do
|
||||
in_split_group = helpers.in_split_group?(@entry, params[:grouped])
|
||||
render turbo_stream: [
|
||||
|
||||
@@ -32,7 +32,7 @@ class TransferMatchesController < ApplicationController
|
||||
|
||||
@transfer.sync_account_later
|
||||
|
||||
redirect_back_or_to transactions_path, notice: "Transfer created"
|
||||
redirect_back_or_to transactions_path, notice: t(".success")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -6,7 +6,7 @@ class UsersController < ApplicationController
|
||||
if @user.resend_confirmation_email
|
||||
redirect_to settings_profile_path, notice: t(".success")
|
||||
else
|
||||
redirect_to settings_profile_path, alert: t("no_pending_change")
|
||||
redirect_to settings_profile_path, alert: t(".no_pending_change")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -57,8 +57,8 @@ class ValuationsController < ApplicationController
|
||||
|
||||
if result.success?
|
||||
respond_to do |format|
|
||||
format.html { redirect_back_or_to account_path(account), notice: "Account updated" }
|
||||
format.turbo_stream { stream_redirect_back_or_to(account_path(account), notice: "Account updated") }
|
||||
format.html { redirect_back_or_to account_path(account), notice: t(".account_updated") }
|
||||
format.turbo_stream { stream_redirect_back_or_to(account_path(account), notice: t(".account_updated")) }
|
||||
end
|
||||
else
|
||||
@error_message = result.error_message
|
||||
@@ -84,7 +84,7 @@ class ValuationsController < ApplicationController
|
||||
@entry.reload
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_back_or_to account_path(@entry.account), notice: "Entry updated" }
|
||||
format.html { redirect_back_or_to account_path(@entry.account), notice: t(".entry_updated") }
|
||||
format.turbo_stream do
|
||||
render turbo_stream: [
|
||||
turbo_stream.replace(
|
||||
|
||||
@@ -74,7 +74,7 @@ module ApplicationHelper
|
||||
|
||||
|
||||
def family_moniker
|
||||
Current.family&.moniker_label || "Family"
|
||||
Current.family&.moniker_label || I18n.t("shared.family_moniker.singular")
|
||||
end
|
||||
|
||||
def family_moniker_downcase
|
||||
@@ -82,7 +82,7 @@ module ApplicationHelper
|
||||
end
|
||||
|
||||
def family_moniker_plural
|
||||
Current.family&.moniker_label_plural || "Families"
|
||||
Current.family&.moniker_label_plural || I18n.t("shared.family_moniker.plural")
|
||||
end
|
||||
|
||||
def family_moniker_plural_downcase
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
module CategoriesHelper
|
||||
def transfer_category
|
||||
Category.new \
|
||||
name: "Transfer",
|
||||
name: I18n.t("categories.virtual.transfer"),
|
||||
color: Category::TRANSFER_COLOR,
|
||||
lucide_icon: "arrow-right-left"
|
||||
end
|
||||
|
||||
def payment_category
|
||||
Category.new \
|
||||
name: "Payment",
|
||||
name: I18n.t("categories.virtual.payment"),
|
||||
color: Category::PAYMENT_COLOR,
|
||||
lucide_icon: "arrow-right"
|
||||
end
|
||||
|
||||
def trade_category
|
||||
Category.new \
|
||||
name: "Trade",
|
||||
name: I18n.t("categories.virtual.trade"),
|
||||
color: Category::TRADE_COLOR
|
||||
end
|
||||
|
||||
|
||||
@@ -38,14 +38,14 @@ class CustomConfirm
|
||||
end
|
||||
|
||||
def default_title
|
||||
"Are you sure?"
|
||||
I18n.t("shared.custom_confirm.default_title")
|
||||
end
|
||||
|
||||
def default_body
|
||||
"This is not reversible."
|
||||
I18n.t("shared.custom_confirm.default_body")
|
||||
end
|
||||
|
||||
def default_btn_text
|
||||
"Confirm"
|
||||
I18n.t("shared.custom_confirm.default_btn_text")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
module ImportsHelper
|
||||
def mapping_label(mapping_class)
|
||||
{
|
||||
"Import::AccountTypeMapping" => "Account Type",
|
||||
"Import::AccountMapping" => "Account",
|
||||
"Import::CategoryMapping" => "Category",
|
||||
"Import::TagMapping" => "Tag"
|
||||
"Import::AccountTypeMapping" => I18n.t("imports.mapping_labels.account_type"),
|
||||
"Import::AccountMapping" => I18n.t("imports.mapping_labels.account"),
|
||||
"Import::CategoryMapping" => I18n.t("imports.mapping_labels.category"),
|
||||
"Import::TagMapping" => I18n.t("imports.mapping_labels.tag")
|
||||
}.fetch(mapping_class.name)
|
||||
end
|
||||
|
||||
def import_col_label(key)
|
||||
{
|
||||
date: "Date",
|
||||
amount: "Amount",
|
||||
name: "Name",
|
||||
currency: "Currency",
|
||||
category: "Category",
|
||||
tags: "Tags",
|
||||
account: "Account",
|
||||
notes: "Notes",
|
||||
qty: "Quantity",
|
||||
ticker: "Ticker",
|
||||
exchange: "Exchange",
|
||||
price: "Price",
|
||||
entity_type: "Type",
|
||||
category_parent: "Parent category",
|
||||
category_color: "Color",
|
||||
category_icon: "Lucide icon"
|
||||
date: I18n.t("imports.column_labels.date"),
|
||||
amount: I18n.t("imports.column_labels.amount"),
|
||||
name: I18n.t("imports.column_labels.name"),
|
||||
currency: I18n.t("imports.column_labels.currency"),
|
||||
category: I18n.t("imports.column_labels.category"),
|
||||
tags: I18n.t("imports.column_labels.tags"),
|
||||
account: I18n.t("imports.column_labels.account"),
|
||||
notes: I18n.t("imports.column_labels.notes"),
|
||||
qty: I18n.t("imports.column_labels.qty"),
|
||||
ticker: I18n.t("imports.column_labels.ticker"),
|
||||
exchange: I18n.t("imports.column_labels.exchange"),
|
||||
price: I18n.t("imports.column_labels.price"),
|
||||
entity_type: I18n.t("imports.column_labels.entity_type"),
|
||||
category_parent: I18n.t("imports.column_labels.category_parent"),
|
||||
category_color: I18n.t("imports.column_labels.category_color"),
|
||||
category_icon: I18n.t("imports.column_labels.category_icon")
|
||||
}[key]
|
||||
end
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ class ApiKey < ApplicationRecord
|
||||
def prevent_demo_monitoring_key_destroy!
|
||||
return unless demo_monitoring_key?
|
||||
|
||||
errors.add(:base, "Cannot destroy demo monitoring API key")
|
||||
errors.add(:base, :cannot_destroy_demo_key)
|
||||
throw(:abort)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -17,7 +17,7 @@ class CategoryImport < Import
|
||||
parent = ensure_placeholder_category(row.category_parent)
|
||||
|
||||
if parent && parent == category
|
||||
errors.add(:base, "Category '#{category.name}' cannot be its own parent")
|
||||
errors.add(:base, :own_parent, name: category.name)
|
||||
raise ActiveRecord::RecordInvalid.new(self)
|
||||
end
|
||||
|
||||
@@ -82,7 +82,7 @@ class CategoryImport < Import
|
||||
missing_headers = required_column_keys.map(&:to_s).reject { |key| header_for(key).present? }
|
||||
return if missing_headers.empty?
|
||||
|
||||
errors.add(:base, "Missing required columns: #{missing_headers.join(', ')}")
|
||||
errors.add(:base, :missing_columns, columns: missing_headers.join(", "))
|
||||
raise ActiveRecord::RecordInvalid.new(self)
|
||||
end
|
||||
|
||||
|
||||
@@ -418,7 +418,7 @@ class Import < ApplicationRecord
|
||||
end
|
||||
|
||||
if duplicate_headers.any?
|
||||
errors.add(:base, "CSV headers normalize to duplicate columns: #{duplicate_headers.map { |headers| headers.join(', ') }.join('; ')}")
|
||||
errors.add(:base, :duplicate_headers, columns: duplicate_headers.map { |headers| headers.join(", ") }.join("; "))
|
||||
raise ActiveRecord::RecordInvalid, self
|
||||
end
|
||||
|
||||
|
||||
@@ -176,6 +176,6 @@ class IndexaCapitalItem < ApplicationRecord
|
||||
def credentials_present_on_create
|
||||
return if credentials_configured?
|
||||
|
||||
errors.add(:base, "Either INDEXA_API_TOKEN env var or username/document/password credentials are required")
|
||||
errors.add(:base, :credentials_required)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -70,6 +70,6 @@ class PlaidAccount < ApplicationRecord
|
||||
# Plaid guarantees at least one of these. This validation is a sanity check for that guarantee.
|
||||
def has_balance
|
||||
return if current_balance.present? || available_balance.present?
|
||||
errors.add(:base, "Plaid account must have either current or available balance")
|
||||
errors.add(:base, :no_balance)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -24,7 +24,7 @@ class RecurringTransaction < ApplicationRecord
|
||||
|
||||
def merchant_or_name_present
|
||||
if merchant_id.blank? && name.blank?
|
||||
errors.add(:base, "Either merchant or name must be present")
|
||||
errors.add(:base, :merchant_or_name_required)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -140,14 +140,14 @@ class Rule < ApplicationRecord
|
||||
return if new_record? && !actions.empty?
|
||||
|
||||
if actions.reject(&:marked_for_destruction?).empty?
|
||||
errors.add(:base, "must have at least one action")
|
||||
errors.add(:base, :min_actions)
|
||||
end
|
||||
end
|
||||
|
||||
def no_duplicate_actions
|
||||
action_types = actions.reject(&:marked_for_destruction?).map(&:action_type)
|
||||
|
||||
errors.add(:base, "Rule cannot have duplicate actions #{action_types.inspect}") if action_types.uniq.count != action_types.count
|
||||
errors.add(:base, :duplicate_actions, types: action_types.inspect) if action_types.uniq.count != action_types.count
|
||||
end
|
||||
|
||||
# Validation: To keep rules simple and easy to understand, we don't allow nested compound conditions.
|
||||
@@ -157,7 +157,7 @@ class Rule < ApplicationRecord
|
||||
conditions.each do |condition|
|
||||
if condition.compound?
|
||||
if condition.sub_conditions.any? { |sub_condition| sub_condition.compound? }
|
||||
errors.add(:base, "Compound conditions cannot be nested")
|
||||
errors.add(:base, :nested_conditions)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -115,7 +115,7 @@ class RuleImport < Import
|
||||
|
||||
# Validate resource type
|
||||
unless resource_type == "transaction"
|
||||
errors.add(:base, "Unsupported resource type: #{resource_type}")
|
||||
errors.add(:base, :unsupported_resource_type, resource_type: resource_type)
|
||||
raise ActiveRecord::RecordInvalid.new(self)
|
||||
end
|
||||
|
||||
@@ -124,13 +124,13 @@ class RuleImport < Import
|
||||
conditions_data = parse_json_safely(row.conditions, "conditions")
|
||||
actions_data = parse_json_safely(row.actions, "actions")
|
||||
rescue JSON::ParserError => e
|
||||
errors.add(:base, "Invalid JSON in conditions or actions: #{e.message}")
|
||||
errors.add(:base, :invalid_json, message: e.message)
|
||||
raise ActiveRecord::RecordInvalid.new(self)
|
||||
end
|
||||
|
||||
# Validate we have at least one action
|
||||
if actions_data.empty?
|
||||
errors.add(:base, "Rule must have at least one action")
|
||||
errors.add(:base, :min_actions)
|
||||
raise ActiveRecord::RecordInvalid.new(self)
|
||||
end
|
||||
|
||||
|
||||
@@ -137,6 +137,6 @@ class SimplefinAccount < ApplicationRecord
|
||||
end
|
||||
def has_balance
|
||||
return if current_balance.present? || available_balance.present?
|
||||
errors.add(:base, "SimpleFin account must have either current or available balance")
|
||||
errors.add(:base, :no_balance)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -158,7 +158,7 @@ class SophtronAccount < ApplicationRecord
|
||||
end
|
||||
def has_balance
|
||||
return if balance.present? || available_balance.present?
|
||||
errors.add(:base, "Sophtron account must have either current or available balance")
|
||||
errors.add(:base, :no_balance)
|
||||
end
|
||||
|
||||
def first_present(hash, *keys)
|
||||
|
||||
@@ -90,7 +90,7 @@ class SsoProvider < ApplicationRecord
|
||||
idp_sso_url = settings&.dig("idp_sso_url")
|
||||
|
||||
if idp_metadata_url.blank? && idp_sso_url.blank?
|
||||
errors.add(:settings, "Either IdP Metadata URL or IdP SSO URL is required for SAML providers")
|
||||
errors.add(:settings, :saml_url_required)
|
||||
end
|
||||
|
||||
# If using manual config, require certificate
|
||||
@@ -99,17 +99,17 @@ class SsoProvider < ApplicationRecord
|
||||
idp_fingerprint = settings&.dig("idp_cert_fingerprint")
|
||||
|
||||
if idp_cert.blank? && idp_fingerprint.blank?
|
||||
errors.add(:settings, "Either IdP Certificate or Certificate Fingerprint is required when not using metadata URL")
|
||||
errors.add(:settings, :saml_cert_required)
|
||||
end
|
||||
end
|
||||
|
||||
# Validate URL formats if provided
|
||||
if idp_metadata_url.present? && !valid_url?(idp_metadata_url)
|
||||
errors.add(:settings, "IdP Metadata URL must be a valid URL")
|
||||
errors.add(:settings, :metadata_url_invalid)
|
||||
end
|
||||
|
||||
if idp_sso_url.present? && !valid_url?(idp_sso_url)
|
||||
errors.add(:settings, "IdP SSO URL must be a valid URL")
|
||||
errors.add(:settings, :sso_url_invalid)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -107,12 +107,12 @@ class Transfer < ApplicationRecord
|
||||
private
|
||||
def transfer_has_different_accounts
|
||||
return unless inflow_transaction&.entry && outflow_transaction&.entry
|
||||
errors.add(:base, "Must be from different accounts") if to_account == from_account
|
||||
errors.add(:base, :different_accounts) if to_account == from_account
|
||||
end
|
||||
|
||||
def transfer_has_same_family
|
||||
return unless inflow_transaction&.entry && outflow_transaction&.entry
|
||||
errors.add(:base, "Must be from same family") unless to_account&.family == from_account&.family
|
||||
errors.add(:base, :same_family) unless to_account&.family == from_account&.family
|
||||
end
|
||||
|
||||
def transfer_has_opposite_amounts
|
||||
@@ -126,10 +126,10 @@ class Transfer < ApplicationRecord
|
||||
|
||||
if inflow_entry.currency == outflow_entry.currency
|
||||
# For same currency, amounts must be exactly opposite
|
||||
errors.add(:base, "Must have opposite amounts") if inflow_amount + outflow_amount != 0
|
||||
errors.add(:base, :opposite_amounts) if inflow_amount + outflow_amount != 0
|
||||
else
|
||||
# For different currencies, just check the signs are opposite
|
||||
errors.add(:base, "Must have opposite amounts") unless inflow_amount.negative? && outflow_amount.positive?
|
||||
errors.add(:base, :opposite_amounts) unless inflow_amount.negative? && outflow_amount.positive?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -138,6 +138,6 @@ class Transfer < ApplicationRecord
|
||||
|
||||
date_diff = (inflow_transaction.entry.date - outflow_transaction.entry.date).abs
|
||||
max_days = status == "confirmed" ? 30 : 4
|
||||
errors.add(:base, "Must be within #{max_days} days") if date_diff > max_days
|
||||
errors.add(:base, :within_days, count: max_days) if date_diff > max_days
|
||||
end
|
||||
end
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
|
||||
<% if account.draft? %>
|
||||
<%= render DS::Link.new(
|
||||
text: "Complete setup",
|
||||
text: t(".complete_setup"),
|
||||
href: edit_account_path(account, return_to: return_to),
|
||||
variant: :outline,
|
||||
frame: :modal
|
||||
|
||||
@@ -29,13 +29,13 @@
|
||||
<div class="border-t border-alpha-black-25 px-4 pt-4 text-secondary text-sm justify-between hidden md:flex">
|
||||
<div class="flex space-x-5">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span>Select</span>
|
||||
<span><%= t(".select") %></span>
|
||||
<kbd class="bg-alpha-black-50 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-5 h-5 shrink-0 grow-0 items-center justify-center">
|
||||
<%= icon("corner-down-left", size: "xs") %>
|
||||
</kbd>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span>Navigate</span>
|
||||
<span><%= t(".navigate") %></span>
|
||||
<kbd class="bg-alpha-black-50 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-5 h-5 shrink-0 grow-0 items-center justify-center">
|
||||
<%= icon("arrow-up", size: "xs") %>
|
||||
</kbd>
|
||||
@@ -45,7 +45,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button data-action="DS--dialog#close">Close</button>
|
||||
<button data-action="DS--dialog#close"><%= t(".close") %></button>
|
||||
<kbd class="bg-alpha-black-50 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-8 h-5 shrink-0 grow-0 items-center justify-center text-xs">ESC</kbd>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
<% unless @account.linked? %>
|
||||
<% if @account.permission_for(Current.user).in?([ :owner, :full_control ]) %>
|
||||
<%= render DS::Menu.new(variant: "button") do |menu| %>
|
||||
<% menu.with_button(text: "New", variant: "secondary", icon: "plus") %>
|
||||
<% menu.with_button(text: t(".new"), variant: "secondary", icon: "plus") %>
|
||||
|
||||
<% menu.with_item(
|
||||
variant: "link",
|
||||
text: "New balance",
|
||||
text: t(".new_balance"),
|
||||
icon: "circle-dollar-sign",
|
||||
href: new_valuation_path(account_id: @account.id),
|
||||
data: { turbo_frame: :modal }) %>
|
||||
@@ -48,7 +48,7 @@
|
||||
<%= icon("search") %>
|
||||
<%= hidden_field_tag :account_id, @account.id %>
|
||||
<%= form.search_field :search,
|
||||
placeholder: "Search entries by name",
|
||||
placeholder: t(".search_placeholder"),
|
||||
value: @q[:search],
|
||||
class: "form-field__input placeholder:text-sm placeholder:text-secondary",
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<% end %>
|
||||
<% if account.draft? %>
|
||||
<%= render DS::Link.new(
|
||||
text: "Complete setup",
|
||||
text: t(".complete_setup"),
|
||||
href: edit_account_path(account),
|
||||
variant: :outline,
|
||||
size: :sm,
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
<% permission = account.permission_for(Current.user) %>
|
||||
<%= render DS::Menu.new(testid: "account-menu") do |menu| %>
|
||||
<% if permission.in?([ :owner, :full_control ]) %>
|
||||
<% menu.with_item(variant: "link", text: "Edit", href: edit_account_path(account), icon: "pencil-line", data: { turbo_frame: :modal }) %>
|
||||
<% menu.with_item(variant: "link", text: t(".edit"), href: edit_account_path(account), icon: "pencil-line", data: { turbo_frame: :modal }) %>
|
||||
<% end %>
|
||||
<% menu.with_item(variant: "link", text: "Sharing", href: account_sharing_path(account), icon: "users", data: { turbo_frame: :modal }) %>
|
||||
<% menu.with_item(variant: "link", text: t(".sharing"), href: account_sharing_path(account), icon: "users", data: { turbo_frame: :modal }) %>
|
||||
<% menu.with_item(variant: "link", text: t(".statements"), href: account_path(account, tab: "statements"), icon: "archive") %>
|
||||
|
||||
<% if permission.in?([ :owner, :full_control ]) %>
|
||||
@@ -31,7 +31,7 @@
|
||||
<% if account.owned_by?(Current.user) && !account.linked? %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: "Delete account",
|
||||
text: t(".delete_account"),
|
||||
href: account_path(account),
|
||||
method: :delete,
|
||||
icon: "trash-2",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<%= icon "alert-circle", class: "w-5 h-5 text-destructive mr-2 shrink-0" %>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-destructive">
|
||||
<%= pluralize(sso_provider.errors.count, "error") %> prohibited this provider from being saved:
|
||||
<%= t("admin.sso_providers.form.errors_title", count: sso_provider.errors.count) %>
|
||||
</p>
|
||||
<ul class="mt-2 text-sm text-destructive list-disc list-inside">
|
||||
<% sso_provider.errors.full_messages.each do |message| %>
|
||||
@@ -20,38 +20,38 @@
|
||||
|
||||
<%= styled_form_with model: [:admin, sso_provider], class: "space-y-6", data: { controller: "admin-sso-form" } do |form| %>
|
||||
<div class="space-y-4">
|
||||
<h3 class="font-medium text-primary">Basic Information</h3>
|
||||
<h3 class="font-medium text-primary"><%= t("admin.sso_providers.form.basic_information") %></h3>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<%= form.select :strategy,
|
||||
options_for_select([
|
||||
["OpenID Connect", "openid_connect"],
|
||||
["SAML 2.0", "saml"],
|
||||
["Google OAuth2", "google_oauth2"],
|
||||
["GitHub", "github"]
|
||||
[t("admin.sso_providers.form.strategy_openid_connect"), "openid_connect"],
|
||||
[t("admin.sso_providers.form.strategy_saml"), "saml"],
|
||||
[t("admin.sso_providers.form.strategy_google_oauth2"), "google_oauth2"],
|
||||
[t("admin.sso_providers.form.strategy_github"), "github"]
|
||||
], sso_provider.strategy),
|
||||
{ label: "Strategy" },
|
||||
{ label: t("admin.sso_providers.form.strategy_label") },
|
||||
{ data: { action: "change->admin-sso-form#toggleFields" } } %>
|
||||
|
||||
<%= form.text_field :name,
|
||||
label: "Name",
|
||||
placeholder: "e.g., keycloak, authentik",
|
||||
label: t("admin.sso_providers.form.name_label"),
|
||||
placeholder: t("admin.sso_providers.form.name_placeholder"),
|
||||
required: true,
|
||||
data: { action: "input->admin-sso-form#updateCallbackUrl" } %>
|
||||
</div>
|
||||
<p class="text-xs text-secondary -mt-2">Unique identifier (lowercase, numbers, underscores only)</p>
|
||||
<p class="text-xs text-secondary -mt-2"><%= t("admin.sso_providers.form.name_help") %></p>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<%= form.text_field :label,
|
||||
label: "Button Label",
|
||||
placeholder: "e.g., Sign in with Keycloak",
|
||||
label: t("admin.sso_providers.form.label_label"),
|
||||
placeholder: t("admin.sso_providers.form.label_placeholder"),
|
||||
required: true %>
|
||||
|
||||
<div>
|
||||
<%= form.text_field :icon,
|
||||
label: "Icon (optional)",
|
||||
placeholder: "e.g., key, shield" %>
|
||||
<p class="text-xs text-secondary mt-1">Lucide icon name for the login button</p>
|
||||
label: t("admin.sso_providers.form.icon_label"),
|
||||
placeholder: t("admin.sso_providers.form.icon_placeholder") %>
|
||||
<p class="text-xs text-secondary mt-1"><%= t("admin.sso_providers.form.icon_help") %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -65,42 +65,42 @@
|
||||
</div>
|
||||
|
||||
<div class="border-t border-primary pt-4 space-y-4">
|
||||
<h3 class="font-medium text-primary">OAuth/OIDC Configuration</h3>
|
||||
<h3 class="font-medium text-primary"><%= t("admin.sso_providers.form.oauth_configuration") %></h3>
|
||||
|
||||
<div data-oidc-field class="<%= "hidden" unless sso_provider.strategy == "openid_connect" %>">
|
||||
<%= form.text_field :issuer,
|
||||
label: "Issuer URL",
|
||||
placeholder: "https://your-idp.example.com/realms/your-realm",
|
||||
label: t("admin.sso_providers.form.issuer_label"),
|
||||
placeholder: t("admin.sso_providers.form.issuer_placeholder"),
|
||||
data: { action: "blur->admin-sso-form#validateIssuer" } %>
|
||||
<p class="text-xs text-secondary mt-1">OIDC issuer URL (validates .well-known/openid-configuration)</p>
|
||||
<p class="text-xs text-secondary mt-1"><%= t("admin.sso_providers.form.issuer_help") %></p>
|
||||
</div>
|
||||
|
||||
<%= form.text_field :client_id,
|
||||
label: "Client ID",
|
||||
placeholder: "your-client-id",
|
||||
label: t("admin.sso_providers.form.client_id_label"),
|
||||
placeholder: t("admin.sso_providers.form.client_id_placeholder"),
|
||||
required: true %>
|
||||
|
||||
<%= form.password_field :client_secret,
|
||||
label: "Client Secret",
|
||||
placeholder: sso_provider.persisted? ? "••••••••" : "your-client-secret",
|
||||
label: t("admin.sso_providers.form.client_secret_label"),
|
||||
placeholder: sso_provider.persisted? ? t("admin.sso_providers.form.client_secret_placeholder_existing") : t("admin.sso_providers.form.client_secret_placeholder_new"),
|
||||
required: !sso_provider.persisted? %>
|
||||
<% if sso_provider.persisted? %>
|
||||
<p class="text-xs text-secondary -mt-2">Leave blank to keep existing secret</p>
|
||||
<p class="text-xs text-secondary -mt-2"><%= t("admin.sso_providers.form.client_secret_help_existing") %></p>
|
||||
<% end %>
|
||||
|
||||
<div data-oidc-field class="<%= "hidden" unless sso_provider.strategy == "openid_connect" %>">
|
||||
<label class="block text-sm font-medium text-primary mb-1">Callback URL</label>
|
||||
<label class="block text-sm font-medium text-primary mb-1"><%= t("admin.sso_providers.form.redirect_uri_label") %></label>
|
||||
<div class="flex items-center gap-2">
|
||||
<code class="flex-1 bg-surface px-3 py-2 rounded text-sm text-secondary overflow-x-auto"
|
||||
data-admin-sso-form-target="callbackUrl"><%= "#{request.base_url}/auth/#{sso_provider.name.presence || 'PROVIDER_NAME'}/callback" %></code>
|
||||
<button type="button"
|
||||
data-action="click->admin-sso-form#copyCallback"
|
||||
class="p-2 text-secondary hover:text-primary shrink-0"
|
||||
title="Copy to clipboard">
|
||||
title="<%= t("admin.sso_providers.form.copy_button") %>">
|
||||
<%= icon "copy", class: "w-4 h-4" %>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-secondary mt-1">Configure this URL in your identity provider</p>
|
||||
<p class="text-xs text-secondary mt-1"><%= t("admin.sso_providers.form.redirect_uri_help") %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -172,18 +172,18 @@
|
||||
</details>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-primary mb-1">SP Callback URL (ACS URL)</label>
|
||||
<label class="block text-sm font-medium text-primary mb-1"><%= t("admin.sso_providers.form.saml_sp_callback_url_label") %></label>
|
||||
<div class="flex items-center gap-2">
|
||||
<code class="flex-1 bg-surface px-3 py-2 rounded text-sm text-secondary overflow-x-auto"
|
||||
data-admin-sso-form-target="samlCallbackUrl"><%= "#{request.base_url}/auth/#{sso_provider.name.presence || 'PROVIDER_NAME'}/callback" %></code>
|
||||
<button type="button"
|
||||
data-action="click->admin-sso-form#copySamlCallback"
|
||||
class="p-2 text-secondary hover:text-primary shrink-0"
|
||||
title="Copy to clipboard">
|
||||
title="<%= t("admin.sso_providers.form.copy_button") %>">
|
||||
<%= icon "copy", class: "w-4 h-4" %>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-secondary mt-1">Configure this URL as the Assertion Consumer Service URL in your IdP</p>
|
||||
<p class="text-xs text-secondary mt-1"><%= t("admin.sso_providers.form.saml_sp_callback_url_help") %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -282,8 +282,8 @@
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<%= link_to "Cancel", admin_sso_providers_path, class: "px-4 py-2 text-sm font-medium text-secondary hover:text-primary" %>
|
||||
<%= form.submit sso_provider.persisted? ? "Update Provider" : "Create Provider",
|
||||
<%= link_to t("admin.sso_providers.form.cancel"), admin_sso_providers_path, class: "px-4 py-2 text-sm font-medium text-secondary hover:text-primary" %>
|
||||
<%= form.submit sso_provider.persisted? ? t("admin.sso_providers.form.update_provider") : t("admin.sso_providers.form.create_provider"),
|
||||
class: "px-4 py-2 button-bg-primary text-inverse rounded-lg text-sm font-medium hover:button-bg-primary-hover" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<%= content_for :page_title, "SSO Providers" %>
|
||||
<%= content_for :page_title, t(".page_title") %>
|
||||
|
||||
<div class="space-y-4">
|
||||
<p class="text-secondary mb-4">
|
||||
Manage single sign-on authentication providers for your instance.
|
||||
<%= t(".description") %>
|
||||
<% unless FeatureFlags.db_sso_providers? %>
|
||||
<span class="text-warning">Changes require a server restart to take effect.</span>
|
||||
<span class="text-warning"><%= t(".restart_required") %></span>
|
||||
<% end %>
|
||||
</p>
|
||||
|
||||
<%= settings_section title: "Configured Providers" do %>
|
||||
<%= settings_section title: t(".configured_providers") do %>
|
||||
<% if @sso_providers.any? %>
|
||||
<div class="divide-y divide-alpha-black-200 theme-dark:divide-alpha-white-200">
|
||||
<% @sso_providers.each do |provider| %>
|
||||
@@ -27,20 +27,20 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<% if provider.enabled? %>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">
|
||||
Enabled
|
||||
<%= t(".enabled") %>
|
||||
</span>
|
||||
<% else %>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-surface text-secondary">
|
||||
Disabled
|
||||
<%= t(".disabled") %>
|
||||
</span>
|
||||
<% end %>
|
||||
<%= link_to edit_admin_sso_provider_path(provider), class: "p-1 text-secondary hover:text-primary", title: "Edit" do %>
|
||||
<%= link_to edit_admin_sso_provider_path(provider), class: "p-1 text-secondary hover:text-primary", title: t(".edit") do %>
|
||||
<%= icon "pencil", class: "w-4 h-4" %>
|
||||
<% end %>
|
||||
<%= button_to toggle_admin_sso_provider_path(provider), method: :patch, class: "p-1 text-secondary hover:text-primary", title: provider.enabled? ? "Disable" : "Enable", form: { data: { turbo_confirm: "Are you sure you want to #{provider.enabled? ? 'disable' : 'enable'} this provider?" } } do %>
|
||||
<%= button_to toggle_admin_sso_provider_path(provider), method: :patch, class: "p-1 text-secondary hover:text-primary", title: provider.enabled? ? t(".disable") : t(".enable"), form: { data: { turbo_confirm: provider.enabled? ? t("admin.sso_providers.toggle.confirm_disable") : t("admin.sso_providers.toggle.confirm_enable") } } do %>
|
||||
<%= icon provider.enabled? ? "toggle-right" : "toggle-left", class: "w-4 h-4" %>
|
||||
<% end %>
|
||||
<%= button_to admin_sso_provider_path(provider), method: :delete, class: "p-1 text-destructive hover:text-destructive", title: "Delete", form: { data: { turbo_confirm: "Are you sure you want to delete this provider? This action cannot be undone." } } do %>
|
||||
<%= button_to admin_sso_provider_path(provider), method: :delete, class: "p-1 text-destructive hover:text-destructive", title: t(".delete"), form: { data: { turbo_confirm: t("admin.sso_providers.destroy.confirm") } } do %>
|
||||
<%= icon "trash-2", class: "w-4 h-4" %>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -50,14 +50,14 @@
|
||||
<% else %>
|
||||
<div class="text-center py-6">
|
||||
<%= icon "key", class: "w-12 h-12 mx-auto text-secondary mb-3" %>
|
||||
<p class="text-secondary">No SSO providers configured yet.</p>
|
||||
<p class="text-secondary"><%= t(".no_providers_message") %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="pt-4 border-t border-primary">
|
||||
<%= link_to new_admin_sso_provider_path, class: "inline-flex items-center gap-2 text-sm font-medium text-primary hover:text-secondary" do %>
|
||||
<%= icon "plus", class: "w-4 h-4" %>
|
||||
Add Provider
|
||||
<%= t(".add_provider") %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -100,26 +100,25 @@
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= settings_section title: "Configuration Mode", collapsible: true, open: false do %>
|
||||
<%= settings_section title: t(".configuration_mode"), collapsible: true, open: false do %>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="font-medium text-primary">Database-backed providers</p>
|
||||
<p class="text-sm text-secondary">Load providers from database instead of YAML config</p>
|
||||
<p class="font-medium text-primary"><%= t(".db_backed_providers") %></p>
|
||||
<p class="text-sm text-secondary"><%= t(".db_backed_providers_description") %></p>
|
||||
</div>
|
||||
<% if FeatureFlags.db_sso_providers? %>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">
|
||||
Enabled
|
||||
<%= t(".enabled") %>
|
||||
</span>
|
||||
<% else %>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-surface text-secondary">
|
||||
Disabled
|
||||
<%= t(".disabled") %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="text-sm text-secondary">
|
||||
Set <code class="bg-surface px-1 py-0.5 rounded text-xs">AUTH_PROVIDERS_SOURCE=db</code> to enable database-backed providers.
|
||||
This allows changes without server restarts.
|
||||
<%= t(".db_backed_providers_help_html") %>
|
||||
</p>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<% elsif assistant_message.reasoning? %>
|
||||
<details class="group mb-1">
|
||||
<summary class="flex items-center gap-2">
|
||||
<p class="text-secondary text-sm">Assistant reasoning</p>
|
||||
<p class="text-secondary text-sm"><%= t(".assistant_reasoning") %></p>
|
||||
<%= icon("chevron-down", class: "group-open:transform group-open:rotate-180") %>
|
||||
</summary>
|
||||
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
<details class="my-2 group mb-4">
|
||||
<summary class="text-secondary text-xs cursor-pointer flex items-center gap-2">
|
||||
<%= icon("chevron-right", class: "group-open:transform group-open:rotate-90") %>
|
||||
<p>Tool Calls</p>
|
||||
<p><%= t(".tool_calls") %></p>
|
||||
</summary>
|
||||
|
||||
<div class="mt-2">
|
||||
<% message.tool_calls.each do |tool_call| %>
|
||||
<div class="bg-blue-50 border-blue-200 px-3 py-2 rounded-lg border mb-2">
|
||||
<p class="text-secondary text-xs">Function:</p>
|
||||
<p class="text-secondary text-xs"><%= t(".function") %></p>
|
||||
<p class="text-primary text-sm font-mono"><%= tool_call.function_name %></p>
|
||||
<p class="text-secondary text-xs mt-2">Arguments:</p>
|
||||
<p class="text-secondary text-xs mt-2"><%= t(".arguments") %></p>
|
||||
<pre class="text-primary text-sm font-mono whitespace-pre-wrap"><%= tool_call.function_arguments %></pre>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div id="<%= dom_id(budget, :confirm_button) %>">
|
||||
<%= render DS::Button.new(
|
||||
text: "Confirm",
|
||||
text: t(".confirm"),
|
||||
variant: "primary",
|
||||
full_width: true,
|
||||
href: budget_path(budget),
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<div class="flex justify-center items-center">
|
||||
<div class="text-center flex flex-col items-center max-w-[500px]">
|
||||
<h2 class="text-lg text-primary font-medium">Oops!</h2>
|
||||
<h2 class="text-lg text-primary font-medium"><%= t(".oops") %></h2>
|
||||
<p class="text-secondary text-sm max-w-sm mx-auto mb-4">
|
||||
You have not created or assigned any expense categories to your transactions yet.
|
||||
<%= t(".no_categories_message") %>
|
||||
</p>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<%= render DS::Button.new(
|
||||
text: "Use defaults (recommended)",
|
||||
text: t(".use_defaults"),
|
||||
href: bootstrap_categories_path,
|
||||
) %>
|
||||
|
||||
<%= render DS::Link.new(
|
||||
text: "New category",
|
||||
text: t(".new_category"),
|
||||
variant: "outline",
|
||||
icon: "plus",
|
||||
href: new_category_path,
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
<div>
|
||||
<div class="space-y-6">
|
||||
<div class="text-center space-y-2">
|
||||
<h1 class="text-3xl text-primary font-medium">Edit your category budgets</h1>
|
||||
<h1 class="text-3xl text-primary font-medium"><%= t(".title") %></h1>
|
||||
<p class="text-secondary text-sm max-w-md mx-auto">
|
||||
Adjust category budgets to set spending limits. Unallocated funds will be automatically assigned as uncategorized.
|
||||
<%= t(".description") %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<%= render DS::Dialog.new(variant: :drawer) do |dialog| %>
|
||||
<% dialog.with_header do %>
|
||||
<div>
|
||||
<p class="text-sm text-secondary">Category</p>
|
||||
<p class="text-sm text-secondary"><%= t(".category") %></p>
|
||||
<h3 class="text-2xl font-medium text-primary">
|
||||
<%= @budget_category.name %>
|
||||
</h3>
|
||||
@@ -26,12 +26,12 @@
|
||||
<% end %>
|
||||
|
||||
<% dialog.with_body do %>
|
||||
<% dialog.with_section(title: "Overview", open: true) do %>
|
||||
<% dialog.with_section(title: t(".overview"), open: true) do %>
|
||||
<div class="pb-4">
|
||||
<dl class="space-y-3 px-3 py-2">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<dt class="text-secondary">
|
||||
<%= @budget_category.budget.start_date.strftime("%b %Y") %> spending
|
||||
<%= t(".spending", date: @budget_category.budget.start_date.strftime("%b %Y")) %>
|
||||
</dt>
|
||||
<dd class="text-primary font-medium privacy-sensitive">
|
||||
<%= format_money @budget_category.actual_spending_money %>
|
||||
@@ -40,30 +40,30 @@
|
||||
|
||||
<% if @budget_category.budget.initialized? %>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<dt class="text-secondary">Status</dt>
|
||||
<dt class="text-secondary"><%= t(".status") %></dt>
|
||||
<% if @budget_category.available_to_spend.negative? %>
|
||||
<dd class="flex items-center gap-1 text-red-500 font-medium privacy-sensitive">
|
||||
<%= icon "alert-circle", size: "sm", color: "destructive" %>
|
||||
<%= format_money @budget_category.available_to_spend_money.abs %>
|
||||
<span>overspent</span>
|
||||
<span><%= t(".overspent") %></span>
|
||||
</dd>
|
||||
<% elsif @budget_category.available_to_spend.zero? %>
|
||||
<dd class="flex items-center gap-1 text-orange-500 font-medium privacy-sensitive">
|
||||
<%= icon "x-circle", size: "sm", color: "warning" %>
|
||||
<%= format_money @budget_category.available_to_spend_money %>
|
||||
<span>left</span>
|
||||
<span><%= t(".left") %></span>
|
||||
</dd>
|
||||
<% else %>
|
||||
<dd class="text-primary flex items-center gap-1 text-green-500 font-medium privacy-sensitive">
|
||||
<%= icon "check-circle", size: "sm", color: "success" %>
|
||||
<%= format_money @budget_category.available_to_spend_money %>
|
||||
<span>left</span>
|
||||
<span><%= t(".left") %></span>
|
||||
</dd>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<dt class="text-secondary">Budgeted</dt>
|
||||
<dt class="text-secondary"><%= t(".budgeted") %></dt>
|
||||
<dd class="text-primary font-medium privacy-sensitive">
|
||||
<%= format_money @budget_category.budgeted_spending_money %>
|
||||
</dd>
|
||||
@@ -71,14 +71,14 @@
|
||||
<% end %>
|
||||
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<dt class="text-secondary">Monthly average spending</dt>
|
||||
<dt class="text-secondary"><%= t(".monthly_average_spending") %></dt>
|
||||
<dd class="text-primary font-medium privacy-sensitive">
|
||||
<%= @budget_category.avg_monthly_expense_money.format %>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<dt class="text-secondary">Monthly median spending</dt>
|
||||
<dt class="text-secondary"><%= t(".monthly_median_spending") %></dt>
|
||||
<dd class="text-primary font-medium privacy-sensitive">
|
||||
<%= @budget_category.median_monthly_expense_money.format %>
|
||||
</dd>
|
||||
@@ -87,7 +87,7 @@
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% dialog.with_section(title: "Recent Transactions", open: true) do %>
|
||||
<% dialog.with_section(title: t(".recent_transactions"), open: true) do %>
|
||||
<div class="space-y-2">
|
||||
<div class="px-3 py-4 space-y-2">
|
||||
<% if @recent_transactions.any? %>
|
||||
@@ -120,7 +120,7 @@
|
||||
</ul>
|
||||
|
||||
<%= render DS::Link.new(
|
||||
text: "View all category transactions",
|
||||
text: t(".view_all_transactions"),
|
||||
variant: "outline",
|
||||
full_width: true,
|
||||
href: transactions_path(q: {
|
||||
@@ -132,7 +132,7 @@
|
||||
) %>
|
||||
<% else %>
|
||||
<p class="text-secondary text-sm mb-4">
|
||||
No transactions found for this budget period.
|
||||
<%= t(".no_transactions") %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<div>
|
||||
<div class="p-4 border-b border-secondary">
|
||||
<h3 class="text-sm text-secondary mb-2">Income</h3>
|
||||
<h3 class="text-sm text-secondary mb-2"><%= t(".income") %></h3>
|
||||
|
||||
<span class="inline-block mb-2 text-xl font-medium text-primary privacy-sensitive">
|
||||
<%= budget.actual_income_money.format %>
|
||||
@@ -30,7 +30,7 @@
|
||||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
<h3 class="text-sm text-secondary mb-2">Expenses</h3>
|
||||
<h3 class="text-sm text-secondary mb-2"><%= t(".expenses") %></h3>
|
||||
|
||||
<span class="inline-block mb-2 text-xl font-medium text-primary privacy-sensitive"><%= budget.actual_spending_money.format %></span>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div data-donut-chart-target="defaultContent" class="flex flex-col items-center">
|
||||
<% if budget.initialized? %>
|
||||
<div class="text-secondary text-sm mb-2">
|
||||
<span>Spent</span>
|
||||
<span><%= t(".spent") %></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-2 text-3xl font-medium privacy-sensitive <%= budget.available_to_spend.negative? ? "text-red-500" : "text-primary" %>">
|
||||
@@ -13,7 +13,7 @@
|
||||
</div>
|
||||
|
||||
<%= render DS::Link.new(
|
||||
text: "of #{budget.budgeted_spending_money.format}",
|
||||
text: t(".of_budget", amount: budget.budgeted_spending_money.format),
|
||||
variant: "secondary",
|
||||
icon: "pencil",
|
||||
icon_position: "right",
|
||||
@@ -26,7 +26,7 @@
|
||||
</div>
|
||||
|
||||
<%= render DS::Link.new(
|
||||
text: "New budget",
|
||||
text: t(".new_budget"),
|
||||
size: "sm",
|
||||
icon: "plus",
|
||||
href: edit_budget_path(budget)
|
||||
@@ -47,7 +47,7 @@
|
||||
</p>
|
||||
|
||||
<%= render DS::Link.new(
|
||||
text: "of #{bc.budgeted_spending_money.format(precision: 0)}",
|
||||
text: t(".of_budget", amount: bc.budgeted_spending_money.format(precision: 0)),
|
||||
variant: "secondary",
|
||||
icon: "pencil",
|
||||
icon_position: "right",
|
||||
@@ -59,7 +59,7 @@
|
||||
<% end %>
|
||||
|
||||
<div id="segment_unused" class="hidden">
|
||||
<p class="text-sm text-secondary text-center mb-2">Unused</p>
|
||||
<p class="text-sm text-secondary text-center mb-2"><%= t(".unused") %></p>
|
||||
|
||||
<p class="text-3xl font-medium text-primary privacy-sensitive">
|
||||
<%= format_money(budget.available_to_spend_money) %>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
<div class="ml-auto">
|
||||
<%= render DS::Link.new(
|
||||
text: "Today",
|
||||
text: t(".today"),
|
||||
variant: "outline",
|
||||
href: budget_path(Budget.date_to_param(Date.current)),
|
||||
) %>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<div>
|
||||
<div class="p-4 border-b border-secondary">
|
||||
<h3 class="text-sm text-secondary mb-2">Expected income</h3>
|
||||
<h3 class="text-sm text-secondary mb-2"><%= t(".expected_income") %></h3>
|
||||
|
||||
<span class="inline-block mb-2 text-xl font-medium text-primary privacy-sensitive">
|
||||
<%= format_money(budget.expected_income_money) %>
|
||||
@@ -19,12 +19,12 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm privacy-sensitive">
|
||||
<p class="text-secondary"><%= format_money(budget.actual_income_money) %> earned</p>
|
||||
<p class="text-secondary"><%= t(".earned", amount: format_money(budget.actual_income_money)) %></p>
|
||||
<p class="font-medium">
|
||||
<% if budget.remaining_expected_income.negative? %>
|
||||
<span class="text-green-500"><%= format_money(budget.remaining_expected_income_money.abs) %> over</span>
|
||||
<span class="text-green-500"><%= t(".over", amount: format_money(budget.remaining_expected_income_money.abs)) %></span>
|
||||
<% else %>
|
||||
<span class="text-primary"><%= format_money(budget.remaining_expected_income_money) %> left</span>
|
||||
<span class="text-primary"><%= t(".left", amount: format_money(budget.remaining_expected_income_money)) %></span>
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
@@ -32,7 +32,7 @@
|
||||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
<h3 class="text-sm text-secondary mb-2">Budgeted</h3>
|
||||
<h3 class="text-sm text-secondary mb-2"><%= t(".budgeted") %></h3>
|
||||
|
||||
<span class="inline-block mb-2 text-xl font-medium text-primary privacy-sensitive">
|
||||
<%= format_money(budget.budgeted_spending_money) %>
|
||||
@@ -49,12 +49,12 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm privacy-sensitive">
|
||||
<p class="text-secondary"><%= format_money(budget.actual_spending_money) %> spent</p>
|
||||
<p class="text-secondary"><%= t(".spent", amount: format_money(budget.actual_spending_money)) %></p>
|
||||
<p class="font-medium">
|
||||
<% if budget.available_to_spend.negative? %>
|
||||
<span class="text-destructive"><%= format_money(budget.available_to_spend_money.abs) %> over</span>
|
||||
<span class="text-destructive"><%= t(".over", amount: format_money(budget.available_to_spend_money.abs)) %></span>
|
||||
<% else %>
|
||||
<span class="text-primary"><%= format_money(budget.available_to_spend_money) %> left</span>
|
||||
<span class="text-primary"><%= t(".left", amount: format_money(budget.available_to_spend_money)) %></span>
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
<div class="flex flex-col gap-4 items-center justify-center h-full">
|
||||
<%= icon "alert-triangle", size: "lg", color: "destructive" %>
|
||||
<p class="text-secondary text-sm text-center">You have over-allocated your budget. Please fix your allocations.</p>
|
||||
<p class="text-secondary text-sm text-center"><%= t(".over_allocated_message") %></p>
|
||||
|
||||
<%= render DS::Link.new(
|
||||
text: "Fix allocations",
|
||||
text: t(".fix_allocations"),
|
||||
variant: "secondary",
|
||||
size: "sm",
|
||||
icon: "pencil",
|
||||
|
||||
@@ -8,24 +8,24 @@
|
||||
<div>
|
||||
<div class="space-y-4">
|
||||
<div class="text-center space-y-2">
|
||||
<h1 class="text-3xl text-primary font-medium">Setup your budget</h1>
|
||||
<h1 class="text-3xl text-primary font-medium"><%= t(".setup_title") %></h1>
|
||||
<p class="text-secondary text-sm max-w-sm mx-auto">
|
||||
Enter your monthly earnings and planned spending below to setup your budget.
|
||||
<%= t(".setup_description") %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-lg">
|
||||
<%= styled_form_with model: @budget, class: "space-y-3", data: { controller: "budget-form" } do |f| %>
|
||||
<%= f.money_field :budgeted_spending, label: "Budgeted spending", required: true, disable_currency: true %>
|
||||
<%= f.money_field :expected_income, label: "Expected income", required: true, disable_currency: true %>
|
||||
<%= f.money_field :budgeted_spending, label: t(".budgeted_spending"), required: true, disable_currency: true %>
|
||||
<%= f.money_field :expected_income, label: t(".expected_income"), required: true, disable_currency: true %>
|
||||
|
||||
<% if @budget.estimated_income && @budget.estimated_spending %>
|
||||
<div class="border border-tertiary rounded-lg p-3 flex">
|
||||
<%= icon "sparkles" %>
|
||||
<div class="ml-2 space-y-1 text-sm">
|
||||
<h4 class="text-primary">Autosuggest income & spending budget</h4>
|
||||
<h4 class="text-primary"><%= t(".autosuggest_title") %></h4>
|
||||
<p class="text-secondary">
|
||||
This will be based on transaction history. AI can make mistakes, verify before continuing.
|
||||
<%= t(".autosuggest_description") %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= f.submit "Continue" %>
|
||||
<%= f.submit t(".continue") %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<div class="fixed right-0 sm:right-auto mx-2 sm:ml-8 sm:mr-0 mt-2 z-50 bg-container p-4 border border-alpha-black-25 rounded-2xl shadow-xs h-fit" data-category-target="popup">
|
||||
<div class="flex gap-2 flex-col mb-4" data-category-target="selection" style="<%= "display:none;" if @category.subcategory? %>">
|
||||
<div data-category-target="pickerSection"></div>
|
||||
<h4 class="text-secondary text-sm">Color</h4>
|
||||
<h4 class="text-secondary text-sm"><%= t(".color") %></h4>
|
||||
<div class="flex flex-wrap md:flex-nowrap gap-2 items-center" data-category-target="colorsSection">
|
||||
<% Category::COLORS.each do |color| %>
|
||||
<label class="relative">
|
||||
@@ -34,14 +34,14 @@
|
||||
<%= icon "palette", size: "2xl", data: { action: "click->category#toggleSections" } %>
|
||||
</div>
|
||||
<div data-category-target="validationMessage" class="hidden self-start flex gap-1 items-center text-xs text-destructive ">
|
||||
<span>Poor contrast, choose darker color or</span>
|
||||
<span><%= t(".poor_contrast") %></span>
|
||||
<button type="button" class="underline cursor-pointer" data-action="category#autoAdjust">auto-adjust.</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2 justify-center flex-col w-auto md:w-87">
|
||||
<h4 class="text-secondary text-sm">Icon</h4>
|
||||
<h4 class="text-secondary text-sm"><%= t(".icon") %></h4>
|
||||
<div class="flex flex-wrap gap-0.5 max-h-52 overflow-auto">
|
||||
<% Category.icon_codes.each do |icon| %>
|
||||
<label class="relative">
|
||||
@@ -62,9 +62,9 @@
|
||||
<% end %>
|
||||
|
||||
<div class="space-y-2">
|
||||
<%= f.text_field :name, placeholder: t(".placeholder"), required: true, autofocus: true, label: "Name", data: { color_avatar_target: "name" } %>
|
||||
<%= f.text_field :name, placeholder: t(".placeholder"), required: true, autofocus: true, label: t(".name_label"), data: { color_avatar_target: "name" } %>
|
||||
<% unless category.parent? %>
|
||||
<%= f.select :parent_id, categories.pluck(:name, :id), { include_blank: "(unassigned)", label: "Parent category (optional)" }, disabled: category.parent?, data: { action: "change->category#handleParentChange" } %>
|
||||
<%= f.select :parent_id, categories.pluck(:name, :id), { include_blank: t(".unassigned"), label: t(".parent_category_label") }, disabled: category.parent?, data: { action: "change->category#handleParentChange" } %>
|
||||
<% end %>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<%= render DS::Menu.new do |menu| %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: "Delete all",
|
||||
text: t(".delete_all"),
|
||||
href: destroy_all_categories_path,
|
||||
method: :delete,
|
||||
icon: "trash-2",
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
data: { turbo_frame: "modal" } do %>
|
||||
<%= icon("refresh-cw") %>
|
||||
|
||||
<p>Match transfer/payment</p>
|
||||
<p><%= t(".match_transfer") %></p>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<p>One-time <%= @transaction.entry.amount.negative? ? "income" : "expense" %></p>
|
||||
<p><%= t(".one_time", type: @transaction.entry.amount.negative? ? t(".income") : t(".expense")) %></p>
|
||||
|
||||
<span class="text-orange-500 ml-auto">
|
||||
<%= icon("asterisk", color: "current") %>
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
<%= render "chats/ai_avatar" %>
|
||||
</div>
|
||||
|
||||
<h3 class="text-sm font-medium text-primary mb-1 -mt-2 text-center">Enable AI Chats</h3>
|
||||
<h3 class="text-sm font-medium text-primary mb-1 -mt-2 text-center"><%= t(".title") %></h3>
|
||||
|
||||
<p class="text-secondary mb-4 text-sm text-center">
|
||||
<% if Current.user.ai_available? %>
|
||||
AI chat can answer financial questions and provide insights based on your data. To use this feature you'll need to explicitly enable it.
|
||||
<%= t(".available_description") %>
|
||||
<% else %>
|
||||
To use the AI assistant, you need to set the <code class="bg-surface-inset px-1 py-0.5 rounded font-mono text-xs">OPENAI_ACCESS_TOKEN</code>
|
||||
environment variable or configure it in the Self-Hosting settings of your instance.
|
||||
<%= t(".unavailable_description_html") %>
|
||||
<% end %>
|
||||
</p>
|
||||
|
||||
@@ -18,9 +17,9 @@
|
||||
<%= form_with url: user_path(Current.user), method: :patch, class: "w-full", data: { turbo: false } do |form| %>
|
||||
<%= form.hidden_field "user[ai_enabled]", value: true %>
|
||||
<%= form.hidden_field "user[redirect_to]", value: "home" %>
|
||||
<%= form.submit "Enable AI Chats", class: "cursor-pointer hover:bg-inverse-hover w-full py-2 px-4 bg-inverse text-inverse rounded-lg text-sm font-medium" %>
|
||||
<%= form.submit t(".enable_button"), class: "cursor-pointer hover:bg-inverse-hover w-full py-2 px-4 bg-inverse text-inverse rounded-lg text-sm font-medium" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<p class="text-xs text-secondary text-center mt-2">Disable anytime. All data sent to our LLM providers is anonymized.</p>
|
||||
<p class="text-xs text-secondary text-center mt-2"><%= t(".disable_note") %></p>
|
||||
</div>
|
||||
|
||||
@@ -2,27 +2,27 @@
|
||||
<%= render "chats/ai_avatar" %>
|
||||
|
||||
<div class="max-w-[85%] text-sm space-y-4 text-primary">
|
||||
<p>Hey <%= Current.user&.first_name || "there" %>! I'm an AI/large-language-model that can help with your finances. I have access to the web and your account data.</p>
|
||||
<p><%= t(".greeting", name: Current.user&.first_name || t(".there")) %></p>
|
||||
|
||||
<p>
|
||||
You can use <span class="bg-container border border-secondary px-1.5 py-0.5 rounded font-mono text-xs">/</span> to access commands
|
||||
<%= t(".commands_hint_html") %>
|
||||
</p>
|
||||
|
||||
<div class="space-y-3">
|
||||
<p>Here's a few questions you can ask:</p>
|
||||
<p><%= t(".questions_intro") %></p>
|
||||
|
||||
<% questions = [
|
||||
{
|
||||
icon: "chart-area",
|
||||
text: "Evaluate investment portfolio"
|
||||
text: t(".evaluate_portfolio")
|
||||
},
|
||||
{
|
||||
icon: "wallet-minimal",
|
||||
text: "Show spending insights"
|
||||
text: t(".spending_insights")
|
||||
},
|
||||
{
|
||||
icon: "alert-triangle",
|
||||
text: "Find unusual patterns"
|
||||
text: t(".unusual_patterns")
|
||||
}
|
||||
] %>
|
||||
|
||||
|
||||
@@ -14,14 +14,14 @@
|
||||
<%= render DS::Menu.new(icon_vertical: true) do |menu| %>
|
||||
<% menu.with_item(
|
||||
variant: "link",
|
||||
text: "Edit chat title",
|
||||
text: t(".edit_chat_title"),
|
||||
href: edit_chat_path(chat, ctx: "list"),
|
||||
icon: "pencil",
|
||||
frame: dom_id(chat, "title")) %>
|
||||
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: "Delete chat",
|
||||
text: t(".delete_chat"),
|
||||
href: chat_path(chat),
|
||||
icon: "trash-2",
|
||||
method: :delete,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
icon: "menu",
|
||||
href: path,
|
||||
frame: chat_frame,
|
||||
text: "All chats"
|
||||
text: t(".all_chats")
|
||||
) %>
|
||||
|
||||
<div class="grow">
|
||||
@@ -19,19 +19,19 @@
|
||||
</div>
|
||||
|
||||
<%= render DS::Menu.new(icon_vertical: true) do |menu| %>
|
||||
<% menu.with_item(variant: "link", text: "Start new chat", href: new_chat_path, icon: "plus") %>
|
||||
<% menu.with_item(variant: "link", text: t(".start_new_chat"), href: new_chat_path, icon: "plus") %>
|
||||
|
||||
<% unless chat.new_record? %>
|
||||
<% menu.with_item(
|
||||
variant: "link",
|
||||
text: "Edit chat title",
|
||||
text: t(".edit_chat_title"),
|
||||
href: edit_chat_path(chat, ctx: "chat"),
|
||||
icon: "pencil",
|
||||
frame: dom_id(chat, "title")) %>
|
||||
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: "Delete chat",
|
||||
text: t(".delete_chat"),
|
||||
href: chat_path(chat),
|
||||
icon: "trash-2",
|
||||
method: :delete,
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<p class="text-xs text-red-500"><%= chat.presentable_error_message %></p>
|
||||
|
||||
<%= render DS::Button.new(
|
||||
text: "Retry",
|
||||
text: t(".retry"),
|
||||
href: retry_chat_path(chat),
|
||||
) %>
|
||||
</div>
|
||||
|
||||
@@ -10,14 +10,14 @@
|
||||
<% if @chats.any? %>
|
||||
<div class="grow flex flex-col">
|
||||
<div class="flex items-center justify-between my-6">
|
||||
<h1 class="text-xl font-medium">Chats</h1>
|
||||
<h1 class="text-xl font-medium"><%= t(".chats") %></h1>
|
||||
<%= render DS::Link.new(
|
||||
id: "new-chat",
|
||||
icon: "plus",
|
||||
variant: "icon",
|
||||
href: new_chat_path,
|
||||
frame: chat_frame,
|
||||
text: "New chat"
|
||||
text: t(".new_chat")
|
||||
) %>
|
||||
</div>
|
||||
<div class="space-y-2 px-0.5">
|
||||
@@ -26,7 +26,7 @@
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="grow flex flex-col">
|
||||
<h1 class="sr-only">Chats</h1>
|
||||
<h1 class="sr-only"><%= t(".chats") %></h1>
|
||||
<div class="mt-auto py-8">
|
||||
<%= render "chats/ai_greeting" %>
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
<div class="flex justify-center py-8">
|
||||
<%= render DS::Link.new(
|
||||
text: "Edit account details",
|
||||
text: t(".edit_account_details"),
|
||||
variant: "ghost",
|
||||
href: edit_credit_card_path(account),
|
||||
frame: :modal
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<div class="text-center">
|
||||
<%= render DS::Link.new(
|
||||
text: "Go back",
|
||||
text: t("doorkeeper.authorizations.error.go_back"),
|
||||
href: "javascript:history.back()",
|
||||
variant: :secondary
|
||||
) %>
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
</div>
|
||||
|
||||
<div class="bg-surface-inset rounded-lg p-4">
|
||||
<p class="text-xs text-secondary mb-2">Authorization Code:</p>
|
||||
<p class="text-xs text-secondary mb-2"><%= t(".authorization_code_label") %></p>
|
||||
<code id="authorization_code" class="block text-sm font-mono text-primary break-all"><%= params[:code] %></code>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-secondary text-center">
|
||||
Copy this code and paste it into the application.
|
||||
<%= t(".copy_instructions") %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -20,29 +20,29 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<%= tag.p enable_banking_item.institution_display_name, class: "font-medium text-primary" %>
|
||||
<% if enable_banking_item.scheduled_for_deletion? %>
|
||||
<p class="text-destructive text-sm animate-pulse">Deletion in progress</p>
|
||||
<p class="text-destructive text-sm animate-pulse"><%= t(".deletion_in_progress") %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="text-xs text-secondary">Enable Banking</p>
|
||||
<p class="text-xs text-secondary"><%= t(".provider_name") %></p>
|
||||
<% if enable_banking_item.syncing? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= icon "loader", size: "sm", class: "animate-spin" %>
|
||||
<%= tag.span "Syncing..." %>
|
||||
<%= tag.span t(".syncing") %>
|
||||
</div>
|
||||
<% elsif enable_banking_item.requires_update? %>
|
||||
<div class="text-warning flex items-center gap-1">
|
||||
<%= icon "alert-triangle", size: "sm", color: "warning" %>
|
||||
<%= tag.span "Reconnect" %>
|
||||
<%= tag.span t(".reconnect") %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-secondary">
|
||||
<% if enable_banking_item.last_synced_at %>
|
||||
Last synced <%= time_ago_in_words(enable_banking_item.last_synced_at) %> ago
|
||||
<%= t(".last_synced", time: time_ago_in_words(enable_banking_item.last_synced_at)) %>
|
||||
<% if enable_banking_item.sync_status_summary %>
|
||||
· <%= enable_banking_item.sync_status_summary %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
Never synced
|
||||
<%= t(".never_synced") %>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
@@ -57,7 +57,7 @@
|
||||
class: "inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-lg text-white bg-warning hover:opacity-90 transition-colors",
|
||||
data: { turbo: false } do %>
|
||||
<%= icon "refresh-cw", size: "sm" %>
|
||||
Update
|
||||
<%= t(".update") %>
|
||||
<% end %>
|
||||
<% elsif Rails.env.development? %>
|
||||
<%= icon(
|
||||
@@ -70,7 +70,7 @@
|
||||
<%= render DS::Menu.new do |menu| %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: "Delete",
|
||||
text: t(".delete"),
|
||||
icon: "trash-2",
|
||||
href: enable_banking_item_path(enable_banking_item),
|
||||
method: :delete,
|
||||
@@ -109,10 +109,10 @@
|
||||
|
||||
<% if enable_banking_item.unlinked_accounts_count > 0 %>
|
||||
<div class="p-4 flex flex-col gap-3 items-center justify-center">
|
||||
<p class="text-primary font-medium text-sm">Setup needed</p>
|
||||
<p class="text-secondary text-sm"><%= pluralize(enable_banking_item.unlinked_accounts_count, "account") %> imported from Enable Banking need to be set up</p>
|
||||
<p class="text-primary font-medium text-sm"><%= t(".setup_needed") %></p>
|
||||
<p class="text-secondary text-sm"><%= t(".setup_needed_description", count: enable_banking_item.unlinked_accounts_count) %></p>
|
||||
<%= render DS::Link.new(
|
||||
text: "Set up accounts",
|
||||
text: t(".set_up_accounts"),
|
||||
icon: "settings",
|
||||
variant: "primary",
|
||||
href: setup_accounts_enable_banking_item_path(enable_banking_item),
|
||||
@@ -121,8 +121,8 @@
|
||||
</div>
|
||||
<% elsif enable_banking_item.accounts.empty? && enable_banking_item.enable_banking_accounts.empty? %>
|
||||
<div class="p-4 flex flex-col gap-3 items-center justify-center">
|
||||
<p class="text-primary font-medium text-sm">No accounts found</p>
|
||||
<p class="text-secondary text-sm">No accounts were found from Enable Banking. Try syncing again.</p>
|
||||
<p class="text-primary font-medium text-sm"><%= t(".no_accounts_found") %></p>
|
||||
<p class="text-secondary text-sm"><%= t(".no_accounts_found_description") %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -17,22 +17,22 @@
|
||||
<% if item.session_valid? %>
|
||||
<div class="w-2 h-2 bg-success rounded-full"></div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-primary"><%= item.aspsp_name || "Connected Bank" %></p>
|
||||
<p class="text-sm font-medium text-primary"><%= item.aspsp_name || t(".connected_bank") %></p>
|
||||
<p class="text-xs text-secondary">
|
||||
Session expires: <%= item.session_expires_at&.strftime("%b %d, %Y") || "Unknown" %>
|
||||
<%= t(".session_expires") %>: <%= item.session_expires_at&.strftime("%b %d, %Y") || t(".unknown") %>
|
||||
</p>
|
||||
</div>
|
||||
<% elsif item.session_expired? %>
|
||||
<div class="w-2 h-2 bg-warning rounded-full"></div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-primary"><%= item.aspsp_name || "Connection" %></p>
|
||||
<p class="text-xs text-destructive">Session expired - re-authorization required</p>
|
||||
<p class="text-sm font-medium text-primary"><%= item.aspsp_name || t(".connection") %></p>
|
||||
<p class="text-xs text-destructive"><%= t(".session_expired") %></p>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="w-2 h-2 bg-secondary rounded-full"></div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-primary">Configured</p>
|
||||
<p class="text-xs text-secondary">Ready to connect a bank</p>
|
||||
<p class="text-sm font-medium text-primary"><%= t(".configured") %></p>
|
||||
<p class="text-xs text-secondary"><%= t(".ready_to_connect") %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -43,28 +43,28 @@
|
||||
method: :post,
|
||||
class: "inline-flex items-center justify-center rounded-lg px-3 py-1.5 text-xs font-medium text-primary bg-container border border-primary hover:bg-surface-inset transition-colors",
|
||||
data: { turbo: false } do %>
|
||||
Sync
|
||||
<%= t(".sync") %>
|
||||
<% end %>
|
||||
<% elsif item.session_expired? %>
|
||||
<%= button_to reauthorize_enable_banking_item_path(item),
|
||||
method: :post,
|
||||
class: "inline-flex items-center justify-center rounded-lg px-3 py-1.5 text-xs font-medium text-white bg-warning hover:opacity-90 transition-colors",
|
||||
data: { turbo: false } do %>
|
||||
Reconnect
|
||||
<%= t(".reconnect") %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= link_to select_bank_enable_banking_item_path(item),
|
||||
class: "inline-flex items-center justify-center rounded-lg px-3 py-1.5 text-xs font-medium text-inverse button-bg-primary hover:button-bg-primary-hover transition-colors",
|
||||
data: { turbo_frame: "modal" } do %>
|
||||
Connect Bank
|
||||
<%= t(".connect_bank") %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= button_to enable_banking_item_path(item),
|
||||
method: :delete,
|
||||
class: "inline-flex items-center justify-center rounded-lg px-3 py-1.5 text-xs font-medium text-destructive hover:bg-destructive/10 transition-colors",
|
||||
data: { turbo_confirm: "Are you sure you want to remove this connection?" } do %>
|
||||
Remove
|
||||
data: { turbo_confirm: t(".remove_confirm") } do %>
|
||||
<%= t(".remove") %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -78,7 +78,7 @@
|
||||
class: "inline-flex items-center gap-2 justify-center rounded-lg px-4 py-2 text-sm font-medium text-inverse button-bg-primary hover:button-bg-primary-hover transition-colors",
|
||||
data: { turbo_frame: "modal" } do %>
|
||||
<%= icon "plus", size: "sm" %>
|
||||
Add Connection
|
||||
<%= t(".add_connection") %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -88,18 +88,18 @@
|
||||
<div class="flex items-start gap-3">
|
||||
<%= icon("alert-circle", class: "text-warning w-5 h-5 shrink-0 mt-0.5") %>
|
||||
<div class="text-sm text-secondary">
|
||||
<p class="font-medium text-primary mb-2">Enable Banking connection not configured</p>
|
||||
<p>Before you can link Enable Banking accounts, you need to configure your Enable Banking connection.</p>
|
||||
<p class="font-medium text-primary mb-2"><%= t(".not_configured") %></p>
|
||||
<p><%= t(".not_configured_description") %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-surface rounded-lg p-4 space-y-2 text-sm">
|
||||
<p class="font-medium text-primary">Setup Steps:</p>
|
||||
<p class="font-medium text-primary"><%= t(".setup_steps_title") %></p>
|
||||
<ol class="list-decimal list-inside space-y-1 text-secondary">
|
||||
<li>Go to <strong>Settings → Providers</strong></li>
|
||||
<li>Find the <strong>Enable Banking</strong> section</li>
|
||||
<li>Enter your Enable Banking credentials</li>
|
||||
<li>Return here to link your accounts</li>
|
||||
<li><%= t(".setup_step_1_html") %></li>
|
||||
<li><%= t(".setup_step_2_html") %></li>
|
||||
<li><%= t(".setup_step_3") %></li>
|
||||
<li><%= t(".setup_step_4") %></li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
<%= link_to settings_providers_path,
|
||||
class: "w-full inline-flex items-center justify-center rounded-lg font-medium whitespace-nowrap rounded-lg hidden md:inline-flex px-3 py-2 text-sm text-inverse bg-inverse hover:bg-inverse-hover disabled:bg-gray-500 theme-dark:disabled:bg-gray-400",
|
||||
data: { turbo: false } do %>
|
||||
Go to Provider Settings
|
||||
<%= t(".go_to_provider_settings") %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<%# Modal: Link an existing manual account to a Enable Banking account %>
|
||||
<%= turbo_frame_tag "modal" do %>
|
||||
<%= render DS::Dialog.new do |dialog| %>
|
||||
<% dialog.with_header(title: "Link Enable Banking account") %>
|
||||
<% dialog.with_header(title: t(".title")) %>
|
||||
|
||||
<% dialog.with_body do %>
|
||||
<% if @available_enable_banking_accounts.blank? %>
|
||||
<div class="p-4 text-sm text-secondary">
|
||||
<p class="mb-2">All Enable Banking accounts appear to be linked already.</p>
|
||||
<p class="mb-2"><%= t(".all_linked") %></p>
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
<li>If you just connected or synced, try again after the sync completes.</li>
|
||||
<li>To link a different account, first unlink it from the account’s actions menu.</li>
|
||||
<li><%= t(".try_after_sync") %></li>
|
||||
<li><%= t(".unlink_to_move") %></li>
|
||||
</ul>
|
||||
</div>
|
||||
<% else %>
|
||||
@@ -22,7 +22,7 @@
|
||||
<div class="flex flex-col">
|
||||
<span class="text-sm text-primary font-medium"><%= eba.name.presence || eba.account_id %></span>
|
||||
<span class="text-xs text-secondary">
|
||||
<%= eba.currency %> • Balance: <%= number_to_currency((eba.current_balance || 0), unit: eba.currency) %>
|
||||
<%= eba.currency %> • <%= t(".balance") %>: <%= number_to_currency((eba.current_balance || 0), unit: eba.currency) %>
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
@@ -30,8 +30,8 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<%= render DS::Button.new(text: "Link", variant: :primary, icon: "link-2", type: :submit) %>
|
||||
<%= render DS::Link.new(text: "Cancel", variant: :secondary, href: accounts_path, data: { turbo_frame: "_top" }) %>
|
||||
<%= render DS::Button.new(text: t(".link"), variant: :primary, icon: "link-2", type: :submit) %>
|
||||
<%= render DS::Link.new(text: t(".cancel"), variant: :secondary, href: accounts_path, data: { turbo_frame: "_top" }) %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<%= render DS::Dialog.new do |dialog| %>
|
||||
<% dialog.with_header(title: "Set Up Your Enable Banking Accounts") do %>
|
||||
<% dialog.with_header(title: t(".title")) do %>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon "building-2", class: "text-primary" %>
|
||||
<span class="text-primary">Choose the correct account types for your imported accounts</span>
|
||||
<span class="text-primary"><%= t(".header_subtitle") %></span>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
data: {
|
||||
controller: "loading-button",
|
||||
action: "submit->loading-button#showLoading",
|
||||
loading_button_loading_text_value: "Creating Accounts...",
|
||||
loading_button_loading_text_value: t(".creating_accounts"),
|
||||
turbo_frame: "_top"
|
||||
},
|
||||
class: "space-y-6" do |form| %>
|
||||
@@ -24,7 +24,7 @@
|
||||
<%= icon "info", size: "sm", class: "text-primary mt-0.5 flex-shrink-0" %>
|
||||
<div>
|
||||
<p class="text-sm text-primary mb-2">
|
||||
<strong>Choose the correct account type for each Enable Banking account:</strong>
|
||||
<strong><%= t(".choose_account_type") %></strong>
|
||||
</p>
|
||||
<ul class="text-xs text-secondary space-y-1 list-disc list-inside">
|
||||
<% @account_type_options.reject { |_, type| type == "skip" }.each do |label, _| %>
|
||||
@@ -53,15 +53,15 @@
|
||||
<%= icon "calendar", size: "sm", class: "text-primary mt-0.5 flex-shrink-0" %>
|
||||
<div class="flex-1">
|
||||
<p class="text-sm text-primary mb-3">
|
||||
<strong>Historical Data Range:</strong>
|
||||
<strong><%= t(".historical_data_range") %></strong>
|
||||
</p>
|
||||
<%= form.date_field :sync_start_date,
|
||||
label: "Start syncing transactions from:",
|
||||
label: t(".sync_start_date_label"),
|
||||
value: @enable_banking_item.sync_start_date || 3.months.ago.to_date,
|
||||
min: 2.years.ago.to_date,
|
||||
max: Date.current,
|
||||
class: "w-full max-w-xs rounded-md border border-primary px-3 py-2 text-sm bg-container-inset text-primary",
|
||||
help_text: "Select how far back you want to sync transaction history. Maximum 2 years of history available." %>
|
||||
help_text: t(".sync_start_date_help") %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -81,7 +81,7 @@
|
||||
<p><%= enable_banking_account.account_type_display %></p>
|
||||
<% end %>
|
||||
<% if enable_banking_account.current_balance.present? %>
|
||||
<p>Balance: <%= number_to_currency(enable_banking_account.current_balance, unit: enable_banking_account.currency) %></p>
|
||||
<p><%= t(".balance") %>: <%= number_to_currency(enable_banking_account.current_balance, unit: enable_banking_account.currency) %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -92,7 +92,7 @@
|
||||
data-account-type-selector-account-id-value="<%= enable_banking_account.id %>"
|
||||
data-account-type-selector-suggested-subtype-value="<%= enable_banking_account.suggested_subtype %>">
|
||||
<div>
|
||||
<%= label_tag "account_types[#{enable_banking_account.id}]", "Account Type:",
|
||||
<%= label_tag "account_types[#{enable_banking_account.id}]", t(".account_type_label"),
|
||||
class: "block text-sm font-medium text-primary mb-2" %>
|
||||
<%= select_tag "account_types[#{enable_banking_account.id}]",
|
||||
options_for_select(@account_type_options, enable_banking_account.suggested_account_type || "skip"),
|
||||
@@ -115,7 +115,7 @@
|
||||
|
||||
<div class="flex gap-3">
|
||||
<%= render DS::Button.new(
|
||||
text: "Create Accounts",
|
||||
text: t(".create_accounts"),
|
||||
variant: "primary",
|
||||
icon: "plus",
|
||||
type: "submit",
|
||||
@@ -123,7 +123,7 @@
|
||||
data: { loading_button_target: "button" }
|
||||
) %>
|
||||
<%= render DS::Link.new(
|
||||
text: "Cancel",
|
||||
text: t(".cancel"),
|
||||
variant: "secondary",
|
||||
href: accounts_path
|
||||
) %>
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
<%= render DS::Dialog.new do |dialog| %>
|
||||
<% dialog.with_header(title: "Export your data", subtitle: "Download all your financial data") %>
|
||||
<% dialog.with_header(title: t(".dialog_title"), subtitle: t(".dialog_subtitle")) %>
|
||||
|
||||
<% dialog.with_body do %>
|
||||
<div class="space-y-4">
|
||||
<div class="bg-container-inset rounded-lg p-4 space-y-3">
|
||||
<h3 class="font-medium text-primary">What's included:</h3>
|
||||
<h3 class="font-medium text-primary"><%= t(".whats_included") %></h3>
|
||||
<ul class="space-y-2 text-sm text-secondary">
|
||||
<li class="flex items-start gap-2">
|
||||
<%= icon "check", class: "shrink-0 mt-0.5 text-positive" %>
|
||||
<span>All accounts and balances</span>
|
||||
<span><%= t(".accounts_and_balances") %></span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<%= icon "check", class: "shrink-0 mt-0.5 text-positive" %>
|
||||
<span>Transaction history</span>
|
||||
<span><%= t(".transaction_history") %></span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<%= icon "check", class: "shrink-0 mt-0.5 text-positive" %>
|
||||
<span>Investment trades</span>
|
||||
<span><%= t(".investment_trades") %></span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<%= icon "check", class: "shrink-0 mt-0.5 text-positive" %>
|
||||
<span>Categories, tags and rules</span>
|
||||
<span><%= t(".categories_tags_rules") %></span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="bg-amber-50 border border-amber-200 rounded-lg p-3">
|
||||
<p class="text-sm text-amber-800">
|
||||
<strong>Note:</strong> This export includes all of your data, but only some of the data can be imported back via the CSV import feature. We support account, transaction (with category and tags), and trade imports. Other account data cannot be imported and is for your records only.
|
||||
<strong><%= t(".note_label") %>:</strong> <%= t(".note_description") %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<%= form_with url: family_exports_path, method: :post, class: "space-y-4" do |form| %>
|
||||
<div class="flex gap-3">
|
||||
<%= link_to "Cancel", "#", class: "flex-1 text-center px-4 py-2 text-primary bg-surface border border-primary rounded-lg hover:bg-surface-hover", data: { action: "click->modal#close" } %>
|
||||
<%= form.submit "Export data", class: "flex-1 bg-inverse text-inverse rounded-lg px-4 py-2 cursor-pointer hover:bg-inverse-hover" %>
|
||||
<%= link_to t(".cancel"), "#", class: "flex-1 text-center px-4 py-2 text-primary bg-surface border border-primary rounded-lg hover:bg-surface-hover", data: { action: "click->modal#close" } %>
|
||||
<%= form.submit t(".export_data"), class: "flex-1 bg-inverse text-inverse rounded-lg px-4 py-2 cursor-pointer hover:bg-inverse-hover" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -18,10 +18,10 @@
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm text-right align-middle">
|
||||
<%= render DS::Menu.new do |menu| %>
|
||||
<% menu.with_item(variant: "link", text: "Edit", href: edit_family_merchant_path(family_merchant), icon: "pencil", data: { turbo_frame: "modal" }) %>
|
||||
<% menu.with_item(variant: "link", text: t(".edit"), href: edit_family_merchant_path(family_merchant), icon: "pencil", data: { turbo_frame: "modal" }) %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: "Delete",
|
||||
text: t(".delete"),
|
||||
href: family_merchant_path(family_merchant),
|
||||
icon: "trash-2",
|
||||
method: :delete,
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<% else %>
|
||||
<div class="flex items-center gap-1 px-2 py-0.5 rounded text-secondary hover:text-primary hover:bg-container-inset-hover transition-colors">
|
||||
<%= icon "pencil", size: "xs" %>
|
||||
<span class="text-xs">Set</span>
|
||||
<span class="text-xs"><%= t(".set") %></span>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<%= turbo_frame_tag "modal" do %>
|
||||
<%= render DS::Dialog.new do |dialog| %>
|
||||
<% dialog.with_header(title: "Link Interactive Brokers account") %>
|
||||
<% dialog.with_header(title: t(".title")) %>
|
||||
|
||||
<% dialog.with_body do %>
|
||||
<% if @available_ibkr_accounts.blank? %>
|
||||
<div class="p-4 text-sm text-secondary">
|
||||
<p class="mb-2">No unlinked Interactive Brokers accounts are available yet.</p>
|
||||
<p class="mb-2"><%= t(".no_accounts_available") %></p>
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
<li>Run a sync from Settings > Providers after updating your Flex query.</li>
|
||||
<li>Wait for the account discovery sync to finish.</li>
|
||||
<li><%= t(".run_sync_hint") %></li>
|
||||
<li><%= t(".wait_for_sync") %></li>
|
||||
</ul>
|
||||
</div>
|
||||
<% else %>
|
||||
@@ -21,7 +21,7 @@
|
||||
<div class="flex flex-col">
|
||||
<span class="text-sm text-primary font-medium"><%= ibkr_account.name.presence || ibkr_account.ibkr_account_id %></span>
|
||||
<span class="text-xs text-secondary">
|
||||
<%= ibkr_account.currency %> • Balance: <%= number_to_currency((ibkr_account.current_balance || 0), unit: Money::Currency.new(ibkr_account.currency || "USD").symbol) %>
|
||||
<%= ibkr_account.currency %> • <%= t(".balance") %>: <%= number_to_currency((ibkr_account.current_balance || 0), unit: Money::Currency.new(ibkr_account.currency || "USD").symbol) %>
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
@@ -29,8 +29,8 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<%= render DS::Button.new(text: "Link", variant: :primary, icon: "link-2", type: :submit) %>
|
||||
<%= render DS::Link.new(text: "Cancel", variant: :secondary, href: accounts_path, data: { turbo_frame: "_top" }) %>
|
||||
<%= render DS::Button.new(text: t(".link"), variant: :primary, icon: "link-2", type: :submit) %>
|
||||
<%= render DS::Link.new(text: t(".cancel"), variant: :secondary, href: accounts_path, data: { turbo_frame: "_top" }) %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<div class="sticky top-0 left-0 w-full bg-black flex items-center justify-between font-mono">
|
||||
<div class="flex items-center bg-red-600 px-6 py-4">
|
||||
<%= icon "alert-triangle", size: "lg", color: "current", class: "mr-2" %>
|
||||
<span class="text-inverse font-semibold uppercase">Super Admin</span>
|
||||
<span class="text-inverse font-semibold uppercase"><%= t(".super_admin") %></span>
|
||||
</div>
|
||||
<div>
|
||||
<%= link_to "Jobs", sidekiq_web_url, class: "text-white underline hover:text-gray-100" %>
|
||||
<%= link_to t(".jobs"), sidekiq_web_url, class: "text-white underline hover:text-gray-100" %>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2 px-2 py-2 text-white">
|
||||
<% if Current.session.active_impersonator_session.present? %>
|
||||
<div class="flex items-center space-x-3 bg-gray-800 border border-gray-700 rounded-md pl-3">
|
||||
<div class="text-sm">
|
||||
Impersonating: <span class="font-semibold text-red-400"><%= Current.impersonated_user.email %></span>
|
||||
<%= t(".impersonating") %>: <span class="font-semibold text-red-400"><%= Current.impersonated_user.email %></span>
|
||||
</div>
|
||||
<%= button_to "Leave", leave_impersonation_sessions_path, method: :delete, class: "items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-red-500" %>
|
||||
<%= button_to "Terminate", complete_impersonation_session_path(Current.session.active_impersonator_session), method: :put, class: "items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-red-500" %>
|
||||
<%= button_to t(".leave"), leave_impersonation_sessions_path, method: :delete, class: "items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-red-500" %>
|
||||
<%= button_to t(".terminate"), complete_impersonation_session_path(Current.session.active_impersonator_session), method: :put, class: "items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-red-500" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<% if Current.true_user.impersonator_support_sessions.in_progress.any? %>
|
||||
@@ -23,16 +23,16 @@
|
||||
Current.true_user.impersonator_support_sessions.in_progress.map { |session|
|
||||
["#{session.impersonated.email} (#{session.status})", session.id]
|
||||
},
|
||||
{ prompt: "Join a session" },
|
||||
{ prompt: t(".join_a_session") },
|
||||
{ class: "rounded-md text-sm border-0 focus:ring-0 ring-0 text-black font-mono" } %>
|
||||
<%= f.submit "Join",
|
||||
<%= f.submit t(".join"),
|
||||
class: "inline-flex items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-red-500" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= form_with model: ImpersonationSession.new, class: "flex items-center space-x-2" do |f| %>
|
||||
<%= f.text_field :impersonated_id, class: "rounded-md text-sm border-0 focus:ring-0 ring-0 text-black font-mono w-96", placeholder: "UUID", autocomplete: "off" %>
|
||||
<%= f.submit "Request Impersonation", class: "inline-flex items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-red-500" %>
|
||||
<%= f.text_field :impersonated_id, class: "rounded-md text-sm border-0 focus:ring-0 ring-0 text-black font-mono w-96", placeholder: t(".uuid_placeholder"), autocomplete: "off" %>
|
||||
<%= f.submit t(".request_impersonation"), class: "inline-flex items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-red-500" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
<div class="bg-container border border-tertiary rounded-lg p-3 flex flex-col md:flex-row items-start md:items-center justify-between gap-2 md:gap-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon "check-circle", size: "sm", color: "success" %>
|
||||
<p class="text-success text-sm">Your data has been cleaned</p>
|
||||
<p class="text-success text-sm"><%= t(".data_cleaned") %></p>
|
||||
</div>
|
||||
|
||||
<%= render DS::Link.new(
|
||||
text: "Next step",
|
||||
text: t(".next_step"),
|
||||
variant: "primary",
|
||||
href: @import.is_a?(PdfImport) ? import_path(@import) : import_confirm_path(@import),
|
||||
frame: :_top,
|
||||
@@ -35,8 +35,8 @@
|
||||
|
||||
<div class="flex justify-center w-full md:w-auto">
|
||||
<div class="bg-surface-inset rounded-lg inline-flex p-1 space-x-2 text-sm text-primary font-medium w-full md:w-auto">
|
||||
<%= link_to "All rows", import_clean_path(@import, per_page: params[:per_page], view: "all"), class: "p-2 rounded-lg flex-1 md:flex-auto text-center #{params[:view] != 'errors' ? 'bg-container' : ''}" %>
|
||||
<%= link_to "Error rows", import_clean_path(@import, per_page: params[:per_page], view: "errors"), class: "p-2 rounded-lg flex-1 md:flex-auto text-center #{params[:view] == 'errors' ? 'bg-container' : ''}" %>
|
||||
<%= link_to t(".all_rows"), import_clean_path(@import, per_page: params[:per_page], view: "all"), class: "p-2 rounded-lg flex-1 md:flex-auto text-center #{params[:view] != 'errors' ? 'bg-container' : ''}" %>
|
||||
<%= link_to t(".error_rows"), import_clean_path(@import, per_page: params[:per_page], view: "errors"), class: "p-2 rounded-lg flex-1 md:flex-auto text-center #{params[:view] == 'errors' ? 'bg-container' : ''}" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<%# locals: (import:) %>
|
||||
|
||||
<%= styled_form_with model: @import, url: import_configuration_path(@import), scope: :import, method: :patch, class: "space-y-4" do |form| %>
|
||||
<%= form.select :entity_type_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Entity Type" } %>
|
||||
<%= form.select :name_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Name" }, required: true %>
|
||||
<%= form.select :amount_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Balance" }, required: true %>
|
||||
<%= form.select :currency_col_label, import.csv_headers, { include_blank: "Default", label: "Currency" } %>
|
||||
<%= form.select :entity_type_col_label, import.csv_headers, { include_blank: t(".leave_empty"), label: t(".entity_type") } %>
|
||||
<%= form.select :name_col_label, import.csv_headers, { include_blank: t(".leave_empty"), label: t(".name") }, required: true %>
|
||||
<%= form.select :amount_col_label, import.csv_headers, { include_blank: t(".leave_empty"), label: t(".balance") }, required: true %>
|
||||
<%= form.select :currency_col_label, import.csv_headers, { include_blank: t(".default"), label: t(".currency") } %>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<%= form.select :date_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Balance Date" } %>
|
||||
<%= form.select :date_col_label, import.csv_headers, { include_blank: t(".leave_empty"), label: t(".balance_date") } %>
|
||||
<%= form.select :date_format,
|
||||
Family::DATE_FORMATS,
|
||||
{ label: "Date Format", prompt: "Select format" },
|
||||
{ label: t(".date_format"), prompt: t(".select_format") },
|
||||
required: @import.date_col_label.present? %>
|
||||
</div>
|
||||
|
||||
<%= form.submit "Apply configuration", disabled: import.complete? %>
|
||||
<%= form.submit t(".apply_configuration"), disabled: import.complete? %>
|
||||
<% end %>
|
||||
|
||||
@@ -3,38 +3,38 @@
|
||||
<%= styled_form_with model: @import, url: import_configuration_path(@import), scope: :import, method: :patch, class: "space-y-4" do |form| %>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<%= form.select :date_col_label, import.csv_headers, { include_blank: "Select column", label: "Date" }, required: true %>
|
||||
<%= form.select :date_col_label, import.csv_headers, { include_blank: t(".select_column"), label: t(".date_label") }, required: true %>
|
||||
<%= form.select :date_format, Family::DATE_FORMATS, { label: t(".date_format_label")}, label: true, required: true %>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<%= form.select :qty_col_label, import.csv_headers, { include_blank: "Select column", label: "Quantity" }, required: true %>
|
||||
<%= form.select :signage_convention, [["Buys are positive qty", "inflows_positive"], ["Buys are negative qty", "inflows_negative"]], label: true, required: true %>
|
||||
<%= form.select :qty_col_label, import.csv_headers, { include_blank: t(".select_column"), label: t(".quantity_label") }, required: true %>
|
||||
<%= form.select :signage_convention, [[t(".buys_are_positive"), "inflows_positive"], [t(".buys_are_negative"), "inflows_negative"]], label: true, required: true %>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<%= form.select :currency_col_label, import.csv_headers, { include_blank: "Default", label: "Currency" } %>
|
||||
<%= form.select :number_format, Import::NUMBER_FORMATS.keys, { label: "Format", prompt: "Select format" }, required: true %>
|
||||
<%= form.select :currency_col_label, import.csv_headers, { include_blank: t(".default"), label: t(".currency_label") } %>
|
||||
<%= form.select :number_format, Import::NUMBER_FORMATS.keys, { label: t(".format_label"), prompt: t(".select_format") }, required: true %>
|
||||
</div>
|
||||
|
||||
<%= form.select :ticker_col_label, import.csv_headers, { include_blank: "Select column", label: "Ticker" }, required: true %>
|
||||
<%= form.select :exchange_operating_mic_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Stock exchange code" } %>
|
||||
<%= form.select :price_col_label, import.csv_headers, { include_blank: "Select column", label: "Price" }, required: true %>
|
||||
<%= form.select :ticker_col_label, import.csv_headers, { include_blank: t(".select_column"), label: t(".ticker_label") }, required: true %>
|
||||
<%= form.select :exchange_operating_mic_col_label, import.csv_headers, { include_blank: t(".leave_empty"), label: t(".stock_exchange_code_label") } %>
|
||||
<%= form.select :price_col_label, import.csv_headers, { include_blank: t(".select_column"), label: t(".price_label") }, required: true %>
|
||||
|
||||
<% unless import.account.present? %>
|
||||
<%= form.select :account_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Account" } %>
|
||||
<%= form.select :account_col_label, import.csv_headers, { include_blank: t(".leave_empty"), label: t(".account_label") } %>
|
||||
<% end %>
|
||||
|
||||
<%= form.select :name_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Name" } %>
|
||||
<%= form.select :name_col_label, import.csv_headers, { include_blank: t(".leave_empty"), label: t(".name_label") } %>
|
||||
|
||||
<% unless Security.providers.any? %>
|
||||
<div class="alert alert-warning">
|
||||
<p>
|
||||
<strong>Note:</strong> The security prices provider is not configured. Your trade imports will work, but Sure will not backfill price history. Please go to your settings to configure this.
|
||||
<strong><%= t(".note_label") %>:</strong> <%= t(".no_security_provider_warning") %>
|
||||
</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= form.submit "Apply configuration", disabled: import.complete? %>
|
||||
<%= form.submit t(".apply_configuration"), disabled: import.complete? %>
|
||||
<% end %>
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
<div class="flex items-center gap-4">
|
||||
<%= form.select :date_col_label,
|
||||
import.csv_headers,
|
||||
{ label: "Date", prompt: "Select column" },
|
||||
{ label: t(".date_label"), prompt: t(".select_column") },
|
||||
required: true %>
|
||||
<%= form.select :date_format,
|
||||
Family::DATE_FORMATS,
|
||||
{ label: t(".date_format_label"), prompt: "Select format" },
|
||||
{ label: t(".date_format_label"), prompt: t(".select_format") },
|
||||
required: true %>
|
||||
</div>
|
||||
|
||||
@@ -31,21 +31,21 @@
|
||||
<div class="flex items-center gap-4">
|
||||
<%= form.select :amount_col_label,
|
||||
import.csv_headers,
|
||||
{ label: "Amount", container_class: "w-2/5", prompt: "Select column" },
|
||||
{ label: t(".amount_label"), container_class: "w-2/5", prompt: t(".select_column") },
|
||||
required: true %>
|
||||
<%= form.select :currency_col_label,
|
||||
import.csv_headers,
|
||||
{ include_blank: "Default", label: "Currency", container_class: "w-1/5" } %>
|
||||
{ include_blank: t(".default"), label: t(".currency_label"), container_class: "w-1/5" } %>
|
||||
<%= form.select :number_format,
|
||||
Import::NUMBER_FORMATS.keys,
|
||||
{ label: "Format", prompt: "Select format", container_class: "w-2/5" },
|
||||
{ label: t(".format_label"), prompt: t(".select_format"), container_class: "w-2/5" },
|
||||
required: true %>
|
||||
</div>
|
||||
|
||||
<%# Amount Type Strategy %>
|
||||
<%= form.select :amount_type_strategy,
|
||||
Import::AMOUNT_TYPE_STRATEGIES.map { |strategy| [strategy.humanize, strategy] },
|
||||
{ label: "Amount type strategy", prompt: "Select strategy" },
|
||||
{ label: t(".amount_type_strategy_label"), prompt: t(".select_strategy") },
|
||||
required: true,
|
||||
data: {
|
||||
action: "import#handleAmountTypeStrategyChange",
|
||||
@@ -58,8 +58,8 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm shrink-0 text-secondary">↪</span>
|
||||
<%= form.select :signage_convention,
|
||||
[["Incomes are positive", "inflows_positive"], ["Incomes are negative", "inflows_negative"]],
|
||||
{ label: "Amount type", prompt: "Select convention" },
|
||||
[[t(".incomes_are_positive"), "inflows_positive"], [t(".incomes_are_negative"), "inflows_negative"]],
|
||||
{ label: t(".amount_type_label"), prompt: t(".select_convention") },
|
||||
required: @import.amount_type_strategy == "signed_amount" %>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -70,32 +70,32 @@
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<span class="shrink-0 text-secondary">↪</span>
|
||||
<span class="text-secondary">Set</span>
|
||||
<span class="text-secondary"><%= t(".set") %></span>
|
||||
<%= form.select :entity_type_col_label,
|
||||
import.csv_headers,
|
||||
{ prompt: "Select column", container_class: "w-48 px-3 py-1.5 border border-secondary rounded-md" },
|
||||
{ prompt: t(".select_column"), container_class: "w-48 px-3 py-1.5 border border-secondary rounded-md" },
|
||||
required: @import.amount_type_strategy == "custom_column",
|
||||
data: { action: "import#handleAmountTypeChange" } %>
|
||||
<span class="text-secondary">as amount type column</span>
|
||||
<span class="text-secondary"><%= t(".as_amount_type_column") %></span>
|
||||
</div>
|
||||
|
||||
<div class="items-center gap-2 text-sm <%= @import.entity_type_col_label.nil? ? "hidden" : "flex" %>" data-import-target="amountTypeValue">
|
||||
<span class="shrink-0 text-secondary">↪</span>
|
||||
<span class="text-secondary">Set</span>
|
||||
<span class="text-secondary"><%= t(".set") %></span>
|
||||
<%= form.select :amount_type_identifier_value,
|
||||
@import.selectable_amount_type_values,
|
||||
{ prompt: "Select value", container_class: "w-48 px-3 py-1.5 border border-secondary rounded-md" },
|
||||
{ prompt: t(".select_value"), container_class: "w-48 px-3 py-1.5 border border-secondary rounded-md" },
|
||||
required: @import.amount_type_strategy == "custom_column",
|
||||
data: { action: "import#handleAmountTypeIdentifierChange" } %>
|
||||
<span class="text-secondary">as identifier value</span>
|
||||
<span class="text-secondary"><%= t(".as_identifier_value") %></span>
|
||||
</div>
|
||||
|
||||
<div class="items-center gap-2 text-sm <%= @import.amount_type_identifier_value.nil? ? "hidden" : "flex" %>" data-import-target="amountTypeInflowValue">
|
||||
<span class="shrink-0 text-secondary">↪</span>
|
||||
<span class="text-secondary">Treat "<span class="font-medium"><%= @import.amount_type_identifier_value %></span>" as</span>
|
||||
<span class="text-secondary"><%= t(".treat_as_html", value: @import.amount_type_identifier_value) %></span>
|
||||
<%= form.select :amount_type_inflow_value,
|
||||
[["Income (inflow)", "inflows_positive"], ["Expense (outflow)", "inflows_negative"]],
|
||||
{ prompt: "Select type", container_class: "w-48 px-3 py-1.5 border border-secondary rounded-md" },
|
||||
[[t(".income_inflow"), "inflows_positive"], [t(".expense_outflow"), "inflows_negative"]],
|
||||
{ prompt: t(".select_type"), container_class: "w-48 px-3 py-1.5 border border-secondary rounded-md" },
|
||||
required: @import.amount_type_strategy == "custom_column" %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -105,21 +105,21 @@
|
||||
<% unless import.account.present? %>
|
||||
<%= form.select :account_col_label,
|
||||
import.csv_headers,
|
||||
{ include_blank: "Leave empty", label: "Account" } %>
|
||||
{ include_blank: t(".leave_empty"), label: t(".account_label") } %>
|
||||
<% end %>
|
||||
|
||||
<%= form.select :name_col_label,
|
||||
import.csv_headers,
|
||||
{ include_blank: "Leave empty", label: "Name" } %>
|
||||
{ include_blank: t(".leave_empty"), label: t(".name_label") } %>
|
||||
<%= form.select :category_col_label,
|
||||
import.csv_headers,
|
||||
{ include_blank: "Leave empty", label: "Category" } %>
|
||||
{ include_blank: t(".leave_empty"), label: t(".category_label") } %>
|
||||
<%= form.select :tags_col_label,
|
||||
import.csv_headers,
|
||||
{ include_blank: "Leave empty", label: "Tags" } %>
|
||||
{ include_blank: t(".leave_empty"), label: t(".tags_label") } %>
|
||||
<%= form.select :notes_col_label,
|
||||
import.csv_headers,
|
||||
{ include_blank: "Leave empty", label: "Notes" } %>
|
||||
{ include_blank: t(".leave_empty"), label: t(".notes_label") } %>
|
||||
|
||||
<%= form.submit "Apply configuration", disabled: import.complete? %>
|
||||
<%= form.submit t(".apply_configuration"), disabled: import.complete? %>
|
||||
<% end %>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<%= tag.p t(".no_accounts"), class: "text-sm" %>
|
||||
|
||||
<%= render DS::Link.new(
|
||||
text: "Create account",
|
||||
text: t(".create_account"),
|
||||
variant: "primary",
|
||||
href: new_account_path(return_to: import_confirm_path(import)),
|
||||
frame: :modal
|
||||
@@ -60,7 +60,7 @@
|
||||
|
||||
<div class="flex justify-center w-full">
|
||||
<%= render DS::Link.new(
|
||||
text: "Next",
|
||||
text: t(".next"),
|
||||
variant: "primary",
|
||||
href: is_last_step ? import_path(import) : url_for(step: step_idx + 2),
|
||||
icon: "arrow-right",
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
<%# ── Standard CSV upload ── %>
|
||||
<div class="space-y-4" data-controller="drag-and-drop-import">
|
||||
<!-- Overlay -->
|
||||
<%= render "imports/drag_drop_overlay", title: "Drop CSV to upload", subtitle: "Your file will be uploaded automatically" %>
|
||||
<%= render "imports/drag_drop_overlay", title: t(".drop_csv_title"), subtitle: t(".drop_csv_subtitle") %>
|
||||
|
||||
<div class="space-y-4 mx-auto max-w-md">
|
||||
<div class="text-center space-y-2">
|
||||
@@ -103,8 +103,8 @@
|
||||
|
||||
<%= render DS::Tabs.new(active_tab: params[:tab] || "csv-upload", url_param_key: "tab", testid: "import-tabs") do |tabs| %>
|
||||
<% tabs.with_nav do |nav| %>
|
||||
<% nav.with_btn(id: "csv-upload", label: "Upload CSV") %>
|
||||
<% nav.with_btn(id: "csv-paste", label: "Copy & Paste") %>
|
||||
<% nav.with_btn(id: "csv-upload", label: t(".upload_csv_tab")) %>
|
||||
<% nav.with_btn(id: "csv-paste", label: t(".copy_paste_tab")) %>
|
||||
<% end %>
|
||||
|
||||
<% tabs.with_panel(tab_id: "csv-upload") do %>
|
||||
@@ -112,7 +112,7 @@
|
||||
<%= form.select :col_sep, Import::SEPARATORS, label: true %>
|
||||
|
||||
<% if @import.type == "TransactionImport" || @import.type == "TradeImport" %>
|
||||
<%= form.select :account_id, @import.family.accounts.visible.alphabetically.pluck(:name, :id), { label: "Account (optional)", include_blank: "Multi-account import", selected: @import.account_id } %>
|
||||
<%= form.select :account_id, @import.family.accounts.visible.alphabetically.pluck(:name, :id), { label: t(".account_optional_label"), include_blank: t(".multi_account_import"), selected: @import.account_id } %>
|
||||
<% end %>
|
||||
|
||||
<label for="import_import_file_csv" class="flex flex-col items-center justify-center w-full h-64 border border-secondary border-dashed rounded-xl cursor-pointer" data-controller="file-upload" data-file-upload-target="uploadArea">
|
||||
@@ -135,7 +135,7 @@
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<%= form.submit "Upload CSV", disabled: @import.complete? %>
|
||||
<%= form.submit t(".upload_csv_button"), disabled: @import.complete? %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -144,16 +144,16 @@
|
||||
<%= form.select :col_sep, Import::SEPARATORS, label: true %>
|
||||
|
||||
<% if @import.type == "TransactionImport" || @import.type == "TradeImport" %>
|
||||
<%= form.select :account_id, @import.family.accounts.visible.alphabetically.pluck(:name, :id), { label: "Account (optional)", include_blank: "Multi-account import", selected: @import.account_id } %>
|
||||
<%= form.select :account_id, @import.family.accounts.visible.alphabetically.pluck(:name, :id), { label: t(".account_optional_label"), include_blank: t(".multi_account_import"), selected: @import.account_id } %>
|
||||
<% end %>
|
||||
|
||||
<%= form.text_area :raw_file_str,
|
||||
rows: 10,
|
||||
required: true,
|
||||
placeholder: "Paste your CSV file contents here",
|
||||
placeholder: t(".paste_csv_placeholder"),
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
|
||||
<%= form.submit "Upload CSV", disabled: @import.complete? %>
|
||||
<%= form.submit t(".upload_csv_button"), disabled: @import.complete? %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -161,7 +161,7 @@
|
||||
|
||||
<div class="flex justify-center">
|
||||
<span class="text-secondary text-sm">
|
||||
<%= link_to "Download a sample CSV", "/imports/#{@import.id}/upload/sample_csv", class: "text-primary underline", data: { turbo: false } %> to see the required CSV format
|
||||
<%= link_to t(".download_sample_csv"), "/imports/#{@import.id}/upload/sample_csv", class: "text-primary underline", data: { turbo: false } %> <%= t(".to_see_format") %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
</div>
|
||||
|
||||
<div class="text-center space-y-2">
|
||||
<h1 class="font-medium text-primary text-center text-3xl">Import failed</h1>
|
||||
<p class="text-sm text-secondary">Please check that your file format, for any errors and that all required fields are filled, then come back and try again.</p>
|
||||
<h1 class="font-medium text-primary text-center text-3xl"><%= t(".title") %></h1>
|
||||
<p class="text-sm text-secondary"><%= t(".description") %></p>
|
||||
</div>
|
||||
|
||||
<%= render DS::Button.new(text: "Try again", href: publish_import_path(import), full_width: true) %>
|
||||
<%= render DS::Button.new(text: t(".try_again"), href: publish_import_path(import), full_width: true) %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
</div>
|
||||
|
||||
<div class="text-center space-y-2">
|
||||
<h1 class="font-medium text-primary text-center text-3xl">Import in progress</h1>
|
||||
<p class="text-sm text-secondary">Your import is in progress. Check the imports menu for status updates or click 'Check Status' to refresh the page for updates. Feel free to continue using the app.</p>
|
||||
<h1 class="font-medium text-primary text-center text-3xl"><%= t(".title") %></h1>
|
||||
<p class="text-sm text-secondary"><%= t(".description") %></p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 flex flex-col">
|
||||
<%= render DS::Link.new(text: "Check status", href: import_path(import), variant: "primary", full_width: true) %>
|
||||
<%= render DS::Link.new(text: "Back to dashboard", href: root_path, variant: "secondary", full_width: true) %>
|
||||
<%= render DS::Link.new(text: t(".check_status"), href: import_path(import), variant: "primary", full_width: true) %>
|
||||
<%= render DS::Link.new(text: t(".back_to_dashboard"), href: root_path, variant: "secondary", full_width: true) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
</div>
|
||||
|
||||
<div class="text-center space-y-2">
|
||||
<h1 class="font-medium text-primary text-center text-3xl">Reverting import failed</h1>
|
||||
<p class="text-sm text-secondary">Please try again</p>
|
||||
<h1 class="font-medium text-primary text-center text-3xl"><%= t(".title") %></h1>
|
||||
<p class="text-sm text-secondary"><%= t(".description") %></p>
|
||||
</div>
|
||||
|
||||
<%= render DS::Button.new(
|
||||
text: "Try again",
|
||||
text: t(".try_again"),
|
||||
full_width: true,
|
||||
href: revert_import_path(import)
|
||||
) %>
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
</div>
|
||||
|
||||
<div class="text-center space-y-2">
|
||||
<h1 class="font-medium text-primary text-center text-3xl">Import successful</h1>
|
||||
<p class="text-sm text-secondary">Your imported data has been successfully added to the app and is now ready for use.</p>
|
||||
<h1 class="font-medium text-primary text-center text-3xl"><%= t(".title") %></h1>
|
||||
<p class="text-sm text-secondary"><%= t(".description") %></p>
|
||||
</div>
|
||||
|
||||
<%= render DS::Link.new(
|
||||
text: "Back to dashboard",
|
||||
text: t(".back_to_dashboard"),
|
||||
variant: "primary",
|
||||
full_width: true,
|
||||
href: root_path
|
||||
|
||||
@@ -5,17 +5,17 @@
|
||||
<form method="dialog" class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<h3 class="font-medium text-primary" data-confirm-dialog-target="title">Are you sure?</h3>
|
||||
<h3 class="font-medium text-primary" data-confirm-dialog-target="title"><%= t(".are_you_sure") %></h3>
|
||||
<%= icon("x", as_button: true, type: "submit", value: "cancel") %>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-secondary" data-confirm-dialog-target="subtitle">This action cannot be undone.</p>
|
||||
<p class="text-sm text-secondary" data-confirm-dialog-target="subtitle"><%= t(".cannot_be_undone") %></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<% ["primary", "outline-destructive", "destructive"].each do |variant| %>
|
||||
<%= render DS::Button.new(
|
||||
text: "Confirm",
|
||||
text: t(".confirm"),
|
||||
variant: variant,
|
||||
autofocus: true,
|
||||
full_width: true,
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
<div class="flex justify-center py-8">
|
||||
<%= render DS::Link.new(
|
||||
text: "Edit loan details",
|
||||
text: t(".edit_loan_details"),
|
||||
variant: "ghost",
|
||||
href: edit_loan_path(account),
|
||||
frame: :modal
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
<%# locals: (error_message:, return_path:) %>
|
||||
<%= turbo_frame_tag "modal" do %>
|
||||
<%= render DS::Dialog.new do |dialog| %>
|
||||
<% dialog.with_header(title: "Lunch Flow Connection Error") %>
|
||||
<% dialog.with_header(title: t(".title")) %>
|
||||
<% dialog.with_body do %>
|
||||
<div class="space-y-4">
|
||||
<%= render DS::Alert.new(
|
||||
title: "Unable to connect to Lunch Flow",
|
||||
title: t(".unable_to_connect"),
|
||||
message: error_message,
|
||||
variant: :error
|
||||
) %>
|
||||
|
||||
<div class="bg-surface rounded-lg p-4 space-y-2 text-sm">
|
||||
<p class="font-medium text-primary">Common Issues:</p>
|
||||
<p class="font-medium text-primary"><%= t(".common_issues") %></p>
|
||||
<ul class="list-disc list-inside space-y-1 text-secondary">
|
||||
<li><strong>Invalid API Key:</strong> Check your API key in Provider Settings</li>
|
||||
<li><strong>Expired Credentials:</strong> Generate a new API key from Lunch Flow</li>
|
||||
<li><strong>Network Issue:</strong> Check your internet connection</li>
|
||||
<li><strong>Service Down:</strong> Lunch Flow API may be temporarily unavailable</li>
|
||||
<li><strong><%= t(".invalid_api_key_label") %>:</strong> <%= t(".invalid_api_key_desc") %></li>
|
||||
<li><strong><%= t(".expired_credentials_label") %>:</strong> <%= t(".expired_credentials_desc") %></li>
|
||||
<li><strong><%= t(".network_issue_label") %>:</strong> <%= t(".network_issue_desc") %></li>
|
||||
<li><strong><%= t(".service_down_label") %>:</strong> <%= t(".service_down_desc") %></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<%= link_to settings_providers_path,
|
||||
class: "inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium text-inverse button-bg-primary hover:button-bg-primary-hover focus:outline-none focus:ring-2 focus:ring-gray-900 theme-dark:focus:ring-white focus:ring-offset-2 transition-colors",
|
||||
data: { turbo: false } do %>
|
||||
Check Provider Settings
|
||||
<%= t(".check_provider_settings") %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<%= turbo_frame_tag "modal" do %>
|
||||
<%= render DS::Dialog.new do |dialog| %>
|
||||
<% dialog.with_header(title: "Lunch Flow Setup Required") %>
|
||||
<% dialog.with_header(title: t(".title")) %>
|
||||
<% dialog.with_body do %>
|
||||
<div class="space-y-4">
|
||||
<%= render DS::Alert.new(
|
||||
title: "API Key Not Configured",
|
||||
message: "Before you can link Lunch Flow accounts, you need to configure your Lunch Flow API key.",
|
||||
title: t(".api_key_not_configured"),
|
||||
message: t(".api_key_description"),
|
||||
variant: :warning
|
||||
) %>
|
||||
|
||||
<div class="bg-surface rounded-lg p-4 space-y-2 text-sm">
|
||||
<p class="font-medium text-primary">Setup Steps:</p>
|
||||
<p class="font-medium text-primary"><%= t(".setup_steps_title") %></p>
|
||||
<ol class="list-decimal list-inside space-y-1 text-secondary">
|
||||
<li>Go to <strong>Settings → Providers</strong></li>
|
||||
<li>Find the <strong>Lunch Flow</strong> section</li>
|
||||
<li>Enter your Lunch Flow API key</li>
|
||||
<li>Return here to link your accounts</li>
|
||||
<li><%= t(".setup_step_1_html") %></li>
|
||||
<li><%= t(".setup_step_2_html") %></li>
|
||||
<li><%= t(".setup_step_3") %></li>
|
||||
<li><%= t(".setup_step_4") %></li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<%= link_to settings_providers_path,
|
||||
class: "inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium text-inverse button-bg-primary hover:button-bg-primary-hover focus:outline-none focus:ring-2 focus:ring-gray-900 theme-dark:focus:ring-white focus:ring-offset-2 transition-colors",
|
||||
data: { turbo: false } do %>
|
||||
Go to Provider Settings
|
||||
<%= t(".go_to_provider_settings") %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
<%# locals: (error_message:, return_path:) %>
|
||||
<%= turbo_frame_tag "modal" do %>
|
||||
<%= render DS::Dialog.new do |dialog| %>
|
||||
<% dialog.with_header(title: "Mercury Connection Error") %>
|
||||
<% dialog.with_header(title: t(".title")) %>
|
||||
<% dialog.with_body do %>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<%= icon("alert-circle", class: "text-destructive w-5 h-5 shrink-0 mt-0.5") %>
|
||||
<div class="text-sm">
|
||||
<p class="font-medium text-primary mb-2">Unable to connect to Mercury</p>
|
||||
<p class="font-medium text-primary mb-2"><%= t(".unable_to_connect") %></p>
|
||||
<p class="text-secondary"><%= error_message %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-surface rounded-lg p-4 space-y-2 text-sm">
|
||||
<p class="font-medium text-primary">Common Issues:</p>
|
||||
<p class="font-medium text-primary"><%= t(".common_issues") %></p>
|
||||
<ul class="list-disc list-inside space-y-1 text-secondary">
|
||||
<li><strong>Invalid API Token:</strong> Check your API token in Provider Settings</li>
|
||||
<li><strong>Expired Credentials:</strong> Generate a new API token from Mercury</li>
|
||||
<li><strong>Insufficient Permissions:</strong> Ensure your token has read-only access</li>
|
||||
<li><strong>Network Issue:</strong> Check your internet connection</li>
|
||||
<li><strong>Service Down:</strong> Mercury API may be temporarily unavailable</li>
|
||||
<li><strong><%= t(".invalid_api_token_label") %>:</strong> <%= t(".invalid_api_token_desc") %></li>
|
||||
<li><strong><%= t(".expired_credentials_label") %>:</strong> <%= t(".expired_credentials_desc") %></li>
|
||||
<li><strong><%= t(".insufficient_permissions_label") %>:</strong> <%= t(".insufficient_permissions_desc") %></li>
|
||||
<li><strong><%= t(".network_issue_label") %>:</strong> <%= t(".network_issue_desc") %></li>
|
||||
<li><strong><%= t(".service_down_label") %>:</strong> <%= t(".service_down_desc") %></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<%= link_to settings_providers_path,
|
||||
class: "inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium text-inverse button-bg-primary hover:button-bg-primary-hover focus:outline-none focus:ring-2 focus:ring-gray-900 theme-dark:focus:ring-white focus:ring-offset-2 transition-colors",
|
||||
data: { turbo: false } do %>
|
||||
Check Provider Settings
|
||||
<%= t(".check_provider_settings") %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<%= turbo_frame_tag "modal" do %>
|
||||
<%= render DS::Dialog.new do |dialog| %>
|
||||
<% dialog.with_header(title: "Mercury Setup Required") %>
|
||||
<% dialog.with_header(title: t(".title")) %>
|
||||
<% dialog.with_body do %>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<%= icon("alert-circle", class: "text-warning w-5 h-5 shrink-0 mt-0.5") %>
|
||||
<div class="text-sm text-secondary">
|
||||
<p class="font-medium text-primary mb-2">API Token Not Configured</p>
|
||||
<p>Before you can link Mercury accounts, you need to configure your Mercury API token.</p>
|
||||
<p class="font-medium text-primary mb-2"><%= t(".api_token_not_configured") %></p>
|
||||
<p><%= t(".api_token_description") %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-surface rounded-lg p-4 space-y-2 text-sm">
|
||||
<p class="font-medium text-primary">Setup Steps:</p>
|
||||
<p class="font-medium text-primary"><%= t(".setup_steps_title") %></p>
|
||||
<ol class="list-decimal list-inside space-y-1 text-secondary">
|
||||
<li>Go to <strong>Settings > Providers</strong></li>
|
||||
<li>Find the <strong>Mercury</strong> section</li>
|
||||
<li>Enter your Mercury API token</li>
|
||||
<li>Return here to link your accounts</li>
|
||||
<li><%= t(".setup_step_1_html") %></li>
|
||||
<li><%= t(".setup_step_2_html") %></li>
|
||||
<li><%= t(".setup_step_3") %></li>
|
||||
<li><%= t(".setup_step_4") %></li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<%= link_to settings_providers_path,
|
||||
class: "inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium text-inverse button-bg-primary hover:button-bg-primary-hover focus:outline-none focus:ring-2 focus:ring-gray-900 theme-dark:focus:ring-white focus:ring-offset-2 transition-colors",
|
||||
data: { turbo: false } do %>
|
||||
Go to Provider Settings
|
||||
<%= t(".go_to_provider_settings") %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<%# In the future, this will be a dropdown with different AI models %>
|
||||
<%= f.hidden_field :ai_model, value: default_ai_model %>
|
||||
|
||||
<%= f.text_area :content, placeholder: "Ask anything ...", value: message_hint,
|
||||
<%= f.text_area :content, placeholder: t(".placeholder"), value: message_hint,
|
||||
class: "w-full border-0 focus:ring-0 text-sm resize-none px-1 bg-transparent",
|
||||
data: { chat_target: "input", action: "input->chat#autoResize keydown->chat#handleInputKeyDown" },
|
||||
rows: 1 %>
|
||||
@@ -27,5 +27,5 @@
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<p class="text-xs text-secondary">AI responses are informational only. Not financial advice!</p>
|
||||
<p class="text-xs text-secondary"><%= t(".disclaimer") %></p>
|
||||
</div>
|
||||
|
||||
@@ -37,13 +37,13 @@
|
||||
|
||||
<div class="bg-container-inset rounded-xl p-1 space-y-1 overflow-x-auto">
|
||||
<div class="px-4 py-2 flex items-center uppercase text-xs font-medium text-secondary">
|
||||
<div class="w-40 shrink-0">Name</div>
|
||||
<div class="w-40 shrink-0"><%= t(".name") %></div>
|
||||
<div class="ml-auto text-right flex items-center gap-2">
|
||||
<div class="w-20 shrink-0">
|
||||
<p>Weight</p>
|
||||
<p><%= t(".weight") %></p>
|
||||
</div>
|
||||
<div class="w-24 shrink-0">
|
||||
<p>Value</p>
|
||||
<p><%= t(".value") %></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user