mirror of
https://github.com/we-promise/sure.git
synced 2026-05-29 23:39:03 +00:00
Merge origin/main into feat/goals-v2-architecture
Pulls in #1853 (DS::SearchInput), #1855 (DS::Disclosure :card), #1856 (3 provider panels migration) so the goals views can migrate to the new primitives.
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
<details class="group" <%= "open" if open %>>
|
||||
<%= tag.summary class: class_names(
|
||||
"px-3 py-2 rounded-xl cursor-pointer flex items-center justify-between bg-surface",
|
||||
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-900 theme-dark:focus-visible:outline-white"
|
||||
) do %>
|
||||
<details class="<%= details_classes %>" <%= "open" if open %>>
|
||||
<%= tag.summary class: summary_classes do %>
|
||||
<% if summary_content? %>
|
||||
<%= summary_content %>
|
||||
<%# `<summary>` is `display: list-item`, so a flex inner div would
|
||||
shrink-wrap to content width and any `justify-between` inside
|
||||
the slot has nothing to distribute. Wrap in a `w-full` block
|
||||
so caller-supplied flex rows stretch across the card. %>
|
||||
<div class="w-full">
|
||||
<%= summary_content %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="flex items-center gap-3">
|
||||
<% if align == :left %>
|
||||
|
||||
@@ -1,12 +1,48 @@
|
||||
class DS::Disclosure < DesignSystemComponent
|
||||
renders_one :summary_content
|
||||
|
||||
attr_reader :title, :align, :open, :opts
|
||||
VARIANTS = %i[default card].freeze
|
||||
|
||||
def initialize(title: nil, align: "right", open: false, **opts)
|
||||
attr_reader :title, :align, :open, :variant, :opts
|
||||
|
||||
# `:default` — bg-surface summary, no chrome on the `<details>`. Use
|
||||
# for inline expanders inside a parent card.
|
||||
#
|
||||
# `:card` — `<details>` itself becomes a `bg-container shadow-border-xs
|
||||
# rounded-xl` card; the summary inherits the container (no own bg).
|
||||
# Use for provider-item rows (binance, lunchflow, plaid, etc.) where
|
||||
# each card is the surface and the summary is custom rich content.
|
||||
# Callers in `:card` mode should pass their own `summary_content`
|
||||
# slot; the built-in title rendering assumes the `:default` shape.
|
||||
def initialize(title: nil, align: "right", open: false, variant: :default, **opts)
|
||||
@title = title
|
||||
@align = align.to_sym
|
||||
@open = open
|
||||
@variant = variant.to_sym
|
||||
@opts = opts
|
||||
|
||||
raise ArgumentError, "Invalid variant: #{@variant}. Must be one of #{VARIANTS.inspect}" unless VARIANTS.include?(@variant)
|
||||
end
|
||||
|
||||
def details_classes
|
||||
case variant
|
||||
when :card
|
||||
"group bg-container p-4 shadow-border-xs rounded-xl"
|
||||
else
|
||||
"group"
|
||||
end
|
||||
end
|
||||
|
||||
def summary_classes
|
||||
case variant
|
||||
when :card
|
||||
# Card variant: no bg on summary — the parent details *is* the
|
||||
# surface. Keep cursor + focus-visible ring + flex baseline.
|
||||
# Ring token matches `settings/provider_card.html.erb` (the
|
||||
# established focus pattern on container cards).
|
||||
"list-none cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-alpha-black-300 rounded-xl"
|
||||
else
|
||||
"px-3 py-2 rounded-xl cursor-pointer flex items-center justify-between bg-surface focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-alpha-black-300"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
17
app/components/DS/search_input.html.erb
Normal file
17
app/components/DS/search_input.html.erb
Normal file
@@ -0,0 +1,17 @@
|
||||
<%= tag.div class: container_classes do %>
|
||||
<%= tag.input type: "search",
|
||||
name: name,
|
||||
value: value,
|
||||
placeholder: placeholder,
|
||||
"aria-label": aria_label,
|
||||
autocomplete: "off",
|
||||
class: input_classes,
|
||||
**opts %>
|
||||
<% if variant == :embedded %>
|
||||
<%= helpers.icon("search", class: "absolute inset-0 ml-2 transform top-1/2 -translate-y-1/2") %>
|
||||
<% else %>
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<%= helpers.icon("search", class: "text-secondary") %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
78
app/components/DS/search_input.rb
Normal file
78
app/components/DS/search_input.rb
Normal file
@@ -0,0 +1,78 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# `DS::SearchInput` is the search-field primitive.
|
||||
#
|
||||
# Two variants:
|
||||
#
|
||||
# - `:standalone` (default) — top-of-list filter inputs (Preferences
|
||||
# currency search, Settings/Bank Sync provider filter). Bordered
|
||||
# bg-container surface, icon-on-left, full token-backed focus ring.
|
||||
#
|
||||
# - `:embedded` — search-inside-a-panel (DS::Select internal search,
|
||||
# splits category filter, any future DS::Popover that hosts a filter).
|
||||
# No border / no own focus ring — the parent panel provides the
|
||||
# chrome, so adding ring + outline here would compete with the
|
||||
# parent's focus-within state.
|
||||
#
|
||||
# For `form.search_field :foo` inside a `styled_form_with` block,
|
||||
# keep using the form helper — it routes through `StyledFormBuilder`'s
|
||||
# form-field CSS, which is a different visual contract.
|
||||
class DS::SearchInput < DesignSystemComponent
|
||||
VARIANTS = %i[standalone embedded].freeze
|
||||
|
||||
attr_reader :variant, :name, :placeholder, :value, :aria_label, :extra_classes, :opts
|
||||
|
||||
def initialize(variant: :standalone, name: nil, placeholder: nil, value: nil, aria_label: nil, class: nil, **opts)
|
||||
@variant = variant.to_sym
|
||||
@name = name
|
||||
@placeholder = placeholder
|
||||
@value = value
|
||||
@aria_label = aria_label || placeholder
|
||||
@extra_classes = binding.local_variable_get(:class)
|
||||
@opts = opts
|
||||
|
||||
raise ArgumentError, "Invalid variant: #{@variant}. Must be one of #{VARIANTS.inspect}" unless VARIANTS.include?(@variant)
|
||||
end
|
||||
|
||||
def container_classes
|
||||
class_names("relative", extra_classes)
|
||||
end
|
||||
|
||||
def input_classes
|
||||
# `text-base sm:text-sm` — keep the base font at 16px so iOS Safari
|
||||
# does not zoom the viewport when the input is focused. Shrink to
|
||||
# 14px from `sm:` upward. The previous unconditional `text-sm`
|
||||
# triggered the mobile zoom regression.
|
||||
case variant
|
||||
when :embedded
|
||||
# No own focus ring — the parent panel handles focus chrome via
|
||||
# `focus-within`. `focus:outline-hidden focus:ring-0` neutralizes
|
||||
# the browser default so it doesn't compete with the panel's
|
||||
# state.
|
||||
"bg-container text-primary text-base sm:text-sm placeholder:text-secondary font-normal " \
|
||||
"h-10 pl-10 w-full border-none rounded-lg " \
|
||||
"focus:outline-hidden focus:ring-0"
|
||||
else
|
||||
# `focus-visible:outline-*` matches the focus-ring pattern from
|
||||
# DS::Button (base.css) so every interactive surface in the design
|
||||
# system uses the same ring token. Replaces the broken
|
||||
# `focus:ring-gray-500` from the inline callsites — that utility
|
||||
# had no backing token and rendered invisibly on the bordered
|
||||
# bg-container surface.
|
||||
"block w-full border border-secondary rounded-md py-2.5 pl-10 pr-3 bg-container text-base sm:text-sm " \
|
||||
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-900 " \
|
||||
"theme-dark:focus-visible:outline-white"
|
||||
end
|
||||
end
|
||||
|
||||
def icon_classes
|
||||
variant == :embedded ? "absolute inset-0 ml-2 transform top-1/2 -translate-y-1/2" : "text-secondary"
|
||||
end
|
||||
|
||||
def icon_wrapper_classes
|
||||
# Standalone variant wraps the icon in a positioned div; embedded
|
||||
# places the icon as an absolutely-positioned sibling so the parent
|
||||
# panel can stay in control of vertical alignment.
|
||||
variant == :embedded ? nil : "absolute inset-0 ml-2 top-1/2 -translate-y-1/2 pointer-events-none"
|
||||
end
|
||||
end
|
||||
@@ -33,15 +33,15 @@
|
||||
</div>
|
||||
<div class="absolute z-50 p-1.5 w-full min-w-32 rounded-lg shadow-lg shadow-border-xs bg-container mt-1.5 transition duration-150 ease-out -translate-y-1 opacity-0 hidden" data-select-target="menu">
|
||||
<% if searchable %>
|
||||
<div class="relative flex items-center bg-container border border-secondary rounded-lg mb-1">
|
||||
<input type="search"
|
||||
placeholder="<%= t("helpers.select.search_placeholder") %>"
|
||||
autocomplete="off"
|
||||
aria-label="<%= t("helpers.select.search_placeholder") %>"
|
||||
class="bg-container text-primary text-sm placeholder:text-secondary font-normal h-10 pl-10 w-full border-none rounded-lg focus:outline-hidden focus:ring-0"
|
||||
data-list-filter-target="input"
|
||||
data-action="input->list-filter#filter input->select#syncTabindex">
|
||||
<%= helpers.icon("search", class: "absolute inset-0 ml-2 transform top-1/2 -translate-y-1/2") %>
|
||||
<div class="flex items-center bg-container border border-secondary rounded-lg mb-1 focus-within:ring-4 focus-within:ring-alpha-black-200 theme-dark:focus-within:ring-alpha-white-300 transition-shadow">
|
||||
<%= render DS::SearchInput.new(
|
||||
variant: :embedded,
|
||||
placeholder: t("helpers.select.search_placeholder"),
|
||||
data: {
|
||||
list_filter_target: "input",
|
||||
action: "input->list-filter#filter input->select#syncTabindex"
|
||||
}
|
||||
) %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div data-list-filter-target="list" data-select-target="content" class="flex flex-col gap-0.5 max-h-64 overflow-auto"
|
||||
|
||||
@@ -1,49 +1,51 @@
|
||||
<%# locals: (binance_item:, unlinked_count: binance_item.unlinked_accounts_count) %>
|
||||
|
||||
<%= tag.div id: dom_id(binance_item) do %>
|
||||
<details open class="group bg-container p-4 shadow-border-xs rounded-xl">
|
||||
<summary class="flex items-center gap-2">
|
||||
<%= icon "chevron-right", class: "group-open:transform group-open:rotate-90" %>
|
||||
<%= render DS::Disclosure.new(variant: :card, open: true) do |disclosure| %>
|
||||
<% disclosure.with_summary_content do %>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon "chevron-right", class: "group-open:rotate-90 motion-safe:transition-transform motion-safe:duration-150" %>
|
||||
|
||||
<div class="flex items-center justify-center h-8 w-8 rounded-full" style="background-color: rgba(240, 185, 11, 0.15);">
|
||||
<div class="flex items-center justify-center">
|
||||
<%= icon "coins", size: "sm", class: "text-[#F0B90B]" %>
|
||||
<div class="flex items-center justify-center h-8 w-8 rounded-full" style="background-color: rgba(240, 185, 11, 0.15);">
|
||||
<div class="flex items-center justify-center">
|
||||
<%= icon "coins", size: "sm", class: "text-[#F0B90B]" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pl-1 text-sm flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= tag.p binance_item.institution_display_name, class: "font-medium text-primary" %>
|
||||
<% if binance_item.scheduled_for_deletion? %>
|
||||
<p class="text-destructive text-sm animate-pulse"><%= t(".deletion_in_progress") %></p>
|
||||
<div class="pl-1 text-sm flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= tag.p binance_item.institution_display_name, class: "font-medium text-primary" %>
|
||||
<% if binance_item.scheduled_for_deletion? %>
|
||||
<p class="text-destructive text-sm animate-pulse"><%= t(".deletion_in_progress") %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="text-xs text-secondary"><%= t(".provider_name") %></p>
|
||||
<% if binance_item.syncing? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= icon "loader", size: "sm", class: "animate-spin" %>
|
||||
<%= tag.span t(".syncing") %>
|
||||
</div>
|
||||
<% elsif binance_item.requires_update? %>
|
||||
<div class="text-warning flex items-center gap-1">
|
||||
<%= icon "alert-triangle", size: "sm", color: "warning" %>
|
||||
<%= tag.span t(".reconnect") %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-secondary">
|
||||
<% if binance_item.last_synced_at %>
|
||||
<% if binance_item.sync_status_summary %>
|
||||
<%= t(".status_with_summary", timestamp: time_ago_in_words(binance_item.last_synced_at), summary: binance_item.sync_status_summary) %>
|
||||
<% else %>
|
||||
<%= t(".status", timestamp: time_ago_in_words(binance_item.last_synced_at)) %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= t(".status_never") %>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="text-xs text-secondary"><%= t(".provider_name") %></p>
|
||||
<% if binance_item.syncing? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= icon "loader", size: "sm", class: "animate-spin" %>
|
||||
<%= tag.span t(".syncing") %>
|
||||
</div>
|
||||
<% elsif binance_item.requires_update? %>
|
||||
<div class="text-warning flex items-center gap-1">
|
||||
<%= icon "alert-triangle", size: "sm", color: "warning" %>
|
||||
<%= tag.span t(".reconnect") %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-secondary">
|
||||
<% if binance_item.last_synced_at %>
|
||||
<% if binance_item.sync_status_summary %>
|
||||
<%= t(".status_with_summary", timestamp: time_ago_in_words(binance_item.last_synced_at), summary: binance_item.sync_status_summary) %>
|
||||
<% else %>
|
||||
<%= t(".status", timestamp: time_ago_in_words(binance_item.last_synced_at)) %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= t(".status_never") %>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</summary>
|
||||
<% end %>
|
||||
|
||||
<% if Current.user&.admin? %>
|
||||
<div class="flex items-center justify-end gap-2 mt-2">
|
||||
@@ -128,5 +130,5 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</details>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -12,82 +12,84 @@
|
||||
<% institutions_count = render_locals[:institutions_count] %>
|
||||
|
||||
<%= tag.div id: dom_id(brex_item) do %>
|
||||
<details open class="group bg-container p-4 shadow-border-xs rounded-xl">
|
||||
<summary class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon "chevron-right", class: "group-open:transform group-open:rotate-90" %>
|
||||
<%= render DS::Disclosure.new(variant: :card, open: true) do |disclosure| %>
|
||||
<% disclosure.with_summary_content do %>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon "chevron-right", class: "group-open:rotate-90 motion-safe:transition-transform motion-safe:duration-150" %>
|
||||
|
||||
<div class="flex items-center justify-center h-8 w-8 bg-container-inset rounded-full">
|
||||
<% if brex_item.logo.attached? %>
|
||||
<%= image_tag brex_item.logo, class: "rounded-full h-full w-full", loading: "lazy" %>
|
||||
<% else %>
|
||||
<div class="flex items-center justify-center">
|
||||
<%= tag.p brex_item.name.first.upcase, class: "text-primary text-xs font-medium" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="pl-1 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= tag.p brex_item.name, class: "font-medium text-primary" %>
|
||||
<% if brex_item.scheduled_for_deletion? %>
|
||||
<%= tag.p t(".deletion_in_progress"), class: "text-destructive text-sm animate-pulse" %>
|
||||
<div class="flex items-center justify-center h-8 w-8 bg-container-inset rounded-full">
|
||||
<% if brex_item.logo.attached? %>
|
||||
<%= image_tag brex_item.logo, class: "rounded-full h-full w-full", loading: "lazy" %>
|
||||
<% else %>
|
||||
<div class="flex items-center justify-center">
|
||||
<%= tag.p brex_item.name.first.upcase, class: "text-primary text-xs font-medium" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if brex_item.accounts.any? %>
|
||||
<p class="text-xs text-secondary">
|
||||
<%= brex_item.institution_summary %>
|
||||
</p>
|
||||
<% end %>
|
||||
<% if brex_item.syncing? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= icon "loader", size: "sm", class: "animate-spin" %>
|
||||
<%= tag.span t(".syncing") %>
|
||||
</div>
|
||||
<% elsif brex_item.sync_error.present? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= render DS::Tooltip.new(text: brex_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive", as: :span) %>
|
||||
<%= tag.span t(".error"), class: "text-destructive" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-secondary">
|
||||
<% if brex_item.last_synced_at %>
|
||||
<% if brex_item.sync_status_summary %>
|
||||
<%= t(".status_with_summary", timestamp: time_ago_in_words(brex_item.last_synced_at), summary: brex_item.sync_status_summary) %>
|
||||
<% else %>
|
||||
<%= t(".status", timestamp: time_ago_in_words(brex_item.last_synced_at)) %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= t(".status_never") %>
|
||||
|
||||
<div class="pl-1 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= tag.p brex_item.name, class: "font-medium text-primary" %>
|
||||
<% if brex_item.scheduled_for_deletion? %>
|
||||
<%= tag.p t(".deletion_in_progress"), class: "text-destructive text-sm animate-pulse" %>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if brex_item.accounts.any? %>
|
||||
<p class="text-xs text-secondary">
|
||||
<%= brex_item.institution_summary %>
|
||||
</p>
|
||||
<% end %>
|
||||
<% if brex_item.syncing? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= icon "loader", size: "sm", class: "animate-spin" %>
|
||||
<%= tag.span t(".syncing") %>
|
||||
</div>
|
||||
<% elsif brex_item.sync_error.present? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= render DS::Tooltip.new(text: brex_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive", as: :span) %>
|
||||
<%= tag.span t(".error"), class: "text-destructive" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-secondary">
|
||||
<% if brex_item.last_synced_at %>
|
||||
<% if brex_item.sync_status_summary %>
|
||||
<%= t(".status_with_summary", timestamp: time_ago_in_words(brex_item.last_synced_at), summary: brex_item.sync_status_summary) %>
|
||||
<% else %>
|
||||
<%= t(".status", timestamp: time_ago_in_words(brex_item.last_synced_at)) %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= t(".status_never") %>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if Current.user&.admin? %>
|
||||
<div class="flex items-center gap-2">
|
||||
<% if Rails.env.development? %>
|
||||
<%= icon(
|
||||
"refresh-cw",
|
||||
as_button: true,
|
||||
href: sync_brex_item_path(brex_item)
|
||||
) %>
|
||||
<% end %>
|
||||
|
||||
<%= render DS::Menu.new do |menu| %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: t(".delete"),
|
||||
icon: "trash-2",
|
||||
href: brex_item_path(brex_item),
|
||||
method: :delete,
|
||||
confirm: CustomConfirm.for_resource_deletion(brex_item.name, high_severity: true)
|
||||
) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if Current.user&.admin? %>
|
||||
<div class="flex items-center gap-2">
|
||||
<% if Rails.env.development? %>
|
||||
<%= icon(
|
||||
"refresh-cw",
|
||||
as_button: true,
|
||||
href: sync_brex_item_path(brex_item)
|
||||
) %>
|
||||
<% end %>
|
||||
|
||||
<%= render DS::Menu.new do |menu| %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: t(".delete"),
|
||||
icon: "trash-2",
|
||||
href: brex_item_path(brex_item),
|
||||
method: :delete,
|
||||
confirm: CustomConfirm.for_resource_deletion(brex_item.name, high_severity: true)
|
||||
) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</summary>
|
||||
<% end %>
|
||||
|
||||
<% unless brex_item.scheduled_for_deletion? %>
|
||||
<div class="space-y-4 mt-4">
|
||||
@@ -128,5 +130,5 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</details>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -16,92 +16,94 @@
|
||||
end
|
||||
end %>
|
||||
|
||||
<details open class="group bg-container p-4 shadow-border-xs rounded-xl">
|
||||
<summary class="flex items-center justify-between gap-2 focus-visible:outline-hidden">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon "chevron-right", class: "group-open:transform group-open:rotate-90" %>
|
||||
<%= render DS::Disclosure.new(variant: :card, open: true) do |disclosure| %>
|
||||
<% disclosure.with_summary_content do %>
|
||||
<div class="flex items-center justify-between gap-2 focus-visible:outline-hidden">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon "chevron-right", class: "group-open:rotate-90 motion-safe:transition-transform motion-safe:duration-150" %>
|
||||
|
||||
<div class="flex items-center justify-center h-8 w-8 rounded-full" style="background-color: rgba(0, 82, 255, 0.1);">
|
||||
<div class="flex items-center justify-center">
|
||||
<%= icon "bitcoin", size: "sm", class: "text-[#0052FF]" %>
|
||||
<div class="flex items-center justify-center h-8 w-8 rounded-full" style="background-color: rgba(0, 82, 255, 0.1);">
|
||||
<div class="flex items-center justify-center">
|
||||
<%= icon "bitcoin", size: "sm", class: "text-[#0052FF]" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pl-1 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= tag.p coinbase_item.institution_display_name, class: "font-medium text-primary" %>
|
||||
<% if coinbase_item.scheduled_for_deletion? %>
|
||||
<p class="text-destructive text-sm animate-pulse"><%= t(".deletion_in_progress") %></p>
|
||||
<div class="pl-1 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= tag.p coinbase_item.institution_display_name, class: "font-medium text-primary" %>
|
||||
<% if coinbase_item.scheduled_for_deletion? %>
|
||||
<p class="text-destructive text-sm animate-pulse"><%= t(".deletion_in_progress") %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="text-xs text-secondary"><%= t(".provider_name") %></p>
|
||||
<% if coinbase_item.syncing? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= icon "loader", size: "sm", class: "animate-spin" %>
|
||||
<%= tag.span t(".syncing") %>
|
||||
</div>
|
||||
<% elsif coinbase_item.requires_update? %>
|
||||
<div class="text-warning flex items-center gap-1">
|
||||
<%= icon "alert-triangle", size: "sm", color: "warning" %>
|
||||
<%= tag.span t(".reconnect") %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-secondary">
|
||||
<% if coinbase_item.last_synced_at %>
|
||||
<% if coinbase_item.sync_status_summary %>
|
||||
<%= t(".status_with_summary", timestamp: time_ago_in_words(coinbase_item.last_synced_at), summary: coinbase_item.sync_status_summary) %>
|
||||
<% else %>
|
||||
<%= t(".status", timestamp: time_ago_in_words(coinbase_item.last_synced_at)) %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= t(".status_never") %>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="text-xs text-secondary"><%= t(".provider_name") %></p>
|
||||
<% if coinbase_item.syncing? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= icon "loader", size: "sm", class: "animate-spin" %>
|
||||
<%= tag.span t(".syncing") %>
|
||||
</div>
|
||||
<% elsif coinbase_item.requires_update? %>
|
||||
<div class="text-warning flex items-center gap-1">
|
||||
<%= icon "alert-triangle", size: "sm", color: "warning" %>
|
||||
<%= tag.span t(".reconnect") %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-secondary">
|
||||
<% if coinbase_item.last_synced_at %>
|
||||
<% if coinbase_item.sync_status_summary %>
|
||||
<%= t(".status_with_summary", timestamp: time_ago_in_words(coinbase_item.last_synced_at), summary: coinbase_item.sync_status_summary) %>
|
||||
<% else %>
|
||||
<%= t(".status", timestamp: time_ago_in_words(coinbase_item.last_synced_at)) %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= t(".status_never") %>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if Current.user&.admin? %>
|
||||
<div class="flex items-center gap-2">
|
||||
<% if coinbase_item.requires_update? %>
|
||||
<%= render DS::Link.new(
|
||||
text: t(".update_credentials"),
|
||||
icon: "refresh-cw",
|
||||
variant: "secondary",
|
||||
href: settings_providers_path,
|
||||
frame: "_top"
|
||||
) %>
|
||||
<% else %>
|
||||
<%= icon(
|
||||
"refresh-cw",
|
||||
as_button: true,
|
||||
href: sync_coinbase_item_path(coinbase_item),
|
||||
disabled: coinbase_item.syncing?
|
||||
) %>
|
||||
<% end %>
|
||||
|
||||
<%= render DS::Menu.new do |menu| %>
|
||||
<% if unlinked_count.to_i > 0 %>
|
||||
<% menu.with_item(
|
||||
variant: "link",
|
||||
text: t(".import_wallets_menu"),
|
||||
icon: "plus",
|
||||
href: setup_accounts_coinbase_item_path(coinbase_item),
|
||||
frame: :modal
|
||||
<% if Current.user&.admin? %>
|
||||
<div class="flex items-center gap-2">
|
||||
<% if coinbase_item.requires_update? %>
|
||||
<%= render DS::Link.new(
|
||||
text: t(".update_credentials"),
|
||||
icon: "refresh-cw",
|
||||
variant: "secondary",
|
||||
href: settings_providers_path,
|
||||
frame: "_top"
|
||||
) %>
|
||||
<% else %>
|
||||
<%= icon(
|
||||
"refresh-cw",
|
||||
as_button: true,
|
||||
href: sync_coinbase_item_path(coinbase_item),
|
||||
disabled: coinbase_item.syncing?
|
||||
) %>
|
||||
<% end %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: t(".delete"),
|
||||
icon: "trash-2",
|
||||
href: coinbase_item_path(coinbase_item),
|
||||
method: :delete,
|
||||
confirm: CustomConfirm.for_resource_deletion(coinbase_item.institution_display_name, high_severity: true)
|
||||
) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</summary>
|
||||
|
||||
<%= render DS::Menu.new do |menu| %>
|
||||
<% if unlinked_count.to_i > 0 %>
|
||||
<% menu.with_item(
|
||||
variant: "link",
|
||||
text: t(".import_wallets_menu"),
|
||||
icon: "plus",
|
||||
href: setup_accounts_coinbase_item_path(coinbase_item),
|
||||
frame: :modal
|
||||
) %>
|
||||
<% end %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: t(".delete"),
|
||||
icon: "trash-2",
|
||||
href: coinbase_item_path(coinbase_item),
|
||||
method: :delete,
|
||||
confirm: CustomConfirm.for_resource_deletion(coinbase_item.institution_display_name, high_severity: true)
|
||||
) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% unless coinbase_item.scheduled_for_deletion? %>
|
||||
<div class="space-y-4 mt-4">
|
||||
@@ -142,5 +144,5 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</details>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,82 +1,84 @@
|
||||
<%# locals: (coinstats_item:) %>
|
||||
|
||||
<%= tag.div id: dom_id(coinstats_item) do %>
|
||||
<details open class="group bg-container p-4 shadow-border-xs rounded-xl">
|
||||
<summary class="flex items-center justify-between gap-2 focus-visible:outline-hidden">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon "chevron-right", class: "group-open:transform group-open:rotate-90" %>
|
||||
<%= render DS::Disclosure.new(variant: :card, open: true) do |disclosure| %>
|
||||
<% disclosure.with_summary_content do %>
|
||||
<div class="flex items-center justify-between gap-2 focus-visible:outline-hidden">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon "chevron-right", class: "group-open:rotate-90 motion-safe:transition-transform motion-safe:duration-150" %>
|
||||
|
||||
<div class="flex items-center justify-center h-8 w-8 bg-success/10 rounded-full">
|
||||
<div class="flex items-center justify-center">
|
||||
<%= tag.p coinstats_item.institution_display_name.first.upcase, class: "text-success text-xs font-medium" %>
|
||||
<div class="flex items-center justify-center h-8 w-8 bg-success/10 rounded-full">
|
||||
<div class="flex items-center justify-center">
|
||||
<%= tag.p coinstats_item.institution_display_name.first.upcase, class: "text-success text-xs font-medium" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pl-1 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= tag.p coinstats_item.institution_display_name, class: "font-medium text-primary" %>
|
||||
<% if coinstats_item.scheduled_for_deletion? %>
|
||||
<p class="text-destructive text-sm animate-pulse"><%= t(".deletion_in_progress") %></p>
|
||||
<div class="pl-1 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= tag.p coinstats_item.institution_display_name, class: "font-medium text-primary" %>
|
||||
<% if coinstats_item.scheduled_for_deletion? %>
|
||||
<p class="text-destructive text-sm animate-pulse"><%= t(".deletion_in_progress") %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="text-xs text-secondary"><%= t(".provider_name") %></p>
|
||||
<% if coinstats_item.syncing? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= icon "loader", size: "sm", class: "animate-spin" %>
|
||||
<%= tag.span t(".syncing") %>
|
||||
</div>
|
||||
<% elsif coinstats_item.requires_update? %>
|
||||
<div class="text-warning flex items-center gap-1">
|
||||
<%= icon "alert-triangle", size: "sm", color: "warning" %>
|
||||
<%= tag.span t(".reconnect") %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-secondary">
|
||||
<% if coinstats_item.last_synced_at %>
|
||||
<% if coinstats_item.sync_status_summary %>
|
||||
<%= t(".status_with_summary", timestamp: time_ago_in_words(coinstats_item.last_synced_at), summary: coinstats_item.sync_status_summary) %>
|
||||
<% else %>
|
||||
<%= t(".status", timestamp: time_ago_in_words(coinstats_item.last_synced_at)) %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= t(".status_never") %>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="text-xs text-secondary"><%= t(".provider_name") %></p>
|
||||
<% if coinstats_item.syncing? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= icon "loader", size: "sm", class: "animate-spin" %>
|
||||
<%= tag.span t(".syncing") %>
|
||||
</div>
|
||||
<% elsif coinstats_item.requires_update? %>
|
||||
<div class="text-warning flex items-center gap-1">
|
||||
<%= icon "alert-triangle", size: "sm", color: "warning" %>
|
||||
<%= tag.span t(".reconnect") %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-secondary">
|
||||
<% if coinstats_item.last_synced_at %>
|
||||
<% if coinstats_item.sync_status_summary %>
|
||||
<%= t(".status_with_summary", timestamp: time_ago_in_words(coinstats_item.last_synced_at), summary: coinstats_item.sync_status_summary) %>
|
||||
<% else %>
|
||||
<%= t(".status", timestamp: time_ago_in_words(coinstats_item.last_synced_at)) %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= t(".status_never") %>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if Current.user&.admin? %>
|
||||
<div class="flex items-center gap-2">
|
||||
<% if coinstats_item.requires_update? %>
|
||||
<%= render DS::Link.new(
|
||||
text: t(".update_api_key"),
|
||||
icon: "refresh-cw",
|
||||
variant: "secondary",
|
||||
href: settings_providers_path,
|
||||
frame: "_top"
|
||||
) %>
|
||||
<% elsif Rails.env.development? %>
|
||||
<%= icon(
|
||||
"refresh-cw",
|
||||
as_button: true,
|
||||
href: sync_coinstats_item_path(coinstats_item)
|
||||
) %>
|
||||
<% end %>
|
||||
|
||||
<%= render DS::Menu.new do |menu| %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: t(".delete"),
|
||||
icon: "trash-2",
|
||||
href: coinstats_item_path(coinstats_item),
|
||||
method: :delete,
|
||||
confirm: CustomConfirm.for_resource_deletion(coinstats_item.institution_display_name, high_severity: true)
|
||||
) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if Current.user&.admin? %>
|
||||
<div class="flex items-center gap-2">
|
||||
<% if coinstats_item.requires_update? %>
|
||||
<%= render DS::Link.new(
|
||||
text: t(".update_api_key"),
|
||||
icon: "refresh-cw",
|
||||
variant: "secondary",
|
||||
href: settings_providers_path,
|
||||
frame: "_top"
|
||||
) %>
|
||||
<% elsif Rails.env.development? %>
|
||||
<%= icon(
|
||||
"refresh-cw",
|
||||
as_button: true,
|
||||
href: sync_coinstats_item_path(coinstats_item)
|
||||
) %>
|
||||
<% end %>
|
||||
|
||||
<%= render DS::Menu.new do |menu| %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: t(".delete"),
|
||||
icon: "trash-2",
|
||||
href: coinstats_item_path(coinstats_item),
|
||||
method: :delete,
|
||||
confirm: CustomConfirm.for_resource_deletion(coinstats_item.institution_display_name, high_severity: true)
|
||||
) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</summary>
|
||||
<% end %>
|
||||
|
||||
<% unless coinstats_item.scheduled_for_deletion? %>
|
||||
<div class="space-y-4 mt-4">
|
||||
@@ -103,5 +105,5 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</details>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,85 +1,87 @@
|
||||
<%# locals: (enable_banking_item:) %>
|
||||
|
||||
<%= tag.div id: dom_id(enable_banking_item) do %>
|
||||
<details open class="group bg-container p-4 shadow-border-xs rounded-xl">
|
||||
<summary class="flex items-center justify-between gap-2 focus-visible:outline-hidden">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon "chevron-right", class: "group-open:transform group-open:rotate-90" %>
|
||||
<%= render DS::Disclosure.new(variant: :card, open: true) do |disclosure| %>
|
||||
<% disclosure.with_summary_content do %>
|
||||
<div class="flex items-center justify-between gap-2 focus-visible:outline-hidden">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon "chevron-right", class: "group-open:rotate-90 motion-safe:transition-transform motion-safe:duration-150" %>
|
||||
|
||||
<div class="flex items-center justify-center h-8 w-8 bg-success/10 rounded-full">
|
||||
<% if enable_banking_item.logo.attached? %>
|
||||
<%= image_tag enable_banking_item.logo, class: "rounded-full h-full w-full", loading: "lazy" %>
|
||||
<% else %>
|
||||
<div class="flex items-center justify-center">
|
||||
<%= tag.p enable_banking_item.institution_display_name.first.upcase, class: "text-success text-xs font-medium" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="pl-1 text-sm">
|
||||
<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"><%= t(".deletion_in_progress") %></p>
|
||||
<div class="flex items-center justify-center h-8 w-8 bg-success/10 rounded-full">
|
||||
<% if enable_banking_item.logo.attached? %>
|
||||
<%= image_tag enable_banking_item.logo, class: "rounded-full h-full w-full", loading: "lazy" %>
|
||||
<% else %>
|
||||
<div class="flex items-center justify-center">
|
||||
<%= tag.p enable_banking_item.institution_display_name.first.upcase, class: "text-success text-xs font-medium" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<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 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 t(".reconnect") %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-secondary">
|
||||
<% if enable_banking_item.last_synced_at %>
|
||||
<%= 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 %>
|
||||
<%= t(".never_synced") %>
|
||||
|
||||
<div class="pl-1 text-sm">
|
||||
<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"><%= t(".deletion_in_progress") %></p>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if Current.user&.admin? %>
|
||||
<div class="flex items-center gap-2">
|
||||
<% if enable_banking_item.requires_update? %>
|
||||
<%= button_to reauthorize_enable_banking_item_path(enable_banking_item),
|
||||
method: :post,
|
||||
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" %>
|
||||
<%= t(".update") %>
|
||||
</div>
|
||||
<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 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 t(".reconnect") %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-secondary">
|
||||
<% if enable_banking_item.last_synced_at %>
|
||||
<%= 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 %>
|
||||
<%= t(".never_synced") %>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
<% elsif Rails.env.development? %>
|
||||
<%= icon(
|
||||
"refresh-cw",
|
||||
as_button: true,
|
||||
href: sync_enable_banking_item_path(enable_banking_item)
|
||||
) %>
|
||||
<% end %>
|
||||
|
||||
<%= render DS::Menu.new do |menu| %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: t(".delete"),
|
||||
icon: "trash-2",
|
||||
href: enable_banking_item_path(enable_banking_item),
|
||||
method: :delete,
|
||||
confirm: CustomConfirm.for_resource_deletion(enable_banking_item.institution_display_name, high_severity: true)
|
||||
) %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</summary>
|
||||
|
||||
<% if Current.user&.admin? %>
|
||||
<div class="flex items-center gap-2">
|
||||
<% if enable_banking_item.requires_update? %>
|
||||
<%= button_to reauthorize_enable_banking_item_path(enable_banking_item),
|
||||
method: :post,
|
||||
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" %>
|
||||
<%= t(".update") %>
|
||||
<% end %>
|
||||
<% elsif Rails.env.development? %>
|
||||
<%= icon(
|
||||
"refresh-cw",
|
||||
as_button: true,
|
||||
href: sync_enable_banking_item_path(enable_banking_item)
|
||||
) %>
|
||||
<% end %>
|
||||
|
||||
<%= render DS::Menu.new do |menu| %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: t(".delete"),
|
||||
icon: "trash-2",
|
||||
href: enable_banking_item_path(enable_banking_item),
|
||||
method: :delete,
|
||||
confirm: CustomConfirm.for_resource_deletion(enable_banking_item.institution_display_name, high_severity: true)
|
||||
) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% unless enable_banking_item.scheduled_for_deletion? %>
|
||||
<div class="space-y-4 mt-4">
|
||||
@@ -127,5 +129,5 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</details>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -3,81 +3,83 @@
|
||||
<%= tag.div id: dom_id(ibkr_item) do %>
|
||||
<% unlinked_count = ibkr_item.unlinked_accounts_count %>
|
||||
|
||||
<details open class="group bg-container p-4 shadow-border-xs rounded-xl">
|
||||
<summary class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon "chevron-right", class: "group-open:transform group-open:rotate-90" %>
|
||||
<%= render DS::Disclosure.new(variant: :card, open: true) do |disclosure| %>
|
||||
<% disclosure.with_summary_content do %>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon "chevron-right", class: "group-open:rotate-90 motion-safe:transition-transform motion-safe:duration-150" %>
|
||||
|
||||
<div class="flex items-center justify-center h-8 w-8 rounded-full bg-[#D32F2F]/10">
|
||||
<span class="text-[#D32F2F] text-xs font-medium">IB</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-center h-8 w-8 rounded-full bg-[#D32F2F]/10">
|
||||
<span class="text-[#D32F2F] text-xs font-medium">IB</span>
|
||||
</div>
|
||||
|
||||
<div class="pl-1 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= tag.p ibkr_item.institution_display_name, class: "font-medium text-primary" %>
|
||||
<% if ibkr_item.scheduled_for_deletion? %>
|
||||
<p class="text-destructive text-sm animate-pulse"><%= t(".deletion_in_progress") %></p>
|
||||
<div class="pl-1 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= tag.p ibkr_item.institution_display_name, class: "font-medium text-primary" %>
|
||||
<% if ibkr_item.scheduled_for_deletion? %>
|
||||
<p class="text-destructive text-sm animate-pulse"><%= t(".deletion_in_progress") %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="text-xs text-secondary"><%= t(".flex_web_service") %></p>
|
||||
<% if ibkr_item.syncing? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= icon "loader", size: "sm", class: "animate-spin" %>
|
||||
<%= tag.span t(".syncing") %>
|
||||
</div>
|
||||
<% elsif ibkr_item.requires_update? %>
|
||||
<div class="text-warning flex items-center gap-1">
|
||||
<%= icon "alert-triangle", size: "sm", color: "warning" %>
|
||||
<%= tag.span t(".requires_update") %>
|
||||
</div>
|
||||
<% elsif ibkr_item.sync_error.present? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= render DS::Tooltip.new(text: ibkr_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive", as: :span) %>
|
||||
<%= tag.span t(".error"), class: "text-destructive" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-secondary">
|
||||
<% if ibkr_item.last_synced_at %>
|
||||
<%= t(".synced", time: time_ago_in_words(ibkr_item.last_synced_at), summary: ibkr_item.sync_status_summary) %>
|
||||
<% else %>
|
||||
<%= t(".never_synced") %>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="text-xs text-secondary"><%= t(".flex_web_service") %></p>
|
||||
<% if ibkr_item.syncing? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= icon "loader", size: "sm", class: "animate-spin" %>
|
||||
<%= tag.span t(".syncing") %>
|
||||
</div>
|
||||
<% elsif ibkr_item.requires_update? %>
|
||||
<div class="text-warning flex items-center gap-1">
|
||||
<%= icon "alert-triangle", size: "sm", color: "warning" %>
|
||||
<%= tag.span t(".requires_update") %>
|
||||
</div>
|
||||
<% elsif ibkr_item.sync_error.present? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= render DS::Tooltip.new(text: ibkr_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive", as: :span) %>
|
||||
<%= tag.span t(".error"), class: "text-destructive" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-secondary">
|
||||
<% if ibkr_item.last_synced_at %>
|
||||
<%= t(".synced", time: time_ago_in_words(ibkr_item.last_synced_at), summary: ibkr_item.sync_status_summary) %>
|
||||
<% else %>
|
||||
<%= t(".never_synced") %>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if Current.user&.admin? %>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon(
|
||||
"refresh-cw",
|
||||
as_button: true,
|
||||
href: sync_ibkr_item_path(ibkr_item),
|
||||
disabled: ibkr_item.syncing?
|
||||
) %>
|
||||
<% if Current.user&.admin? %>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon(
|
||||
"refresh-cw",
|
||||
as_button: true,
|
||||
href: sync_ibkr_item_path(ibkr_item),
|
||||
disabled: ibkr_item.syncing?
|
||||
) %>
|
||||
|
||||
<%= render DS::Menu.new do |menu| %>
|
||||
<% if unlinked_count > 0 %>
|
||||
<%= render DS::Menu.new do |menu| %>
|
||||
<% if unlinked_count > 0 %>
|
||||
<% menu.with_item(
|
||||
variant: "link",
|
||||
text: t(".setup_accounts"),
|
||||
icon: "settings",
|
||||
href: setup_accounts_ibkr_item_path(ibkr_item),
|
||||
frame: :modal
|
||||
) %>
|
||||
<% end %>
|
||||
<% menu.with_item(
|
||||
variant: "link",
|
||||
text: t(".setup_accounts"),
|
||||
icon: "settings",
|
||||
href: setup_accounts_ibkr_item_path(ibkr_item),
|
||||
frame: :modal
|
||||
variant: "button",
|
||||
text: t(".delete"),
|
||||
icon: "trash-2",
|
||||
href: ibkr_item_path(ibkr_item),
|
||||
method: :delete,
|
||||
confirm: CustomConfirm.for_resource_deletion(ibkr_item.institution_display_name, high_severity: true)
|
||||
) %>
|
||||
<% end %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: t(".delete"),
|
||||
icon: "trash-2",
|
||||
href: ibkr_item_path(ibkr_item),
|
||||
method: :delete,
|
||||
confirm: CustomConfirm.for_resource_deletion(ibkr_item.institution_display_name, high_severity: true)
|
||||
) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</summary>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% unless ibkr_item.scheduled_for_deletion? %>
|
||||
<div class="space-y-4 mt-4">
|
||||
@@ -110,5 +112,5 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</details>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,101 +1,103 @@
|
||||
<%# locals: (indexa_capital_item:) %>
|
||||
|
||||
<%= tag.div id: dom_id(indexa_capital_item) do %>
|
||||
<details open class="group bg-container p-4 shadow-border-xs rounded-xl">
|
||||
<summary class="flex items-center justify-between gap-2 focus-visible:outline-hidden">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon "chevron-right", class: "group-open:transform group-open:rotate-90" %>
|
||||
<% unlinked_count = indexa_capital_item.unlinked_accounts_count %>
|
||||
|
||||
<div class="flex items-center justify-center h-8 w-8 bg-gray-tint-10 rounded-full">
|
||||
<div class="flex items-center justify-center">
|
||||
<%= tag.p indexa_capital_item.name.first.upcase, class: "text-primary text-xs font-medium" %>
|
||||
<%= render DS::Disclosure.new(variant: :card, open: true) do |disclosure| %>
|
||||
<% disclosure.with_summary_content do %>
|
||||
<div class="flex items-center justify-between gap-2 focus-visible:outline-hidden">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon "chevron-right", class: "group-open:rotate-90 motion-safe:transition-transform motion-safe:duration-150" %>
|
||||
|
||||
<div class="flex items-center justify-center h-8 w-8 bg-gray-tint-10 rounded-full">
|
||||
<div class="flex items-center justify-center">
|
||||
<%= tag.p indexa_capital_item.name.first.upcase, class: "text-primary text-xs font-medium" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% unlinked_count = indexa_capital_item.unlinked_accounts_count %>
|
||||
|
||||
<div class="pl-1 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= tag.p indexa_capital_item.name, class: "font-medium text-primary" %>
|
||||
<% if indexa_capital_item.scheduled_for_deletion? %>
|
||||
<p class="text-destructive text-sm animate-pulse"><%= t(".deletion_in_progress") %></p>
|
||||
<div class="pl-1 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= tag.p indexa_capital_item.name, class: "font-medium text-primary" %>
|
||||
<% if indexa_capital_item.scheduled_for_deletion? %>
|
||||
<p class="text-destructive text-sm animate-pulse"><%= t(".deletion_in_progress") %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="text-xs text-secondary"><%= t(".provider_name") %></p>
|
||||
<% if indexa_capital_item.syncing? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= icon "loader", size: "sm", class: "animate-spin" %>
|
||||
<%= tag.span t(".syncing") %>
|
||||
</div>
|
||||
<% elsif indexa_capital_item.requires_update? %>
|
||||
<div class="text-warning flex items-center gap-1">
|
||||
<%= icon "alert-triangle", size: "sm", color: "warning" %>
|
||||
<%= tag.span t(".requires_update") %>
|
||||
</div>
|
||||
<% elsif indexa_capital_item.sync_error.present? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= render DS::Tooltip.new(text: indexa_capital_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive", as: :span) %>
|
||||
<%= tag.span t(".error"), class: "text-destructive" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-secondary">
|
||||
<% if indexa_capital_item.last_synced_at %>
|
||||
<%= t(".status", timestamp: time_ago_in_words(indexa_capital_item.last_synced_at), summary: indexa_capital_item.sync_status_summary) %>
|
||||
<% else %>
|
||||
<%= t(".status_never") %>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="text-xs text-secondary"><%= t(".provider_name") %></p>
|
||||
<% if indexa_capital_item.syncing? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= icon "loader", size: "sm", class: "animate-spin" %>
|
||||
<%= tag.span t(".syncing") %>
|
||||
</div>
|
||||
<% elsif indexa_capital_item.requires_update? %>
|
||||
<div class="text-warning flex items-center gap-1">
|
||||
<%= icon "alert-triangle", size: "sm", color: "warning" %>
|
||||
<%= tag.span t(".requires_update") %>
|
||||
</div>
|
||||
<% elsif indexa_capital_item.sync_error.present? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= render DS::Tooltip.new(text: indexa_capital_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive", as: :span) %>
|
||||
<%= tag.span t(".error"), class: "text-destructive" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-secondary">
|
||||
<% if indexa_capital_item.last_synced_at %>
|
||||
<%= t(".status", timestamp: time_ago_in_words(indexa_capital_item.last_synced_at), summary: indexa_capital_item.sync_status_summary) %>
|
||||
<% else %>
|
||||
<%= t(".status_never") %>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if Current.user&.admin? %>
|
||||
<div class="flex items-center gap-2">
|
||||
<% if indexa_capital_item.requires_update? %>
|
||||
<%= render DS::Link.new(
|
||||
text: t(".update_credentials"),
|
||||
icon: "refresh-cw",
|
||||
variant: "secondary",
|
||||
href: settings_providers_path,
|
||||
frame: "_top"
|
||||
) %>
|
||||
<% else %>
|
||||
<%= icon(
|
||||
"refresh-cw",
|
||||
as_button: true,
|
||||
href: sync_indexa_capital_item_path(indexa_capital_item),
|
||||
disabled: indexa_capital_item.syncing?
|
||||
) %>
|
||||
<% end %>
|
||||
|
||||
<%= render DS::Menu.new do |menu| %>
|
||||
<% if unlinked_count > 0 %>
|
||||
<% menu.with_item(
|
||||
variant: "link",
|
||||
text: t(".setup_action"),
|
||||
icon: "settings",
|
||||
href: setup_accounts_indexa_capital_item_path(indexa_capital_item),
|
||||
frame: :modal
|
||||
<% if Current.user&.admin? %>
|
||||
<div class="flex items-center gap-2">
|
||||
<% if indexa_capital_item.requires_update? %>
|
||||
<%= render DS::Link.new(
|
||||
text: t(".update_credentials"),
|
||||
icon: "refresh-cw",
|
||||
variant: "secondary",
|
||||
href: settings_providers_path,
|
||||
frame: "_top"
|
||||
) %>
|
||||
<% else %>
|
||||
<%= icon(
|
||||
"refresh-cw",
|
||||
as_button: true,
|
||||
href: sync_indexa_capital_item_path(indexa_capital_item),
|
||||
disabled: indexa_capital_item.syncing?
|
||||
) %>
|
||||
<% end %>
|
||||
<% menu.with_item(
|
||||
variant: "link",
|
||||
text: t(".update_credentials"),
|
||||
icon: "cable",
|
||||
href: settings_providers_path(manage: "1")
|
||||
) %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: t(".delete"),
|
||||
icon: "trash-2",
|
||||
href: indexa_capital_item_path(indexa_capital_item),
|
||||
method: :delete,
|
||||
confirm: CustomConfirm.for_resource_deletion(indexa_capital_item.name, high_severity: true)
|
||||
) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</summary>
|
||||
|
||||
<%= render DS::Menu.new do |menu| %>
|
||||
<% if unlinked_count > 0 %>
|
||||
<% menu.with_item(
|
||||
variant: "link",
|
||||
text: t(".setup_action"),
|
||||
icon: "settings",
|
||||
href: setup_accounts_indexa_capital_item_path(indexa_capital_item),
|
||||
frame: :modal
|
||||
) %>
|
||||
<% end %>
|
||||
<% menu.with_item(
|
||||
variant: "link",
|
||||
text: t(".update_credentials"),
|
||||
icon: "cable",
|
||||
href: settings_providers_path(manage: "1")
|
||||
) %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: t(".delete"),
|
||||
icon: "trash-2",
|
||||
href: indexa_capital_item_path(indexa_capital_item),
|
||||
method: :delete,
|
||||
confirm: CustomConfirm.for_resource_deletion(indexa_capital_item.name, high_severity: true)
|
||||
) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% unless indexa_capital_item.scheduled_for_deletion? %>
|
||||
<div class="space-y-4 mt-4">
|
||||
@@ -144,5 +146,5 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</details>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,48 +1,50 @@
|
||||
<%# locals: (kraken_item:, unlinked_count: kraken_item.unlinked_accounts_count) %>
|
||||
|
||||
<%= tag.div id: dom_id(kraken_item) do %>
|
||||
<details open class="group bg-container p-4 shadow-border-xs rounded-xl">
|
||||
<summary class="flex items-center gap-2">
|
||||
<%= icon "chevron-right", class: "group-open:transform group-open:rotate-90" %>
|
||||
<%= render DS::Disclosure.new(variant: :card, open: true) do |disclosure| %>
|
||||
<% disclosure.with_summary_content do %>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon "chevron-right", class: "group-open:rotate-90 motion-safe:transition-transform motion-safe:duration-150" %>
|
||||
|
||||
<div class="flex items-center justify-center h-8 w-8 rounded-full bg-surface-inset">
|
||||
<%= icon "waves", size: "sm", class: "text-primary" %>
|
||||
</div>
|
||||
<div class="flex items-center justify-center h-8 w-8 rounded-full bg-surface-inset">
|
||||
<%= icon "waves", size: "sm", class: "text-primary" %>
|
||||
</div>
|
||||
|
||||
<div class="pl-1 text-sm flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= tag.p kraken_item.institution_display_name, class: "font-medium text-primary" %>
|
||||
<% if kraken_item.scheduled_for_deletion? %>
|
||||
<p class="text-destructive text-sm animate-pulse"><%= t(".deletion_in_progress") %></p>
|
||||
<div class="pl-1 text-sm flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= tag.p kraken_item.institution_display_name, class: "font-medium text-primary" %>
|
||||
<% if kraken_item.scheduled_for_deletion? %>
|
||||
<p class="text-destructive text-sm animate-pulse"><%= t(".deletion_in_progress") %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="text-xs text-secondary"><%= t(".provider_name") %></p>
|
||||
|
||||
<% if kraken_item.syncing? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= icon "loader", size: "sm", class: "animate-spin" %>
|
||||
<%= tag.span t(".syncing") %>
|
||||
</div>
|
||||
<% elsif kraken_item.requires_update? %>
|
||||
<div class="text-warning flex items-center gap-1">
|
||||
<%= icon "alert-triangle", size: "sm", color: "warning" %>
|
||||
<%= tag.span t(".reconnect") %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-secondary">
|
||||
<% if kraken_item.last_synced_at %>
|
||||
<% if kraken_item.sync_status_summary %>
|
||||
<%= t(".status_with_summary", timestamp: time_ago_in_words(kraken_item.last_synced_at), summary: kraken_item.sync_status_summary) %>
|
||||
<% else %>
|
||||
<%= t(".status", timestamp: time_ago_in_words(kraken_item.last_synced_at)) %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= t(".status_never") %>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="text-xs text-secondary"><%= t(".provider_name") %></p>
|
||||
|
||||
<% if kraken_item.syncing? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= icon "loader", size: "sm", class: "animate-spin" %>
|
||||
<%= tag.span t(".syncing") %>
|
||||
</div>
|
||||
<% elsif kraken_item.requires_update? %>
|
||||
<div class="text-warning flex items-center gap-1">
|
||||
<%= icon "alert-triangle", size: "sm", color: "warning" %>
|
||||
<%= tag.span t(".reconnect") %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-secondary">
|
||||
<% if kraken_item.last_synced_at %>
|
||||
<% if kraken_item.sync_status_summary %>
|
||||
<%= t(".status_with_summary", timestamp: time_ago_in_words(kraken_item.last_synced_at), summary: kraken_item.sync_status_summary) %>
|
||||
<% else %>
|
||||
<%= t(".status", timestamp: time_ago_in_words(kraken_item.last_synced_at)) %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= t(".status_never") %>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</summary>
|
||||
<% end %>
|
||||
|
||||
<% if Current.user&.admin? %>
|
||||
<div class="flex items-center justify-end gap-2 mt-2">
|
||||
@@ -111,5 +113,5 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</details>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,82 +1,84 @@
|
||||
<%# locals: (lunchflow_item:) %>
|
||||
|
||||
<%= tag.div id: dom_id(lunchflow_item) do %>
|
||||
<details open class="group bg-container p-4 shadow-border-xs rounded-xl">
|
||||
<summary class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon "chevron-right", class: "group-open:transform group-open:rotate-90" %>
|
||||
<%= render DS::Disclosure.new(variant: :card, open: true) do |disclosure| %>
|
||||
<% disclosure.with_summary_content do %>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon "chevron-right", class: "group-open:rotate-90 motion-safe:transition-transform motion-safe:duration-150" %>
|
||||
|
||||
<div class="flex items-center justify-center h-8 w-8 bg-orange-600/10 rounded-full">
|
||||
<% if lunchflow_item.logo.attached? %>
|
||||
<%= image_tag lunchflow_item.logo, class: "rounded-full h-full w-full", loading: "lazy" %>
|
||||
<% else %>
|
||||
<div class="flex items-center justify-center">
|
||||
<%= tag.p lunchflow_item.name.first.upcase, class: "text-orange-600 text-xs font-medium" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="pl-1 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= tag.p lunchflow_item.name, class: "font-medium text-primary" %>
|
||||
<% if lunchflow_item.scheduled_for_deletion? %>
|
||||
<p class="text-destructive text-sm animate-pulse"><%= t(".deletion_in_progress") %></p>
|
||||
<div class="flex items-center justify-center h-8 w-8 bg-orange-600/10 rounded-full">
|
||||
<% if lunchflow_item.logo.attached? %>
|
||||
<%= image_tag lunchflow_item.logo, class: "rounded-full h-full w-full", loading: "lazy" %>
|
||||
<% else %>
|
||||
<div class="flex items-center justify-center">
|
||||
<%= tag.p lunchflow_item.name.first.upcase, class: "text-orange-600 text-xs font-medium" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if lunchflow_item.accounts.any? %>
|
||||
<p class="text-xs text-secondary">
|
||||
<%= lunchflow_item.institution_summary %>
|
||||
</p>
|
||||
<% end %>
|
||||
<% if lunchflow_item.syncing? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= icon "loader", size: "sm", class: "animate-spin" %>
|
||||
<%= tag.span t(".syncing") %>
|
||||
</div>
|
||||
<% elsif lunchflow_item.sync_error.present? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= render DS::Tooltip.new(text: lunchflow_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive", as: :span) %>
|
||||
<%= tag.span t(".error"), class: "text-destructive" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-secondary">
|
||||
<% if lunchflow_item.last_synced_at %>
|
||||
<% if lunchflow_item.sync_status_summary %>
|
||||
<%= t(".status_with_summary", timestamp: time_ago_in_words(lunchflow_item.last_synced_at), summary: lunchflow_item.sync_status_summary) %>
|
||||
<% else %>
|
||||
<%= t(".status", timestamp: time_ago_in_words(lunchflow_item.last_synced_at)) %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= t(".status_never") %>
|
||||
|
||||
<div class="pl-1 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= tag.p lunchflow_item.name, class: "font-medium text-primary" %>
|
||||
<% if lunchflow_item.scheduled_for_deletion? %>
|
||||
<p class="text-destructive text-sm animate-pulse"><%= t(".deletion_in_progress") %></p>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if lunchflow_item.accounts.any? %>
|
||||
<p class="text-xs text-secondary">
|
||||
<%= lunchflow_item.institution_summary %>
|
||||
</p>
|
||||
<% end %>
|
||||
<% if lunchflow_item.syncing? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= icon "loader", size: "sm", class: "animate-spin" %>
|
||||
<%= tag.span t(".syncing") %>
|
||||
</div>
|
||||
<% elsif lunchflow_item.sync_error.present? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= render DS::Tooltip.new(text: lunchflow_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive", as: :span) %>
|
||||
<%= tag.span t(".error"), class: "text-destructive" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-secondary">
|
||||
<% if lunchflow_item.last_synced_at %>
|
||||
<% if lunchflow_item.sync_status_summary %>
|
||||
<%= t(".status_with_summary", timestamp: time_ago_in_words(lunchflow_item.last_synced_at), summary: lunchflow_item.sync_status_summary) %>
|
||||
<% else %>
|
||||
<%= t(".status", timestamp: time_ago_in_words(lunchflow_item.last_synced_at)) %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= t(".status_never") %>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if Current.user&.admin? %>
|
||||
<div class="flex items-center gap-2">
|
||||
<% if Rails.env.development? %>
|
||||
<%= icon(
|
||||
"refresh-cw",
|
||||
as_button: true,
|
||||
href: sync_lunchflow_item_path(lunchflow_item)
|
||||
) %>
|
||||
<% end %>
|
||||
|
||||
<%= render DS::Menu.new do |menu| %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: t(".delete"),
|
||||
icon: "trash-2",
|
||||
href: lunchflow_item_path(lunchflow_item),
|
||||
method: :delete,
|
||||
confirm: CustomConfirm.for_resource_deletion(lunchflow_item.name, high_severity: true)
|
||||
) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if Current.user&.admin? %>
|
||||
<div class="flex items-center gap-2">
|
||||
<% if Rails.env.development? %>
|
||||
<%= icon(
|
||||
"refresh-cw",
|
||||
as_button: true,
|
||||
href: sync_lunchflow_item_path(lunchflow_item)
|
||||
) %>
|
||||
<% end %>
|
||||
|
||||
<%= render DS::Menu.new do |menu| %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: t(".delete"),
|
||||
icon: "trash-2",
|
||||
href: lunchflow_item_path(lunchflow_item),
|
||||
method: :delete,
|
||||
confirm: CustomConfirm.for_resource_deletion(lunchflow_item.name, high_severity: true)
|
||||
) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</summary>
|
||||
<% end %>
|
||||
|
||||
<% unless lunchflow_item.scheduled_for_deletion? %>
|
||||
<div class="space-y-4 mt-4">
|
||||
@@ -128,5 +130,5 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</details>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,82 +1,84 @@
|
||||
<%# locals: (mercury_item:) %>
|
||||
|
||||
<%= tag.div id: dom_id(mercury_item) do %>
|
||||
<details open class="group bg-container p-4 shadow-border-xs rounded-xl">
|
||||
<summary class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon "chevron-right", class: "group-open:transform group-open:rotate-90" %>
|
||||
<%= render DS::Disclosure.new(variant: :card, open: true) do |disclosure| %>
|
||||
<% disclosure.with_summary_content do %>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon "chevron-right", class: "group-open:rotate-90 motion-safe:transition-transform motion-safe:duration-150" %>
|
||||
|
||||
<div class="flex items-center justify-center h-8 w-8 bg-blue-600/10 rounded-full">
|
||||
<% if mercury_item.logo.attached? %>
|
||||
<%= image_tag mercury_item.logo, class: "rounded-full h-full w-full", loading: "lazy" %>
|
||||
<% else %>
|
||||
<div class="flex items-center justify-center">
|
||||
<%= tag.p mercury_item.name.first.upcase, class: "text-blue-600 text-xs font-medium" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="pl-1 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= tag.p mercury_item.name, class: "font-medium text-primary" %>
|
||||
<% if mercury_item.scheduled_for_deletion? %>
|
||||
<p class="text-destructive text-sm animate-pulse"><%= t(".deletion_in_progress") %></p>
|
||||
<div class="flex items-center justify-center h-8 w-8 bg-blue-600/10 rounded-full">
|
||||
<% if mercury_item.logo.attached? %>
|
||||
<%= image_tag mercury_item.logo, class: "rounded-full h-full w-full", loading: "lazy" %>
|
||||
<% else %>
|
||||
<div class="flex items-center justify-center">
|
||||
<%= tag.p mercury_item.name.first.upcase, class: "text-blue-600 text-xs font-medium" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if mercury_item.accounts.any? %>
|
||||
<p class="text-xs text-secondary">
|
||||
<%= mercury_item.institution_summary %>
|
||||
</p>
|
||||
<% end %>
|
||||
<% if mercury_item.syncing? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= icon "loader", size: "sm", class: "animate-spin" %>
|
||||
<%= tag.span t(".syncing") %>
|
||||
</div>
|
||||
<% elsif mercury_item.sync_error.present? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= render DS::Tooltip.new(text: mercury_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive", as: :span) %>
|
||||
<%= tag.span t(".error"), class: "text-destructive" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-secondary">
|
||||
<% if mercury_item.last_synced_at %>
|
||||
<% if mercury_item.sync_status_summary %>
|
||||
<%= t(".status_with_summary", timestamp: time_ago_in_words(mercury_item.last_synced_at), summary: mercury_item.sync_status_summary) %>
|
||||
<% else %>
|
||||
<%= t(".status", timestamp: time_ago_in_words(mercury_item.last_synced_at)) %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= t(".status_never") %>
|
||||
|
||||
<div class="pl-1 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= tag.p mercury_item.name, class: "font-medium text-primary" %>
|
||||
<% if mercury_item.scheduled_for_deletion? %>
|
||||
<p class="text-destructive text-sm animate-pulse"><%= t(".deletion_in_progress") %></p>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if mercury_item.accounts.any? %>
|
||||
<p class="text-xs text-secondary">
|
||||
<%= mercury_item.institution_summary %>
|
||||
</p>
|
||||
<% end %>
|
||||
<% if mercury_item.syncing? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= icon "loader", size: "sm", class: "animate-spin" %>
|
||||
<%= tag.span t(".syncing") %>
|
||||
</div>
|
||||
<% elsif mercury_item.sync_error.present? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= render DS::Tooltip.new(text: mercury_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive", as: :span) %>
|
||||
<%= tag.span t(".error"), class: "text-destructive" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-secondary">
|
||||
<% if mercury_item.last_synced_at %>
|
||||
<% if mercury_item.sync_status_summary %>
|
||||
<%= t(".status_with_summary", timestamp: time_ago_in_words(mercury_item.last_synced_at), summary: mercury_item.sync_status_summary) %>
|
||||
<% else %>
|
||||
<%= t(".status", timestamp: time_ago_in_words(mercury_item.last_synced_at)) %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= t(".status_never") %>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if Current.user&.admin? %>
|
||||
<div class="flex items-center gap-2">
|
||||
<% if Rails.env.development? %>
|
||||
<%= icon(
|
||||
"refresh-cw",
|
||||
as_button: true,
|
||||
href: sync_mercury_item_path(mercury_item)
|
||||
) %>
|
||||
<% end %>
|
||||
|
||||
<%= render DS::Menu.new do |menu| %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: t(".delete"),
|
||||
icon: "trash-2",
|
||||
href: mercury_item_path(mercury_item),
|
||||
method: :delete,
|
||||
confirm: CustomConfirm.for_resource_deletion(mercury_item.name, high_severity: true)
|
||||
) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if Current.user&.admin? %>
|
||||
<div class="flex items-center gap-2">
|
||||
<% if Rails.env.development? %>
|
||||
<%= icon(
|
||||
"refresh-cw",
|
||||
as_button: true,
|
||||
href: sync_mercury_item_path(mercury_item)
|
||||
) %>
|
||||
<% end %>
|
||||
|
||||
<%= render DS::Menu.new do |menu| %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: t(".delete"),
|
||||
icon: "trash-2",
|
||||
href: mercury_item_path(mercury_item),
|
||||
method: :delete,
|
||||
confirm: CustomConfirm.for_resource_deletion(mercury_item.name, high_severity: true)
|
||||
) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</summary>
|
||||
<% end %>
|
||||
|
||||
<% unless mercury_item.scheduled_for_deletion? %>
|
||||
<div class="space-y-4 mt-4">
|
||||
@@ -128,5 +130,5 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</details>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,82 +1,84 @@
|
||||
<%# locals: (plaid_item:) %>
|
||||
|
||||
<%= tag.div id: dom_id(plaid_item) do %>
|
||||
<details open class="group bg-container p-4 shadow-border-xs rounded-xl">
|
||||
<summary class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon "chevron-right", class: "group-open:transform group-open:rotate-90" %>
|
||||
<%= render DS::Disclosure.new(variant: :card, open: true) do |disclosure| %>
|
||||
<% disclosure.with_summary_content do %>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon "chevron-right", class: "group-open:rotate-90 motion-safe:transition-transform motion-safe:duration-150" %>
|
||||
|
||||
<div class="flex items-center justify-center h-8 w-8 bg-blue-600/10 rounded-full">
|
||||
<% if plaid_item.logo.attached? %>
|
||||
<%= image_tag plaid_item.logo, class: "rounded-full h-full w-full", loading: "lazy" %>
|
||||
<% else %>
|
||||
<div class="flex items-center justify-center">
|
||||
<%= tag.p plaid_item.name.first.upcase, class: "text-blue-600 text-xs font-medium" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="pl-1 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= tag.p plaid_item.name, class: "font-medium text-primary" %>
|
||||
<% if plaid_item.scheduled_for_deletion? %>
|
||||
<p class="text-destructive text-sm animate-pulse">(deletion in progress...)</p>
|
||||
<div class="flex items-center justify-center h-8 w-8 bg-blue-600/10 rounded-full">
|
||||
<% if plaid_item.logo.attached? %>
|
||||
<%= image_tag plaid_item.logo, class: "rounded-full h-full w-full", loading: "lazy" %>
|
||||
<% else %>
|
||||
<div class="flex items-center justify-center">
|
||||
<%= tag.p plaid_item.name.first.upcase, class: "text-blue-600 text-xs font-medium" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if plaid_item.syncing? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= icon "loader", size: "sm", class: "animate-pulse" %>
|
||||
<%= tag.span t(".syncing") %>
|
||||
|
||||
<div class="pl-1 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= tag.p plaid_item.name, class: "font-medium text-primary" %>
|
||||
<% if plaid_item.scheduled_for_deletion? %>
|
||||
<p class="text-destructive text-sm animate-pulse">(deletion in progress...)</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% elsif plaid_item.requires_update? %>
|
||||
<div class="text-warning flex items-center gap-1">
|
||||
<%= icon "alert-triangle", size: "sm", color: "warning" %>
|
||||
<%= tag.span t(".requires_update") %>
|
||||
</div>
|
||||
<% elsif plaid_item.sync_error.present? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= icon "alert-circle", size: "sm", color: "destructive" %>
|
||||
<%= tag.span t(".error"), class: "text-destructive" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-secondary">
|
||||
<%= plaid_item.last_synced_at ? t(".status", timestamp: time_ago_in_words(plaid_item.last_synced_at)) : t(".status_never") %>
|
||||
</p>
|
||||
<% end %>
|
||||
<% if plaid_item.syncing? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= icon "loader", size: "sm", class: "animate-pulse" %>
|
||||
<%= tag.span t(".syncing") %>
|
||||
</div>
|
||||
<% elsif plaid_item.requires_update? %>
|
||||
<div class="text-warning flex items-center gap-1">
|
||||
<%= icon "alert-triangle", size: "sm", color: "warning" %>
|
||||
<%= tag.span t(".requires_update") %>
|
||||
</div>
|
||||
<% elsif plaid_item.sync_error.present? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= icon "alert-circle", size: "sm", color: "destructive" %>
|
||||
<%= tag.span t(".error"), class: "text-destructive" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-secondary">
|
||||
<%= plaid_item.last_synced_at ? t(".status", timestamp: time_ago_in_words(plaid_item.last_synced_at)) : t(".status_never") %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if Current.user&.admin? %>
|
||||
<div class="flex items-center gap-2">
|
||||
<% if plaid_item.requires_update? %>
|
||||
<%= render DS::Link.new(
|
||||
text: t(".update"),
|
||||
icon: "refresh-cw",
|
||||
variant: "secondary",
|
||||
href: edit_plaid_item_path(plaid_item),
|
||||
frame: "modal"
|
||||
) %>
|
||||
<% elsif Rails.env.development? %>
|
||||
<%= icon(
|
||||
"refresh-cw",
|
||||
as_button: true,
|
||||
href: sync_plaid_item_path(plaid_item)
|
||||
) %>
|
||||
<% end %>
|
||||
|
||||
<%= render DS::Menu.new do |menu| %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: t(".delete"),
|
||||
icon: "trash-2",
|
||||
href: plaid_item_path(plaid_item),
|
||||
method: :delete,
|
||||
confirm: CustomConfirm.for_resource_deletion(plaid_item.name, high_severity: true)
|
||||
) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if Current.user&.admin? %>
|
||||
<div class="flex items-center gap-2">
|
||||
<% if plaid_item.requires_update? %>
|
||||
<%= render DS::Link.new(
|
||||
text: t(".update"),
|
||||
icon: "refresh-cw",
|
||||
variant: "secondary",
|
||||
href: edit_plaid_item_path(plaid_item),
|
||||
frame: "modal"
|
||||
) %>
|
||||
<% elsif Rails.env.development? %>
|
||||
<%= icon(
|
||||
"refresh-cw",
|
||||
as_button: true,
|
||||
href: sync_plaid_item_path(plaid_item)
|
||||
) %>
|
||||
<% end %>
|
||||
|
||||
<%= render DS::Menu.new do |menu| %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: t(".delete"),
|
||||
icon: "trash-2",
|
||||
href: plaid_item_path(plaid_item),
|
||||
method: :delete,
|
||||
confirm: CustomConfirm.for_resource_deletion(plaid_item.name, high_severity: true)
|
||||
) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</summary>
|
||||
<% end %>
|
||||
|
||||
<% unless plaid_item.scheduled_for_deletion? %>
|
||||
<div class="space-y-4 mt-4">
|
||||
@@ -103,5 +105,5 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</details>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -127,18 +127,13 @@
|
||||
aria-atomic="true"></p>
|
||||
</div>
|
||||
<div data-controller="list-filter" class="space-y-3">
|
||||
<div class="relative">
|
||||
<input type="search"
|
||||
autocomplete="off"
|
||||
placeholder="<%= t(".currency_search_placeholder") %>"
|
||||
aria-label="<%= t(".currency_search_placeholder") %>"
|
||||
data-list-filter-target="input"
|
||||
data-action="input->list-filter#filter"
|
||||
class="block w-full border border-secondary rounded-md py-2.5 pl-10 pr-3 bg-container focus:ring-gray-500 sm:text-sm">
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<%= icon("search", class: "text-secondary") %>
|
||||
</div>
|
||||
</div>
|
||||
<%= render DS::SearchInput.new(
|
||||
placeholder: t(".currency_search_placeholder"),
|
||||
data: {
|
||||
list_filter_target: "input",
|
||||
action: "input->list-filter#filter"
|
||||
}
|
||||
) %>
|
||||
<div data-list-filter-target="list" class="max-h-80 overflow-auto rounded-xl border border-secondary divide-y divide-alpha-black-100 theme-dark:divide-alpha-white-100">
|
||||
<p class="hidden px-4 py-3 text-sm text-secondary" data-list-filter-target="emptyMessage">
|
||||
<%= t(".no_matching_currencies") %>
|
||||
|
||||
@@ -38,18 +38,20 @@
|
||||
<% if active_items.any? %>
|
||||
<div class="space-y-3">
|
||||
<% active_items.each do |item| %>
|
||||
<details class="group bg-container p-4 shadow-border-xs rounded-xl">
|
||||
<summary class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center justify-center h-8 w-8 bg-container-inset rounded-full">
|
||||
<p class="text-primary text-xs font-medium"><%= item.name.to_s.first.to_s.upcase %></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium text-primary"><%= item.name %></p>
|
||||
<p class="text-xs text-secondary"><%= item.sync_status_summary %></p>
|
||||
<%= render DS::Disclosure.new(variant: :card) do |disclosure| %>
|
||||
<% disclosure.with_summary_content do %>
|
||||
<div class="flex items-center justify-between gap-2 w-full">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center justify-center h-8 w-8 bg-container-inset rounded-full">
|
||||
<p class="text-primary text-xs font-medium"><%= item.name.to_s.first.to_s.upcase %></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium text-primary"><%= item.name %></p>
|
||||
<p class="text-xs text-secondary"><%= item.sync_status_summary %></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</summary>
|
||||
<% end %>
|
||||
|
||||
<div class="mt-4 space-y-4">
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -102,16 +104,18 @@
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</details>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<details <%= "open" unless active_items.any? %> class="group bg-container p-4 shadow-border-xs rounded-xl">
|
||||
<summary class="flex items-center gap-2 text-sm font-medium text-primary">
|
||||
<%= icon "plus" %>
|
||||
<%= t("brex_items.provider_panel.add_connection") %>
|
||||
</summary>
|
||||
<%= render DS::Disclosure.new(variant: :card, open: active_items.none?) do |disclosure| %>
|
||||
<% disclosure.with_summary_content do %>
|
||||
<div class="flex items-center gap-2 text-sm font-medium text-primary">
|
||||
<%= icon "plus" %>
|
||||
<%= t("brex_items.provider_panel.add_connection") %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% brex_item = Current.family.brex_items.build(name: t("brex_items.provider_panel.default_connection_name")) %>
|
||||
<%= styled_form_with model: brex_item,
|
||||
@@ -138,7 +142,7 @@
|
||||
<%= form.submit t("brex_items.provider_panel.add_connection") %>
|
||||
</div>
|
||||
<% end %>
|
||||
</details>
|
||||
<% end %>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<% if credentialed_items.any? %>
|
||||
|
||||
@@ -24,24 +24,26 @@
|
||||
<% if items.any? %>
|
||||
<div class="space-y-3">
|
||||
<% items.each do |item| %>
|
||||
<details class="group bg-container p-4 shadow-border-xs rounded-xl">
|
||||
<summary class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<div class="flex items-center justify-center h-8 w-8 rounded-full bg-surface-inset">
|
||||
<%= icon "waves", size: "sm", class: "text-primary" %>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="font-medium text-primary truncate"><%= item.name %></p>
|
||||
<p class="text-xs text-secondary">
|
||||
<% if item.syncing? %>
|
||||
<%= t("settings.providers.kraken_panel.syncing") %>
|
||||
<% else %>
|
||||
<%= item.sync_status_summary %>
|
||||
<% end %>
|
||||
</p>
|
||||
<%= render DS::Disclosure.new(variant: :card) do |disclosure| %>
|
||||
<% disclosure.with_summary_content do %>
|
||||
<div class="flex items-center justify-between gap-3 w-full">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<div class="flex items-center justify-center h-8 w-8 rounded-full bg-surface-inset">
|
||||
<%= icon "waves", size: "sm", class: "text-primary" %>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="font-medium text-primary truncate"><%= item.name %></p>
|
||||
<p class="text-xs text-secondary">
|
||||
<% if item.syncing? %>
|
||||
<%= t("settings.providers.kraken_panel.syncing") %>
|
||||
<% else %>
|
||||
<%= item.sync_status_summary %>
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</summary>
|
||||
<% end %>
|
||||
|
||||
<div class="mt-4 space-y-4">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
@@ -97,7 +99,7 @@
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</details>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -19,18 +19,20 @@
|
||||
<% if active_items.any? %>
|
||||
<div class="space-y-3">
|
||||
<% active_items.each do |item| %>
|
||||
<details class="group bg-container p-4 shadow-border-xs rounded-xl">
|
||||
<summary class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center justify-center h-8 w-8 bg-blue-600/10 rounded-full">
|
||||
<p class="text-blue-600 text-xs font-medium"><%= item.name.to_s.first.to_s.upcase %></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium text-primary"><%= item.name %></p>
|
||||
<p class="text-xs text-secondary"><%= item.sync_status_summary %></p>
|
||||
<%= render DS::Disclosure.new(variant: :card) do |disclosure| %>
|
||||
<% disclosure.with_summary_content do %>
|
||||
<div class="flex items-center justify-between gap-2 w-full">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center justify-center h-8 w-8 bg-blue-600/10 rounded-full">
|
||||
<p class="text-blue-600 text-xs font-medium"><%= item.name.to_s.first.to_s.upcase %></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium text-primary"><%= item.name %></p>
|
||||
<p class="text-xs text-secondary"><%= item.sync_status_summary %></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</summary>
|
||||
<% end %>
|
||||
|
||||
<div class="mt-4 space-y-4">
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -82,7 +84,7 @@
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</details>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
<div class="flex flex-wrap items-center gap-2.5 mt-5 mb-3">
|
||||
<div class="relative flex-1 min-w-[200px]">
|
||||
<input type="search"
|
||||
autocomplete="off"
|
||||
data-providers-filter-target="input"
|
||||
data-action="input->providers-filter#filter"
|
||||
aria-label="<%= t("settings.providers.search_filters.aria_label") %>"
|
||||
placeholder="<%= t("settings.providers.search_filters.placeholder") %>"
|
||||
class="block w-full border border-secondary rounded-md py-2.5 pl-10 pr-3 bg-container focus:ring-gray-500 sm:text-sm">
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<%= icon "search", class: "text-secondary" %>
|
||||
</div>
|
||||
</div>
|
||||
<%= render DS::SearchInput.new(
|
||||
class: "flex-1 min-w-[200px]",
|
||||
placeholder: t("settings.providers.search_filters.placeholder"),
|
||||
aria_label: t("settings.providers.search_filters.aria_label"),
|
||||
data: {
|
||||
providers_filter_target: "input",
|
||||
action: "input->providers-filter#filter"
|
||||
}
|
||||
) %>
|
||||
<div class="inline-flex items-center gap-1 p-1 bg-surface-inset rounded-xl">
|
||||
<% %w[all bank crypto investment].each do |kind| %>
|
||||
<% active = kind == "all" %>
|
||||
|
||||
@@ -16,175 +16,177 @@
|
||||
end
|
||||
end %>
|
||||
|
||||
<details open class="group bg-container p-4 shadow-border-xs rounded-xl">
|
||||
<summary class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon "chevron-right", class: "group-open:transform group-open:rotate-90" %>
|
||||
<%= render DS::Disclosure.new(variant: :card, open: true) do |disclosure| %>
|
||||
<% disclosure.with_summary_content do %>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon "chevron-right", class: "group-open:rotate-90 motion-safe:transition-transform motion-safe:duration-150" %>
|
||||
|
||||
<div class="flex items-center justify-center h-8 w-8 bg-success/10 rounded-full">
|
||||
<% if simplefin_item.logo.attached? %>
|
||||
<%= image_tag simplefin_item.logo, class: "rounded-full h-full w-full", loading: "lazy" %>
|
||||
<% else %>
|
||||
<div class="flex items-center justify-center">
|
||||
<%= tag.p simplefin_item.name.first.upcase, class: "text-success text-xs font-medium" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="pl-1 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= tag.p simplefin_item.name, class: "font-medium text-primary" %>
|
||||
<% if simplefin_item.scheduled_for_deletion? %>
|
||||
<p class="text-destructive text-sm animate-pulse"><%= t(".deletion_in_progress") %></p>
|
||||
<div class="flex items-center justify-center h-8 w-8 bg-success/10 rounded-full">
|
||||
<% if simplefin_item.logo.attached? %>
|
||||
<%= image_tag simplefin_item.logo, class: "rounded-full h-full w-full", loading: "lazy" %>
|
||||
<% else %>
|
||||
<div class="flex items-center justify-center">
|
||||
<%= tag.p simplefin_item.name.first.upcase, class: "text-success text-xs font-medium" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if simplefin_item.accounts.any? %>
|
||||
<p class="text-xs text-secondary">
|
||||
<%= simplefin_item.institution_summary %>
|
||||
</p>
|
||||
<%# Extra inline badges from latest sync stats - only show warnings %>
|
||||
<% stats = (@simplefin_sync_stats_map || {})[simplefin_item.id] || {} %>
|
||||
<% has_warnings = stats["accounts_skipped"].to_i > 0 ||
|
||||
stats["rate_limited"].present? ||
|
||||
stats["rate_limited_at"].present? ||
|
||||
stats["total_errors"].to_i > 0 ||
|
||||
(stats["errors"].is_a?(Array) && stats["errors"].any?) %>
|
||||
<% if has_warnings %>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<% if stats["accounts_skipped"].to_i > 0 %>
|
||||
<%= render DS::Tooltip.new(text: t(".accounts_skipped_tooltip"), icon: "alert-triangle", size: "sm", color: "warning", as: :span) %>
|
||||
<span class="text-xs text-warning"><%= t(".accounts_skipped_label", count: stats["accounts_skipped"].to_i) %></span>
|
||||
<% end %>
|
||||
|
||||
<% if stats["rate_limited"].present? || stats["rate_limited_at"].present? %>
|
||||
<% ts = stats["rate_limited_at"] %>
|
||||
<% ago = (ts.present? ? (begin; time_ago_in_words(Time.parse(ts)); rescue StandardError; nil; end) : nil) %>
|
||||
<%= render DS::Tooltip.new(
|
||||
text: (ago ? t(".rate_limited_ago", time: ago) : t(".rate_limited_recently")),
|
||||
icon: "clock",
|
||||
size: "sm",
|
||||
color: "warning",
|
||||
as: :span
|
||||
) %>
|
||||
<% end %>
|
||||
|
||||
<% if stats["total_errors"].to_i > 0 || (stats["errors"].is_a?(Array) && stats["errors"].any?) %>
|
||||
<% tooltip_text = simplefin_error_tooltip(stats) %>
|
||||
<% if tooltip_text.present? %>
|
||||
<%= render DS::Tooltip.new(text: tooltip_text, icon: "alert-octagon", size: "sm", color: "warning", as: :span) %>
|
||||
<div class="pl-1 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= tag.p simplefin_item.name, class: "font-medium text-primary" %>
|
||||
<% if simplefin_item.scheduled_for_deletion? %>
|
||||
<p class="text-destructive text-sm animate-pulse"><%= t(".deletion_in_progress") %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if simplefin_item.accounts.any? %>
|
||||
<p class="text-xs text-secondary">
|
||||
<%= simplefin_item.institution_summary %>
|
||||
</p>
|
||||
<%# Extra inline badges from latest sync stats - only show warnings %>
|
||||
<% stats = (@simplefin_sync_stats_map || {})[simplefin_item.id] || {} %>
|
||||
<% has_warnings = stats["accounts_skipped"].to_i > 0 ||
|
||||
stats["rate_limited"].present? ||
|
||||
stats["rate_limited_at"].present? ||
|
||||
stats["total_errors"].to_i > 0 ||
|
||||
(stats["errors"].is_a?(Array) && stats["errors"].any?) %>
|
||||
<% if has_warnings %>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<% if stats["accounts_skipped"].to_i > 0 %>
|
||||
<%= render DS::Tooltip.new(text: t(".accounts_skipped_tooltip"), icon: "alert-triangle", size: "sm", color: "warning", as: :span) %>
|
||||
<span class="text-xs text-warning"><%= t(".accounts_skipped_label", count: stats["accounts_skipped"].to_i) %></span>
|
||||
<% end %>
|
||||
|
||||
<% if stats["rate_limited"].present? || stats["rate_limited_at"].present? %>
|
||||
<% ts = stats["rate_limited_at"] %>
|
||||
<% ago = (ts.present? ? (begin; time_ago_in_words(Time.parse(ts)); rescue StandardError; nil; end) : nil) %>
|
||||
<%= render DS::Tooltip.new(
|
||||
text: (ago ? t(".rate_limited_ago", time: ago) : t(".rate_limited_recently")),
|
||||
icon: "clock",
|
||||
size: "sm",
|
||||
color: "warning",
|
||||
as: :span
|
||||
) %>
|
||||
<% end %>
|
||||
|
||||
<% if stats["total_errors"].to_i > 0 || (stats["errors"].is_a?(Array) && stats["errors"].any?) %>
|
||||
<% tooltip_text = simplefin_error_tooltip(stats) %>
|
||||
<% if tooltip_text.present? %>
|
||||
<%= render DS::Tooltip.new(text: tooltip_text, icon: "alert-octagon", size: "sm", color: "warning", as: :span) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%# Determine if all reported errors are benign duplicate-skips (suppress scary banner). Computed in controller for testability. %>
|
||||
<% duplicate_only_errors = (@simplefin_duplicate_only_map || {})[simplefin_item.id] || false %>
|
||||
<% if simplefin_item.syncing? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= icon "loader", size: "sm", class: "animate-spin" %>
|
||||
<%= tag.span t(".syncing") %>
|
||||
</div>
|
||||
<% elsif simplefin_item.requires_update? %>
|
||||
<div class="text-warning flex items-center gap-1">
|
||||
<%= icon "alert-triangle", size: "sm", color: "warning" %>
|
||||
<%= tag.span t(".requires_update") %>
|
||||
</div>
|
||||
<% elsif (stale_status = simplefin_item.stale_sync_status)[:stale] %>
|
||||
<div class="text-warning flex items-center gap-1">
|
||||
<%= icon "alert-circle", size: "sm", color: "warning" %>
|
||||
<%= tag.span stale_status[:message], class: "text-sm" %>
|
||||
</div>
|
||||
<% elsif (pending_status = simplefin_item.stale_pending_status)[:count] > 0 %>
|
||||
<div class="text-secondary">
|
||||
<div class="flex items-center gap-1">
|
||||
<%= icon "clock", size: "sm", color: "secondary" %>
|
||||
<%= tag.span pending_status[:message], class: "text-sm" %>
|
||||
<span class="text-xs text-subdued"><%= t(".stale_pending_note") %></span>
|
||||
</div>
|
||||
<% if pending_status[:accounts]&.any? %>
|
||||
<div class="text-xs text-subdued ml-5">
|
||||
<%= t(".stale_pending_accounts", accounts: pending_status[:accounts].join(", ")) %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% elsif (reconciled_status = simplefin_item.last_sync_reconciled_status)[:count] > 0 %>
|
||||
<div class="text-success flex items-center gap-1">
|
||||
<%= icon "check-circle", size: "sm", color: "success" %>
|
||||
<%= tag.span reconciled_status[:message], class: "text-sm" %>
|
||||
<span class="text-xs text-subdued"><%= t(".reconciled_details_note") %></span>
|
||||
</div>
|
||||
<% elsif simplefin_item.rate_limited_message.present? %>
|
||||
<div class="text-warning flex items-center gap-1">
|
||||
<%= icon "clock", size: "sm", color: "warning" %>
|
||||
<%= tag.span simplefin_item.rate_limited_message %>
|
||||
</div>
|
||||
<% elsif simplefin_item.sync_error.present? && !duplicate_only_errors %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= render DS::Tooltip.new(text: simplefin_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive", as: :span) %>
|
||||
<%= tag.span t(".error"), class: "text-destructive" %>
|
||||
</div>
|
||||
<% elsif duplicate_only_errors %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= icon "info", size: "sm" %>
|
||||
<%= tag.span t(".duplicate_accounts_skipped"), class: "text-secondary" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-secondary">
|
||||
<% if simplefin_item.last_synced_at %>
|
||||
<% if simplefin_item.sync_status_summary %>
|
||||
<%= t(".status_with_summary", timestamp: time_ago_in_words(simplefin_item.last_synced_at), summary: simplefin_item.sync_status_summary) %>
|
||||
<% else %>
|
||||
<%= t(".status", timestamp: time_ago_in_words(simplefin_item.last_synced_at)) %>
|
||||
<% end %>
|
||||
<%# Determine if all reported errors are benign duplicate-skips (suppress scary banner). Computed in controller for testability. %>
|
||||
<% duplicate_only_errors = (@simplefin_duplicate_only_map || {})[simplefin_item.id] || false %>
|
||||
<% if simplefin_item.syncing? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= icon "loader", size: "sm", class: "animate-spin" %>
|
||||
<%= tag.span t(".syncing") %>
|
||||
</div>
|
||||
<% elsif simplefin_item.requires_update? %>
|
||||
<div class="text-warning flex items-center gap-1">
|
||||
<%= icon "alert-triangle", size: "sm", color: "warning" %>
|
||||
<%= tag.span t(".requires_update") %>
|
||||
</div>
|
||||
<% elsif (stale_status = simplefin_item.stale_sync_status)[:stale] %>
|
||||
<div class="text-warning flex items-center gap-1">
|
||||
<%= icon "alert-circle", size: "sm", color: "warning" %>
|
||||
<%= tag.span stale_status[:message], class: "text-sm" %>
|
||||
</div>
|
||||
<% elsif (pending_status = simplefin_item.stale_pending_status)[:count] > 0 %>
|
||||
<div class="text-secondary">
|
||||
<div class="flex items-center gap-1">
|
||||
<%= icon "clock", size: "sm", color: "secondary" %>
|
||||
<%= tag.span pending_status[:message], class: "text-sm" %>
|
||||
<span class="text-xs text-subdued"><%= t(".stale_pending_note") %></span>
|
||||
</div>
|
||||
<% if pending_status[:accounts]&.any? %>
|
||||
<div class="text-xs text-subdued ml-5">
|
||||
<%= t(".stale_pending_accounts", accounts: pending_status[:accounts].join(", ")) %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= t(".status_never") %>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% elsif (reconciled_status = simplefin_item.last_sync_reconciled_status)[:count] > 0 %>
|
||||
<div class="text-success flex items-center gap-1">
|
||||
<%= icon "check-circle", size: "sm", color: "success" %>
|
||||
<%= tag.span reconciled_status[:message], class: "text-sm" %>
|
||||
<span class="text-xs text-subdued"><%= t(".reconciled_details_note") %></span>
|
||||
</div>
|
||||
<% elsif simplefin_item.rate_limited_message.present? %>
|
||||
<div class="text-warning flex items-center gap-1">
|
||||
<%= icon "clock", size: "sm", color: "warning" %>
|
||||
<%= tag.span simplefin_item.rate_limited_message %>
|
||||
</div>
|
||||
<% elsif simplefin_item.sync_error.present? && !duplicate_only_errors %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= render DS::Tooltip.new(text: simplefin_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive", as: :span) %>
|
||||
<%= tag.span t(".error"), class: "text-destructive" %>
|
||||
</div>
|
||||
<% elsif duplicate_only_errors %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= icon "info", size: "sm" %>
|
||||
<%= tag.span t(".duplicate_accounts_skipped"), class: "text-secondary" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-secondary">
|
||||
<% if simplefin_item.last_synced_at %>
|
||||
<% if simplefin_item.sync_status_summary %>
|
||||
<%= t(".status_with_summary", timestamp: time_ago_in_words(simplefin_item.last_synced_at), summary: simplefin_item.sync_status_summary) %>
|
||||
<% else %>
|
||||
<%= t(".status", timestamp: time_ago_in_words(simplefin_item.last_synced_at)) %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= t(".status_never") %>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if Current.user&.admin? %>
|
||||
<div class="flex items-center gap-2">
|
||||
<% if simplefin_item.requires_update? %>
|
||||
<%= render DS::Link.new(
|
||||
text: t(".update"),
|
||||
icon: "refresh-cw",
|
||||
variant: "secondary",
|
||||
href: edit_simplefin_item_path(simplefin_item),
|
||||
frame: "modal"
|
||||
) %>
|
||||
<% else %>
|
||||
<%= icon(
|
||||
"refresh-cw",
|
||||
as_button: true,
|
||||
href: sync_simplefin_item_path(simplefin_item),
|
||||
disabled: simplefin_item.syncing?
|
||||
) %>
|
||||
<% end %>
|
||||
|
||||
<%= render DS::Menu.new do |menu| %>
|
||||
<% if unlinked_count.to_i > 0 %>
|
||||
<% menu.with_item(
|
||||
variant: "link",
|
||||
text: t(".setup_accounts_menu"),
|
||||
icon: "settings",
|
||||
href: setup_accounts_simplefin_item_path(simplefin_item),
|
||||
frame: :modal
|
||||
<% if Current.user&.admin? %>
|
||||
<div class="flex items-center gap-2">
|
||||
<% if simplefin_item.requires_update? %>
|
||||
<%= render DS::Link.new(
|
||||
text: t(".update"),
|
||||
icon: "refresh-cw",
|
||||
variant: "secondary",
|
||||
href: edit_simplefin_item_path(simplefin_item),
|
||||
frame: "modal"
|
||||
) %>
|
||||
<% else %>
|
||||
<%= icon(
|
||||
"refresh-cw",
|
||||
as_button: true,
|
||||
href: sync_simplefin_item_path(simplefin_item),
|
||||
disabled: simplefin_item.syncing?
|
||||
) %>
|
||||
<% end %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: t(".delete"),
|
||||
icon: "trash-2",
|
||||
href: simplefin_item_path(simplefin_item),
|
||||
method: :delete,
|
||||
confirm: CustomConfirm.for_resource_deletion(simplefin_item.name, high_severity: true)
|
||||
) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</summary>
|
||||
|
||||
<%= render DS::Menu.new do |menu| %>
|
||||
<% if unlinked_count.to_i > 0 %>
|
||||
<% menu.with_item(
|
||||
variant: "link",
|
||||
text: t(".setup_accounts_menu"),
|
||||
icon: "settings",
|
||||
href: setup_accounts_simplefin_item_path(simplefin_item),
|
||||
frame: :modal
|
||||
) %>
|
||||
<% end %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: t(".delete"),
|
||||
icon: "trash-2",
|
||||
href: simplefin_item_path(simplefin_item),
|
||||
method: :delete,
|
||||
confirm: CustomConfirm.for_resource_deletion(simplefin_item.name, high_severity: true)
|
||||
) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% unless simplefin_item.scheduled_for_deletion? %>
|
||||
<div class="space-y-4 mt-4">
|
||||
@@ -237,5 +239,5 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</details>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,114 +1,116 @@
|
||||
<%# locals: (snaptrade_item:) %>
|
||||
|
||||
<%= tag.div id: dom_id(snaptrade_item) do %>
|
||||
<details open class="group bg-container p-4 shadow-border-xs rounded-xl">
|
||||
<summary class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon "chevron-right", class: "group-open:transform group-open:rotate-90" %>
|
||||
<% unlinked_count = snaptrade_item.unlinked_accounts_count %>
|
||||
|
||||
<div class="flex items-center justify-center h-8 w-8 bg-success/10 rounded-full">
|
||||
<% if snaptrade_item.logo.attached? %>
|
||||
<%= image_tag snaptrade_item.logo, class: "rounded-full h-full w-full", loading: "lazy" %>
|
||||
<% else %>
|
||||
<div class="flex items-center justify-center">
|
||||
<%= tag.p snaptrade_item.name.first.upcase, class: "text-success text-xs font-medium" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= render DS::Disclosure.new(variant: :card, open: true) do |disclosure| %>
|
||||
<% disclosure.with_summary_content do %>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon "chevron-right", class: "group-open:rotate-90 motion-safe:transition-transform motion-safe:duration-150" %>
|
||||
|
||||
<% unlinked_count = snaptrade_item.unlinked_accounts_count %>
|
||||
|
||||
<div class="pl-1 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= tag.p snaptrade_item.name, class: "font-medium text-primary" %>
|
||||
<% if snaptrade_item.scheduled_for_deletion? %>
|
||||
<p class="text-destructive text-sm animate-pulse"><%= t(".deletion_in_progress") %></p>
|
||||
<div class="flex items-center justify-center h-8 w-8 bg-success/10 rounded-full">
|
||||
<% if snaptrade_item.logo.attached? %>
|
||||
<%= image_tag snaptrade_item.logo, class: "rounded-full h-full w-full", loading: "lazy" %>
|
||||
<% else %>
|
||||
<div class="flex items-center justify-center">
|
||||
<%= tag.p snaptrade_item.name.first.upcase, class: "text-success text-xs font-medium" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if snaptrade_item.snaptrade_accounts.any? %>
|
||||
<p class="text-xs text-secondary">
|
||||
<%= snaptrade_item.brokerage_summary %>
|
||||
</p>
|
||||
<% end %>
|
||||
<% if snaptrade_item.syncing? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= icon "loader", size: "sm", class: "animate-spin" %>
|
||||
<%= tag.span t(".syncing") %>
|
||||
</div>
|
||||
<% elsif snaptrade_item.requires_update? %>
|
||||
<div class="text-warning flex items-center gap-1">
|
||||
<%= icon "alert-triangle", size: "sm", color: "warning" %>
|
||||
<%= tag.span t(".requires_update") %>
|
||||
</div>
|
||||
<% elsif snaptrade_item.sync_error.present? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= render DS::Tooltip.new(text: snaptrade_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive", as: :span) %>
|
||||
<%= tag.span t(".error"), class: "text-destructive" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-secondary">
|
||||
<% if snaptrade_item.last_synced_at %>
|
||||
<%= t(".status", timestamp: time_ago_in_words(snaptrade_item.last_synced_at), summary: snaptrade_item.sync_status_summary) %>
|
||||
<% else %>
|
||||
<%= t(".status_never") %>
|
||||
|
||||
<div class="pl-1 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= tag.p snaptrade_item.name, class: "font-medium text-primary" %>
|
||||
<% if snaptrade_item.scheduled_for_deletion? %>
|
||||
<p class="text-destructive text-sm animate-pulse"><%= t(".deletion_in_progress") %></p>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if snaptrade_item.snaptrade_accounts.any? %>
|
||||
<p class="text-xs text-secondary">
|
||||
<%= snaptrade_item.brokerage_summary %>
|
||||
</p>
|
||||
<% end %>
|
||||
<% if snaptrade_item.syncing? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= icon "loader", size: "sm", class: "animate-spin" %>
|
||||
<%= tag.span t(".syncing") %>
|
||||
</div>
|
||||
<% elsif snaptrade_item.requires_update? %>
|
||||
<div class="text-warning flex items-center gap-1">
|
||||
<%= icon "alert-triangle", size: "sm", color: "warning" %>
|
||||
<%= tag.span t(".requires_update") %>
|
||||
</div>
|
||||
<% elsif snaptrade_item.sync_error.present? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= render DS::Tooltip.new(text: snaptrade_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive", as: :span) %>
|
||||
<%= tag.span t(".error"), class: "text-destructive" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-secondary">
|
||||
<% if snaptrade_item.last_synced_at %>
|
||||
<%= t(".status", timestamp: time_ago_in_words(snaptrade_item.last_synced_at), summary: snaptrade_item.sync_status_summary) %>
|
||||
<% else %>
|
||||
<%= t(".status_never") %>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if Current.user&.admin? %>
|
||||
<div class="flex items-center gap-2">
|
||||
<% if snaptrade_item.requires_update? || !snaptrade_item.user_registered? %>
|
||||
<%= render DS::Link.new(
|
||||
text: t(".reconnect"),
|
||||
icon: "link",
|
||||
variant: "secondary",
|
||||
href: connect_snaptrade_item_path(snaptrade_item)
|
||||
) %>
|
||||
<% else %>
|
||||
<%= icon(
|
||||
"refresh-cw",
|
||||
as_button: true,
|
||||
href: sync_snaptrade_item_path(snaptrade_item),
|
||||
disabled: snaptrade_item.syncing?
|
||||
) %>
|
||||
<% end %>
|
||||
|
||||
<%= render DS::Menu.new do |menu| %>
|
||||
<% menu.with_item(
|
||||
variant: "link",
|
||||
text: t(".connect_brokerage"),
|
||||
icon: "plus",
|
||||
href: connect_snaptrade_item_path(snaptrade_item)
|
||||
) %>
|
||||
<% if unlinked_count > 0 %>
|
||||
<% menu.with_item(
|
||||
variant: "link",
|
||||
text: t(".setup_accounts_menu"),
|
||||
icon: "settings",
|
||||
href: setup_accounts_snaptrade_item_path(snaptrade_item),
|
||||
frame: :modal
|
||||
<% if Current.user&.admin? %>
|
||||
<div class="flex items-center gap-2">
|
||||
<% if snaptrade_item.requires_update? || !snaptrade_item.user_registered? %>
|
||||
<%= render DS::Link.new(
|
||||
text: t(".reconnect"),
|
||||
icon: "link",
|
||||
variant: "secondary",
|
||||
href: connect_snaptrade_item_path(snaptrade_item)
|
||||
) %>
|
||||
<% else %>
|
||||
<%= icon(
|
||||
"refresh-cw",
|
||||
as_button: true,
|
||||
href: sync_snaptrade_item_path(snaptrade_item),
|
||||
disabled: snaptrade_item.syncing?
|
||||
) %>
|
||||
<% end %>
|
||||
<% menu.with_item(
|
||||
variant: "link",
|
||||
text: t(".manage_connections"),
|
||||
icon: "cable",
|
||||
href: settings_providers_path(manage: "1")
|
||||
) %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: t(".delete"),
|
||||
icon: "trash-2",
|
||||
href: snaptrade_item_path(snaptrade_item),
|
||||
method: :delete,
|
||||
confirm: CustomConfirm.for_resource_deletion(snaptrade_item.name, high_severity: true)
|
||||
) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</summary>
|
||||
|
||||
<%= render DS::Menu.new do |menu| %>
|
||||
<% menu.with_item(
|
||||
variant: "link",
|
||||
text: t(".connect_brokerage"),
|
||||
icon: "plus",
|
||||
href: connect_snaptrade_item_path(snaptrade_item)
|
||||
) %>
|
||||
<% if unlinked_count > 0 %>
|
||||
<% menu.with_item(
|
||||
variant: "link",
|
||||
text: t(".setup_accounts_menu"),
|
||||
icon: "settings",
|
||||
href: setup_accounts_snaptrade_item_path(snaptrade_item),
|
||||
frame: :modal
|
||||
) %>
|
||||
<% end %>
|
||||
<% menu.with_item(
|
||||
variant: "link",
|
||||
text: t(".manage_connections"),
|
||||
icon: "cable",
|
||||
href: settings_providers_path(manage: "1")
|
||||
) %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: t(".delete"),
|
||||
icon: "trash-2",
|
||||
href: snaptrade_item_path(snaptrade_item),
|
||||
method: :delete,
|
||||
confirm: CustomConfirm.for_resource_deletion(snaptrade_item.name, high_severity: true)
|
||||
) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% unless snaptrade_item.scheduled_for_deletion? %>
|
||||
<div class="space-y-4 mt-4">
|
||||
@@ -155,5 +157,5 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</details>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -4,105 +4,107 @@
|
||||
<% provider_display_name = sophtron_item.provider_display_name %>
|
||||
<% manual_sync_required = sophtron_item.manual_sync_required? %>
|
||||
<% connected_institution_options = sophtron_item.connected_institution_options %>
|
||||
<details open class="group bg-container p-4 shadow-border-xs rounded-xl">
|
||||
<summary class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon "chevron-right", class: "group-open:transform group-open:rotate-90" %>
|
||||
<%= render DS::Disclosure.new(variant: :card, open: true) do |disclosure| %>
|
||||
<% disclosure.with_summary_content do %>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon "chevron-right", class: "group-open:rotate-90 motion-safe:transition-transform motion-safe:duration-150" %>
|
||||
|
||||
<div class="flex items-center justify-center h-8 w-8 bg-orange-600/10 rounded-full">
|
||||
<% if sophtron_item.logo.attached? %>
|
||||
<%= image_tag sophtron_item.logo, class: "rounded-full h-full w-full", loading: "lazy" %>
|
||||
<% else %>
|
||||
<div class="flex items-center justify-center">
|
||||
<%= tag.p provider_display_name.first.upcase, class: "text-orange-600 text-xs font-medium" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="pl-1 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= tag.p provider_display_name, class: "font-medium text-primary" %>
|
||||
<% if manual_sync_required %>
|
||||
<span class="rounded-full bg-warning/10 px-2 py-0.5 text-xs font-medium text-warning"><%= t(".manual_sync") %></span>
|
||||
<% end %>
|
||||
<% if sophtron_item.scheduled_for_deletion? %>
|
||||
<p class="text-destructive text-sm animate-pulse"><%= t(".deletion_in_progress") %></p>
|
||||
<div class="flex items-center justify-center h-8 w-8 bg-orange-600/10 rounded-full">
|
||||
<% if sophtron_item.logo.attached? %>
|
||||
<%= image_tag sophtron_item.logo, class: "rounded-full h-full w-full", loading: "lazy" %>
|
||||
<% else %>
|
||||
<div class="flex items-center justify-center">
|
||||
<%= tag.p provider_display_name.first.upcase, class: "text-orange-600 text-xs font-medium" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if sophtron_item.accounts.any? %>
|
||||
<p class="text-xs text-secondary">
|
||||
<%= sophtron_item.institution_summary %>
|
||||
</p>
|
||||
<% end %>
|
||||
<% if sophtron_item.syncing? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= icon "loader", size: "sm", class: "animate-spin" %>
|
||||
<%= tag.span t(".syncing") %>
|
||||
</div>
|
||||
<% elsif sophtron_item.sync_error.present? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= render DS::Tooltip.new(text: sophtron_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive", as: :span) %>
|
||||
<%= tag.span t(".error"), class: "text-destructive" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-secondary">
|
||||
<% if sophtron_item.last_synced_at %>
|
||||
<% if sophtron_item.sync_status_summary %>
|
||||
<%= t(".status_with_summary", timestamp: time_ago_in_words(sophtron_item.last_synced_at), summary: sophtron_item.sync_status_summary) %>
|
||||
<% else %>
|
||||
<%= t(".status", timestamp: time_ago_in_words(sophtron_item.last_synced_at)) %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= t(".status_never") %>
|
||||
|
||||
<div class="pl-1 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= tag.p provider_display_name, class: "font-medium text-primary" %>
|
||||
<% if manual_sync_required %>
|
||||
<span class="rounded-full bg-warning/10 px-2 py-0.5 text-xs font-medium text-warning"><%= t(".manual_sync") %></span>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
<% if sophtron_item.scheduled_for_deletion? %>
|
||||
<p class="text-destructive text-sm animate-pulse"><%= t(".deletion_in_progress") %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if sophtron_item.accounts.any? %>
|
||||
<p class="text-xs text-secondary">
|
||||
<%= sophtron_item.institution_summary %>
|
||||
</p>
|
||||
<% end %>
|
||||
<% if sophtron_item.syncing? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= icon "loader", size: "sm", class: "animate-spin" %>
|
||||
<%= tag.span t(".syncing") %>
|
||||
</div>
|
||||
<% elsif sophtron_item.sync_error.present? %>
|
||||
<div class="text-secondary flex items-center gap-1">
|
||||
<%= render DS::Tooltip.new(text: sophtron_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive", as: :span) %>
|
||||
<%= tag.span t(".error"), class: "text-destructive" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-secondary">
|
||||
<% if sophtron_item.last_synced_at %>
|
||||
<% if sophtron_item.sync_status_summary %>
|
||||
<%= t(".status_with_summary", timestamp: time_ago_in_words(sophtron_item.last_synced_at), summary: sophtron_item.sync_status_summary) %>
|
||||
<% else %>
|
||||
<%= t(".status", timestamp: time_ago_in_words(sophtron_item.last_synced_at)) %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= t(".status_never") %>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<% if manual_sync_required || Rails.env.development? %>
|
||||
<%= render DS::Button.new(
|
||||
variant: :icon,
|
||||
icon: "refresh-cw",
|
||||
href: sync_sophtron_item_path(sophtron_item),
|
||||
frame: (manual_sync_required ? "modal" : nil),
|
||||
title: t(".sync_now")
|
||||
) %>
|
||||
<% end %>
|
||||
<div class="flex items-center gap-2">
|
||||
<% if manual_sync_required || Rails.env.development? %>
|
||||
<%= render DS::Button.new(
|
||||
variant: :icon,
|
||||
icon: "refresh-cw",
|
||||
href: sync_sophtron_item_path(sophtron_item),
|
||||
frame: (manual_sync_required ? "modal" : nil),
|
||||
title: t(".sync_now")
|
||||
) %>
|
||||
<% end %>
|
||||
|
||||
<%= render DS::Menu.new do |menu| %>
|
||||
<% if connected_institution_options.many? %>
|
||||
<% connected_institution_options.each do |institution| %>
|
||||
<% institution_manual_sync_required = sophtron_item.manual_sync_required_for_institution?(institution[:institution_key]) %>
|
||||
<%= render DS::Menu.new do |menu| %>
|
||||
<% if connected_institution_options.many? %>
|
||||
<% connected_institution_options.each do |institution| %>
|
||||
<% institution_manual_sync_required = sophtron_item.manual_sync_required_for_institution?(institution[:institution_key]) %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: t(institution_manual_sync_required ? ".automatic_sync_for" : ".manual_sync_action_for", institution: institution[:name]),
|
||||
icon: institution_manual_sync_required ? "refresh-cw" : "pause-circle",
|
||||
href: toggle_manual_sync_sophtron_item_path(sophtron_item, institution_key: institution[:institution_key]),
|
||||
method: :post
|
||||
) %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: t(institution_manual_sync_required ? ".automatic_sync_for" : ".manual_sync_action_for", institution: institution[:name]),
|
||||
icon: institution_manual_sync_required ? "refresh-cw" : "pause-circle",
|
||||
href: toggle_manual_sync_sophtron_item_path(sophtron_item, institution_key: institution[:institution_key]),
|
||||
text: t(manual_sync_required ? ".automatic_sync" : ".manual_sync_action"),
|
||||
icon: manual_sync_required ? "refresh-cw" : "pause-circle",
|
||||
href: toggle_manual_sync_sophtron_item_path(sophtron_item),
|
||||
method: :post
|
||||
) %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: t(manual_sync_required ? ".automatic_sync" : ".manual_sync_action"),
|
||||
icon: manual_sync_required ? "refresh-cw" : "pause-circle",
|
||||
href: toggle_manual_sync_sophtron_item_path(sophtron_item),
|
||||
method: :post
|
||||
text: t(".delete"),
|
||||
icon: "trash-2",
|
||||
href: sophtron_item_path(sophtron_item),
|
||||
method: :delete,
|
||||
confirm: CustomConfirm.for_resource_deletion(provider_display_name, high_severity: true)
|
||||
) %>
|
||||
<% end %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: t(".delete"),
|
||||
icon: "trash-2",
|
||||
href: sophtron_item_path(sophtron_item),
|
||||
method: :delete,
|
||||
confirm: CustomConfirm.for_resource_deletion(provider_display_name, high_severity: true)
|
||||
) %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</summary>
|
||||
<% end %>
|
||||
|
||||
<% unless sophtron_item.scheduled_for_deletion? %>
|
||||
<div class="space-y-4 mt-4">
|
||||
@@ -154,5 +156,5 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</details>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -33,14 +33,15 @@
|
||||
<% end %>
|
||||
</button>
|
||||
<div class="absolute z-50 p-1.5 w-full min-w-48 rounded-lg shadow-lg shadow-border-xs bg-container mt-1.5 transition duration-150 ease-out -translate-y-1 opacity-0 hidden" data-select-target="menu">
|
||||
<div class="relative flex items-center bg-container border border-secondary rounded-lg mb-1">
|
||||
<input type="search"
|
||||
placeholder="<%= t("helpers.select.search_placeholder") %>"
|
||||
autocomplete="off"
|
||||
class="bg-container text-sm placeholder:text-secondary font-normal h-10 pl-10 w-full border-none rounded-lg focus:outline-hidden focus:ring-0"
|
||||
data-list-filter-target="input"
|
||||
data-action="list-filter#filter">
|
||||
<%= icon("search", class: "absolute inset-0 ml-2 transform top-1/2 -translate-y-1/2") %>
|
||||
<div class="flex items-center bg-container border border-secondary rounded-lg mb-1 focus-within:ring-4 focus-within:ring-alpha-black-200 theme-dark:focus-within:ring-alpha-white-300 transition-shadow">
|
||||
<%= render DS::SearchInput.new(
|
||||
variant: :embedded,
|
||||
placeholder: t("helpers.select.search_placeholder"),
|
||||
data: {
|
||||
list_filter_target: "input",
|
||||
action: "list-filter#filter"
|
||||
}
|
||||
) %>
|
||||
</div>
|
||||
<div data-list-filter-target="list" data-select-target="content" class="flex flex-col gap-0.5 max-h-64 overflow-auto" role="listbox" tabindex="-1">
|
||||
<%# Uncategorized option %>
|
||||
|
||||
Reference in New Issue
Block a user