mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-05 05:25:24 +02:00
CSV Imports Overhaul (Transactions, Trades, Accounts, and Mint import support) (#1209)
* Remove stale 1.0 import logic and model * Fresh start * Checkpoint before removing nav * First working prototype * Add trade, account, and mint import flows * Basic working version with tests * System tests for each import type * Clean up mappings flow * Clean up PR, refactor stale code, tests * Add back row validations * Row validations * Fix import job test * Fix import navigation * Fix mint import configuration form * Currency preset for new accounts
This commit is contained in:
parent
23786b444a
commit
398b246965
103 changed files with 2420 additions and 1689 deletions
59
app/views/import/cleans/show.html.erb
Normal file
59
app/views/import/cleans/show.html.erb
Normal file
|
@ -0,0 +1,59 @@
|
|||
<%= content_for :header_nav do %>
|
||||
<%= render "imports/nav", import: @import %>
|
||||
<% end %>
|
||||
|
||||
<%= content_for :previous_path, import_configuration_path(@import) %>
|
||||
|
||||
<div class="space-y-4 mx-auto max-w-screen-lg">
|
||||
<div class="text-center space-y-2 max-w-[400px] mx-auto mb-4">
|
||||
<h2 class="text-3xl text-gray-900 font-medium"><%= t(".title") %></h2>
|
||||
<p class="text-gray-500 text-sm"><%= t(".description") %></p>
|
||||
</div>
|
||||
|
||||
<% if @import.cleaned? %>
|
||||
<div class="bg-white border border-alpha-black-100 rounded-lg p-3 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= lucide_icon "check-circle", class: "w-4 h-4 text-green-500" %>
|
||||
<p class="text-green-500">Your data has been cleaned</p>
|
||||
</div>
|
||||
|
||||
<%= link_to "Next step", import_confirm_path(@import), class: "btn btn--primary" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="bg-white border border-alpha-black-100 rounded-lg p-3 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= lucide_icon "alert-triangle", class: "w-4 h-4 text-red-500" %>
|
||||
<p class="text-red-500">You have errors in your data</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<div class="bg-gray-50 rounded-lg inline-flex p-1 space-x-2 text-sm text-gray-900 font-medium">
|
||||
<%= link_to "All rows", import_clean_path(@import, per_page: params[:per_page], view: "all"), class: "p-2 rounded-lg #{params[:view] != 'errors' ? 'bg-white' : ''}" %>
|
||||
<%= link_to "Error rows", import_clean_path(@import, per_page: params[:per_page], view: "errors"), class: "p-2 rounded-lg #{params[:view] == 'errors' ? 'bg-white' : ''}" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="pb-12">
|
||||
<div class="bg-gray-25 rounded-xl p-1 mb-6">
|
||||
<div style="grid-template-columns: repeat(<%= @import.column_keys.count %>, 1fr)" class="grid items-center uppercase text-xs font-medium text-gray-500 py-3">
|
||||
<% @import.column_keys.each do |key| %>
|
||||
<div class="px-5"><%= import_col_label(key) %></div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="bg-white border border-alpha-black-200 rounded-xl shadow-xs divide-y divide-alpha-black-200">
|
||||
<% @rows.each do |row| %>
|
||||
<%= render "import/rows/form", row: row %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fixed bottom-0 left-1/2 -translate-x-1/2 w-full p-12">
|
||||
<div class="border border-alpha-black-100 rounded-lg p-3 max-w-screen-sm mx-auto bg-white shadow-xs">
|
||||
<%= render "application/pagination", pagy: @pagy %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
9
app/views/import/configurations/_account_import.html.erb
Normal file
9
app/views/import/configurations/_account_import.html.erb
Normal file
|
@ -0,0 +1,9 @@
|
|||
<%# locals: (import:) %>
|
||||
|
||||
<%= styled_form_with model: @import, url: import_configuration_path(@import), scope: :import, method: :patch, class: "space-y-2" do |form| %>
|
||||
<%= form.select :entity_type_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Entity Type" } %>
|
||||
<%= form.select :name_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Name (optional)" } %>
|
||||
<%= form.select :amount_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Balance" } %>
|
||||
|
||||
<%= form.submit "Apply configuration", class: "w-full btn btn--primary", disabled: import.complete? %>
|
||||
<% end %>
|
25
app/views/import/configurations/_mint_import.html.erb
Normal file
25
app/views/import/configurations/_mint_import.html.erb
Normal file
|
@ -0,0 +1,25 @@
|
|||
<%# locals: (import:) %>
|
||||
|
||||
<div class="flex items-center justify-between border border-alpha-black-200 rounded-lg bg-green-500/5 p-5 gap-4">
|
||||
<%= lucide_icon("check-circle", class: "w-5 h-5 shrink-0 text-green-500") %>
|
||||
<p class="text-sm text-gray-900 italic">We have pre-configured your Mint import for you. Please proceed to the next step.</p>
|
||||
</div>
|
||||
|
||||
<%= styled_form_with model: @import, url: import_configuration_path(@import), scope: :import, method: :patch, class: "space-y-2" do |form| %>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= form.select :date_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Date" }, required: true, disabled: import.complete? %>
|
||||
<%= form.select :date_format, [["DD-MM-YYYY", "%d-%m-%Y"], ["MM-DD-YYYY", "%m-%d-%Y"], ["YYYY-MM-DD", "%Y-%m-%d"], ["DD/MM/YYYY", "%d/%m/%Y"], ["YYYY/MM/DD", "%Y/%m/%d"], ["MM/DD/YYYY", "%m/%d/%Y"]], { label: true }, required: true, disabled: import.complete? %>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<%= form.select :amount_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Amount" }, required: true, disabled: import.complete? %>
|
||||
<%= form.select :signage_convention, [["Incomes are negative", "inflows_negative"], ["Incomes are positive", "inflows_positive"]], { label: true }, disabled: import.complete? %>
|
||||
</div>
|
||||
|
||||
<%= form.select :account_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Account (optional)" }, disabled: import.complete? %>
|
||||
<%= form.select :name_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Name (optional)" }, disabled: import.complete? %>
|
||||
<%= form.select :category_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Category (optional)" }, disabled: import.complete? %>
|
||||
<%= form.select :tags_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Tags (optional)" }, disabled: import.complete? %>
|
||||
|
||||
<%= form.submit "Apply configuration", class: "w-full btn btn--primary", disabled: import.complete? %>
|
||||
<% end %>
|
20
app/views/import/configurations/_trade_import.html.erb
Normal file
20
app/views/import/configurations/_trade_import.html.erb
Normal file
|
@ -0,0 +1,20 @@
|
|||
<%# locals: (import:) %>
|
||||
|
||||
<%= styled_form_with model: @import, url: import_configuration_path(@import), scope: :import, method: :patch, class: "space-y-2" do |form| %>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= form.select :date_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Date" }, required: true %>
|
||||
<%= form.select :date_format, [["DD-MM-YYYY", "%d-%m-%Y"], ["MM-DD-YYYY", "%m-%d-%Y"], ["YYYY-MM-DD", "%Y-%m-%d"], ["DD/MM/YYYY", "%d/%m/%Y"], ["YYYY/MM/DD", "%Y/%m/%d"], ["MM/DD/YYYY", "%m/%d/%Y"]], label: true, required: true %>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<%= form.select :qty_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Quantity" } %>
|
||||
<%= form.select :signage_convention, [["Buys are positive qty", "inflows_positive"], ["Buys are negative qty", "inflows_negative"]], label: true %>
|
||||
</div>
|
||||
|
||||
<%= form.select :ticker_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Ticker" } %>
|
||||
<%= form.select :price_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Price" } %>
|
||||
<%= form.select :account_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Account (optional)" } %>
|
||||
<%= form.select :name_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Name (optional)" } %>
|
||||
|
||||
<%= form.submit "Apply configuration", class: "w-full btn btn--primary", disabled: import.complete? %>
|
||||
<% end %>
|
21
app/views/import/configurations/_transaction_import.html.erb
Normal file
21
app/views/import/configurations/_transaction_import.html.erb
Normal file
|
@ -0,0 +1,21 @@
|
|||
<%# locals: (import:) %>
|
||||
|
||||
<%= styled_form_with model: @import, url: import_configuration_path(@import), scope: :import, method: :patch, class: "space-y-2" do |form| %>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= form.select :date_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Date" }, required: true %>
|
||||
<%= form.select :date_format, [["DD-MM-YYYY", "%d-%m-%Y"], ["MM-DD-YYYY", "%m-%d-%Y"], ["YYYY-MM-DD", "%Y-%m-%d"], ["DD/MM/YYYY", "%d/%m/%Y"], ["YYYY/MM/DD", "%Y/%m/%d"], ["MM/DD/YYYY", "%m/%d/%Y"]], label: true, required: true %>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<%= form.select :amount_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Amount" }, required: true %>
|
||||
<%= form.select :signage_convention, [["Incomes are positive", "inflows_positive"], ["Incomes are negative", "inflows_negative"]], label: true %>
|
||||
</div>
|
||||
|
||||
<%= form.select :account_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Account (optional)" } %>
|
||||
<%= form.select :name_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Name (optional)" } %>
|
||||
<%= form.select :category_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Category (optional)" } %>
|
||||
<%= form.select :tags_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Tags (optional)" } %>
|
||||
<%= form.select :notes_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Notes (optional)" } %>
|
||||
|
||||
<%= form.submit "Apply configuration", class: "w-full btn btn--primary", disabled: import.complete? %>
|
||||
<% end %>
|
22
app/views/import/configurations/show.html.erb
Normal file
22
app/views/import/configurations/show.html.erb
Normal file
|
@ -0,0 +1,22 @@
|
|||
<%= content_for :header_nav do %>
|
||||
<%= render "imports/nav", import: @import %>
|
||||
<% end %>
|
||||
|
||||
<%= content_for :previous_path, import_upload_path(@import) %>
|
||||
|
||||
<div>
|
||||
<div class="space-y-4">
|
||||
<div class="text-center space-y-2">
|
||||
<h1 class="text-3xl text-gray-900 font-medium"><%= t(".title") %></h1>
|
||||
<p class="text-gray-500 text-sm"><%= t(".description") %></p>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-lg">
|
||||
<%= render partial: permitted_import_configuration_path(@import), locals: { import: @import } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-screen-lg my-12">
|
||||
<%= render "imports/table", headers: @import.csv_headers, rows: @import.csv_sample, caption: "Sample data from your uploaded CSV" %>
|
||||
</div>
|
||||
</div>
|
29
app/views/import/confirms/_mappings.html.erb
Normal file
29
app/views/import/confirms/_mappings.html.erb
Normal file
|
@ -0,0 +1,29 @@
|
|||
<%# locals: (import:, mapping_class:, step_idx:) %>
|
||||
|
||||
<% mappings = mapping_class.for_import(import) %>
|
||||
<% is_last_step = step_idx == import.mapping_steps.count - 1 %>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="bg-gray-25 rounded-xl p-1 space-y-1 w-[650px]">
|
||||
<div class="grid grid-cols-3 gap-2 text-xs font-medium text-gray-500 uppercase px-5 py-3">
|
||||
<p>CSV <%= mapping_label(mapping_class) %></p>
|
||||
<p>Maybe <%= mapping_label(mapping_class) %></p>
|
||||
<p class="justify-self-end">Rows</p>
|
||||
</div>
|
||||
|
||||
<div class="border border-alpha-black-25 rounded-md shadow-xs divide-y divide-alpha-black-100 text-sm">
|
||||
<% mappings.sort_by(&:key).each do |mapping| %>
|
||||
<div class="px-5 py-3 bg-white first:rounded-tl-xl first:rounded-tr-xl last:rounded-bl-xl last:rounded-br-xl">
|
||||
<%= render partial: "import/mappings/form", locals: { mapping: mapping } %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<%= link_to is_last_step ? import_path(import) : url_for(step: step_idx + 2), class: "btn btn--primary w-36 flex items-center justify-between gap-2" do %>
|
||||
<span>Next</span>
|
||||
<%= lucide_icon "arrow-right", class: "w-5 h-5" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
33
app/views/import/confirms/show.html.erb
Normal file
33
app/views/import/confirms/show.html.erb
Normal file
|
@ -0,0 +1,33 @@
|
|||
<%= content_for :header_nav do %>
|
||||
<%= render "imports/nav", import: @import %>
|
||||
<% end %>
|
||||
|
||||
<%= content_for :previous_path, import_clean_path(@import) %>
|
||||
|
||||
<% step_idx = (params[:step] || "1").to_i - 1 %>
|
||||
<% step_mapping_class = @import.mapping_steps[step_idx] %>
|
||||
|
||||
<div class="space-y-12 mx-auto max-w-md mb-6">
|
||||
<div class="flex justify-center items-center gap-2">
|
||||
<% @import.mapping_steps.each_with_index do |step_mapping_class, idx| %>
|
||||
<% is_active = step_idx == idx %>
|
||||
|
||||
<%= link_to url_for(step: idx + 1), class: "w-5 h-[3px] #{is_active ? 'bg-gray-900' : 'bg-gray-100'} rounded-xl hover:bg-gray-300 transition-colors duration-200" do %>
|
||||
<span class="sr-only">Step <%= idx + 1 %></span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="text-center space-y-2">
|
||||
<h1 class="text-3xl text-gray-900 font-medium">
|
||||
<%= t(".#{step_mapping_class.name.demodulize.underscore}_title", import_type: @import.type.underscore.humanize) %>
|
||||
</h1>
|
||||
<p class="text-gray-500 text-sm">
|
||||
<%= t(".#{step_mapping_class.name.demodulize.underscore}_description", import_type: @import.type.underscore.humanize) %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-screen-md mx-auto flex justify-center">
|
||||
<%= render partial: "import/confirms/mappings", locals: { import: @import, mapping_class: step_mapping_class, step_idx: step_idx } %>
|
||||
</div>
|
29
app/views/import/mappings/_form.html.erb
Normal file
29
app/views/import/mappings/_form.html.erb
Normal file
|
@ -0,0 +1,29 @@
|
|||
<%# locals: (mapping:) %>
|
||||
|
||||
<%= styled_form_with model: mapping,
|
||||
scope: :import_mapping,
|
||||
url: import_mapping_path(mapping.import, mapping),
|
||||
class: "grid grid-cols-3 gap-2 items-center",
|
||||
data: { controller: "auto-submit-form" },
|
||||
html: { id: dom_id(mapping, :form) } do |form| %>
|
||||
<span><%= mapping.key.blank? ? "(unassigned)" : mapping.key %></span>
|
||||
|
||||
<% if mapping.mappable_class.present? %>
|
||||
<%= form.hidden_field :mappable_type, value: mapping.mappable_class, id: dom_id(mapping, :mappable_type) %>
|
||||
<%= form.select :mappable_id,
|
||||
mapping.selectable_values,
|
||||
{ container_class: mapping.invalid? ? "border-red-500" : nil, include_blank: mapping.requires_selection? ? "Select an option" : "Leave unassigned", selected: mapping.create_when_empty? ? mapping.class::CREATE_NEW_KEY : mapping.mappable_id },
|
||||
"data-auto-submit-form-target": "auto", "data-autosubmit-trigger-event": "change", disabled: mapping.import.complete?, id: dom_id(mapping, :mappable_id) %>
|
||||
<% else %>
|
||||
<%= form.select :value, mapping.selectable_values,
|
||||
{ container_class: mapping.invalid? ? "border-red-500" : nil, include_blank: mapping.requires_selection? ? "Select an option" : "Leave unassigned" },
|
||||
"data-auto-submit-form-target": "auto", "data-autosubmit-trigger-event": "change", disabled: mapping.import.complete?, id: dom_id(mapping, :value) %>
|
||||
<% end %>
|
||||
|
||||
<%= form.hidden_field :key, value: mapping.key, id: dom_id(mapping, :key) %>
|
||||
<%= form.hidden_field :type, value: mapping.type, id: dom_id(mapping, :type) %>
|
||||
|
||||
<span class="justify-self-end">
|
||||
<%= mapping.values_count %>
|
||||
</span>
|
||||
<% end %>
|
27
app/views/import/rows/_form.html.erb
Normal file
27
app/views/import/rows/_form.html.erb
Normal file
|
@ -0,0 +1,27 @@
|
|||
<%# locals: (row:) %>
|
||||
|
||||
<div style="grid-template-columns: repeat(<%= row.import.column_keys.count %>, 1fr)" class="first:rounded-tl-lg first:rounded-tr-lg last:rounded-bl-lg last:rounded-br-lg grid divide-x divide-alpha-black-200 group">
|
||||
<% row.import.column_keys.each_with_index do |key, idx| %>
|
||||
<%= turbo_frame_tag dom_id(row, key), title: row.valid? ? nil : row.errors.full_messages.join(", ") do %>
|
||||
<%= form_with(
|
||||
model: [row.import, row],
|
||||
scope: :import_row,
|
||||
url: import_row_path(row.import, row),
|
||||
method: :patch,
|
||||
data: {
|
||||
controller: "auto-submit-form",
|
||||
auto_submit_form_trigger_event_value: "blur"
|
||||
}
|
||||
) do |form| %>
|
||||
<%= form.text_field key,
|
||||
"data-auto-submit-form-target": "auto",
|
||||
class: [
|
||||
cell_class(row, key),
|
||||
idx == 0 ? "group-first:rounded-tl-lg group-last:rounded-bl-lg" : "",
|
||||
idx == row.import.column_keys.count - 1 ? "group-first:rounded-tr-lg group-last:rounded-br-lg" : "",
|
||||
],
|
||||
disabled: row.import.complete? %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
1
app/views/import/rows/show.html.erb
Normal file
1
app/views/import/rows/show.html.erb
Normal file
|
@ -0,0 +1 @@
|
|||
<%= render "import/rows/form", row: @row %>
|
69
app/views/import/uploads/show.html.erb
Normal file
69
app/views/import/uploads/show.html.erb
Normal file
|
@ -0,0 +1,69 @@
|
|||
<%= content_for :header_nav do %>
|
||||
<%= render "imports/nav", import: @import %>
|
||||
<% end %>
|
||||
|
||||
<%= content_for :previous_path, imports_path %>
|
||||
|
||||
<div class="space-y-12">
|
||||
<div class="space-y-4 mx-auto max-w-md">
|
||||
<div class="text-center space-y-2">
|
||||
<h1 class="text-3xl text-gray-900 font-medium"><%= t(".title") %></h1>
|
||||
<p class="text-gray-500 text-sm"><%= t(".description") %></p>
|
||||
</div>
|
||||
|
||||
<div data-controller="tabs" data-tabs-active-class="bg-white" data-tabs-default-tab-value="csv-paste-tab">
|
||||
<div class="flex justify-center mb-4">
|
||||
<div class="bg-gray-50 rounded-lg inline-flex p-1 space-x-2 text-sm text-gray-900 font-medium">
|
||||
<button type="button" data-id="csv-paste-tab" class="p-2 rounded-lg" data-tabs-target="btn" data-action="click->tabs#select">Copy & Paste</button>
|
||||
<button type="button" data-id="csv-upload-tab" class="p-2 rounded-lg" data-tabs-target="btn" data-action="click->tabs#select">Upload CSV</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-tabs-target="tab" id="csv-paste-tab">
|
||||
<%= styled_form_with model: @import, scope: :import, url: import_upload_path(@import), multipart: true, class: "space-y-2" do |form| %>
|
||||
<%= form.select :col_sep, [["Comma (,)", ","], ["Semicolon (;)", ";"]], label: true %>
|
||||
<%= form.text_area :raw_file_str,
|
||||
rows: 10,
|
||||
required: true,
|
||||
placeholder: "Paste your CSV file contents here",
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
|
||||
<%= form.submit "Upload CSV", disabled: @import.complete? %>
|
||||
<% end %>
|
||||
|
||||
</div>
|
||||
|
||||
<div data-tabs-target="tab" id="csv-upload-tab" class="hidden">
|
||||
<%= styled_form_with model: @import, scope: :import, url: import_upload_path(@import), multipart: true, class: "space-y-2" do |form| %>
|
||||
<%= form.select :col_sep, [["Comma (,)", ","], ["Semicolon (;)", ";"]], label: true %>
|
||||
|
||||
<label for="import_csv_file" class="flex flex-col items-center justify-center w-full h-56 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50">
|
||||
<div class="flex flex-col items-center justify-center pt-5 pb-6">
|
||||
<%= form.file_field :csv_file, class: "ml-32", "data-auto-submit-form-target": "auto" %>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<%= form.submit "Upload CSV", disabled: @import.complete? %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-alpha-black-25 rounded-xl p-1 mt-5 mx-auto max-w-screen-xl">
|
||||
<div class="text-gray-500 p-2 mb-2">
|
||||
<div class="flex gap-2 mb-2">
|
||||
<%= lucide_icon("info", class: "w-5 h-5 shrink-0") %>
|
||||
<p class="text-sm"><%= t(".instructions_1") %></p>
|
||||
|
||||
</div>
|
||||
|
||||
<ul class="list-disc list-inside text-sm pl-8">
|
||||
<li><%= t(".instructions_2") %></li>
|
||||
<li><%= t(".instructions_3") %></li>
|
||||
<li><%= t(".instructions_4") %></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<%= render partial: "imports/table", locals: { headers: @import.csv_template.headers, rows: @import.csv_template } %>
|
||||
</div>
|
||||
</div>
|
|
@ -1,25 +0,0 @@
|
|||
<%= styled_form_with model: @import, url: load_import_path(@import), class: "space-y-4" do |form| %>
|
||||
<%= form.text_area :raw_file_str,
|
||||
rows: 10,
|
||||
required: true,
|
||||
placeholder: "Paste your CSV file contents here",
|
||||
class: "rounded-md w-full border text-sm border-alpha-black-100 bg-white placeholder:text-gray-400" %>
|
||||
|
||||
<%= form.submit t(".next"), class: "px-4 py-2 mb-4 block w-full rounded-lg bg-gray-900 text-white text-sm font-medium", data: { turbo_confirm: (@import.raw_file_str? ? { title: t(".confirm_title"), body: t(".confirm_body"), accept: t(".confirm_accept") } : nil) } %>
|
||||
<% end %>
|
||||
|
||||
<div class="bg-alpha-black-25 rounded-xl p-1 mt-5">
|
||||
<div class="text-gray-500 p-2 mb-2">
|
||||
<div class="flex gap-2 mb-2">
|
||||
<%= lucide_icon("info", class: "w-5 h-5 shrink-0") %>
|
||||
<p class="text-sm"><%= t(".instructions") %></p>
|
||||
</div>
|
||||
|
||||
<ul class="list-disc text-sm pl-10">
|
||||
<li><%= t(".requirement1") %></li>
|
||||
<li><%= t(".requirement2") %></li>
|
||||
<li><%= t(".requirement3") %></li>
|
||||
</ul>
|
||||
</div>
|
||||
<%= render partial: "imports/sample_table" %>
|
||||
</div>
|
|
@ -1,39 +0,0 @@
|
|||
<%= styled_form_with model: @import, url: upload_import_path(@import), class: "dropzone space-y-4", data: { controller: "import-upload", import_upload_accepted_types_value: ["text/csv", "application/csv", ".csv"], import_upload_extension_value: "csv", import_upload_unacceptable_type_label_value: t(".allowed_filetypes") }, method: :patch, multipart: true do |form| %>
|
||||
<div class="flex items-center justify-center w-full">
|
||||
<label for="import_raw_file_str" class="raw-file-drop-box flex flex-col items-center justify-center w-full h-64 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50" data-action="dragover->import-upload#dragover dragleave->import-upload#dragleave drop->import-upload#drop">
|
||||
<div class="flex flex-col items-center justify-center pt-5 pb-6">
|
||||
<%= lucide_icon "plus", class: "w-5 h-5 text-gray-500" %>
|
||||
<%= form.file_field :raw_file_str, class: "hidden", direct_upload: false, accept: "text/csv,.csv,application/csv", data: { import_upload_target: "input", action: "change->import-upload#addFile" } %>
|
||||
<p class="mb-2 text-sm text-gray-500 mt-3">Drag and drop your csv file here or <span class="text-black">click to browse</span></p>
|
||||
<p class="text-xs text-gray-500">CSV (Max. 5MB)</p>
|
||||
<div class="csv-preview" data-import-upload-target="preview"></div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<%= form.submit t(".next"), class: "px-4 py-2 mb-4 block w-full rounded-lg bg-alpha-black-25 text-gray text-sm font-medium", data: { import_upload_target: "submit", turbo_confirm: (@import.raw_file_str? ? { title: t(".confirm_title"), body: t(".confirm_body"), accept: t(".confirm_accept") } : nil) } %>
|
||||
<% end %>
|
||||
|
||||
<div id="template-preview" class="hidden">
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<%= lucide_icon "file-text", class: "w-10 h-10 pt-2 text-black" %>
|
||||
<div class="flex flex-row items-center justify-center gap-0.5">
|
||||
<div><span data-import-upload-target="filename"></span></div>
|
||||
<div><span data-import-upload-target="filesize" class="font-semibold"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-alpha-black-25 rounded-xl p-1 mt-5">
|
||||
<div class="text-gray-500 p-2 mb-2">
|
||||
<div class="flex gap-2 mb-2">
|
||||
<%= lucide_icon("info", class: "w-5 h-5 shrink-0") %>
|
||||
<p class="text-sm">
|
||||
<%= t(".instructions") %>
|
||||
<span class="text-black underline">
|
||||
<%= link_to "download this template", "/transactions.csv", download: "" %>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<%= render partial: "imports/sample_table" %>
|
||||
</div>
|
|
@ -1,7 +1,7 @@
|
|||
<div class="flex justify-center items-center py-20">
|
||||
<div class="text-center flex flex-col items-center max-w-[300px]">
|
||||
<div class="text-center flex flex-col items-center max-w-[300px] gap-4">
|
||||
<p class="text-gray-900 mb-1 font-medium text-sm"><%= t(".message") %></p>
|
||||
<%= link_to new_import_path(enable_type_selector: true), class: "w-fit flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-2 pr-3", data: { turbo_frame: "modal" } do %>
|
||||
<%= link_to new_import_path(enable_type_selector: true), class: "btn btn--primary flex items-center gap-2", data: { turbo_frame: "modal" } do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||
<span><%= t(".new") %></span>
|
||||
<% end %>
|
||||
|
|
18
app/views/imports/_failure.html.erb
Normal file
18
app/views/imports/_failure.html.erb
Normal file
|
@ -0,0 +1,18 @@
|
|||
<%# locals: (import:) %>
|
||||
|
||||
<div class="h-full flex flex-col justify-center items-center">
|
||||
<div class="space-y-6 max-w-sm">
|
||||
<div class="mx-auto bg-red-500/5 h-8 w-8 rounded-full flex items-center justify-center">
|
||||
<%= lucide_icon "alert-octagon", class: "w-5 h-5 text-red-500" %>
|
||||
</div>
|
||||
|
||||
<div class="text-center space-y-2">
|
||||
<h1 class="font-medium text-gray-900 text-center text-3xl">Import failed</h1>
|
||||
<p class="text-sm text-gray-500">Please check that your file format, for any errors and that all required fields are filled, then come back and try again.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= button_to "Try again", publish_import_path(import), class: "btn btn--primary text-center w-full" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,8 +0,0 @@
|
|||
<%= styled_form_with model: @import do |form| %>
|
||||
<div class="mb-4 space-y-3">
|
||||
<%= form.collection_select :account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".select_account"), label: t(".account"), required: true } %>
|
||||
<%= form.collection_select :col_sep, Import::Csv::COL_SEP_LIST, :to_s, -> { t(".col_sep_char.#{_1.ord}") }, { prompt: t(".select_col_sep"), label: t(".col_sep"), required: true } %>
|
||||
</div>
|
||||
|
||||
<%= form.submit t(".next"), class: "px-4 py-2 block w-full rounded-lg bg-gray-900 text-white text-sm font-medium cursor-pointer hover:bg-gray-700" %>
|
||||
<% end %>
|
|
@ -1,51 +1,39 @@
|
|||
<div id="<%= dom_id import %>" class="flex items-center justify-between mx-4 py-5 border-b last:border-b-0 border-alpha-black-50">
|
||||
<div>
|
||||
<div id="<%= dom_id import %>" class="flex items-center justify-between mx-4 py-4 border-b last:border-b-0 border-alpha-black-50">
|
||||
|
||||
<div class="flex items-center gap-1 mb-1">
|
||||
<p class="text-sm text-gray-900">
|
||||
<%= t(".label", account: import.account.name) %>
|
||||
</p>
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<%= link_to import_path(import), class: "text-sm text-gray-900 hover:underline" do %>
|
||||
<%= t(".label", type: import.type.titleize, datetime: import.updated_at.strftime("%b %-d, %Y at %l:%M %p")) %>
|
||||
<% end %>
|
||||
|
||||
<% if import.pending? %>
|
||||
<span class="px-1 py text-xs rounded-full bg-gray-500/5 text-gray-500 border border-alpha-black-50">
|
||||
<%= t(".in_progress") %>
|
||||
</span>
|
||||
<% elsif import.importing? %>
|
||||
<span class="px-1 py text-xs animate-pulse rounded-full bg-orange-500/5 text-orange-500 border border-alpha-black-50">
|
||||
<%= t(".uploading") %>
|
||||
</span>
|
||||
<% elsif import.failed? %>
|
||||
<span class="px-1 py text-xs rounded-full bg-red-500/5 text-red-500 border border-alpha-black-50">
|
||||
<%= t(".failed") %>
|
||||
</span>
|
||||
<% elsif import.complete? %>
|
||||
<span class="px-1 py text-xs rounded-full bg-green-500/5 text-green-500 border border-alpha-black-50">
|
||||
<%= t(".complete") %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if import.complete? %>
|
||||
<p class="text-xs text-gray-500"><%= t(".completed_on", datetime: import.updated_at.strftime("%Y-%m-%d")) %></p>
|
||||
<% else %>
|
||||
<p class="text-xs text-gray-500"><%= t(".started_on", datetime: import.created_at.strftime("%Y-%m-%d")) %></p>
|
||||
<% if import.pending? %>
|
||||
<span class="px-1 py text-xs rounded-full bg-gray-500/5 text-gray-500 border border-alpha-black-50">
|
||||
<%= t(".in_progress") %>
|
||||
</span>
|
||||
<% elsif import.importing? %>
|
||||
<span class="px-1 py text-xs animate-pulse rounded-full bg-orange-500/5 text-orange-500 border border-alpha-black-50">
|
||||
<%= t(".uploading") %>
|
||||
</span>
|
||||
<% elsif import.failed? %>
|
||||
<span class="px-1 py text-xs rounded-full bg-red-500/5 text-red-500 border border-alpha-black-50">
|
||||
<%= t(".failed") %>
|
||||
</span>
|
||||
<% elsif import.complete? %>
|
||||
<span class="px-1 py text-xs rounded-full bg-green-500/5 text-green-500 border border-alpha-black-50">
|
||||
<%= t(".complete") %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if import.complete? %>
|
||||
<div class="w-7 h-7 bg-green-500/5 flex items-center justify-center rounded-full">
|
||||
<%= lucide_icon("check", class: "text-green-500 w-4 h-4") %>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= contextual_menu do %>
|
||||
<div class="w-48 p-1 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
||||
<%= link_to edit_import_path(import),
|
||||
<%= contextual_menu do %>
|
||||
<div class="w-48 p-1 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
||||
<%= link_to import_path(import),
|
||||
class: "block w-full py-2 px-3 space-x-2 text-gray-900 hover:bg-gray-50 flex items-center rounded-lg" do %>
|
||||
<%= lucide_icon "pencil-line", class: "w-5 h-5 text-gray-500" %>
|
||||
<%= lucide_icon "eye", class: "w-5 h-5 text-gray-500" %>
|
||||
|
||||
<span><%= t(".edit") %></span>
|
||||
<% end %>
|
||||
<span><%= t(".view") %></span>
|
||||
<% end %>
|
||||
|
||||
<% unless import.complete? %>
|
||||
<%= button_to import_path(import),
|
||||
method: :delete,
|
||||
class: "block w-full py-2 px-3 space-x-2 text-red-600 hover:bg-red-50 flex items-center rounded-lg",
|
||||
|
@ -54,8 +42,7 @@
|
|||
|
||||
<span><%= t(".delete") %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
</div>
|
||||
|
|
19
app/views/imports/_importing.html.erb
Normal file
19
app/views/imports/_importing.html.erb
Normal file
|
@ -0,0 +1,19 @@
|
|||
<%# locals: (import:) %>
|
||||
|
||||
<div class="h-full flex flex-col justify-center items-center">
|
||||
<div class="space-y-6 max-w-sm">
|
||||
<div class="mx-auto bg-gray-500/5 h-8 w-8 rounded-full flex items-center justify-center">
|
||||
<%= lucide_icon "loader", class: "animate-pulse w-5 h-5 text-gray-500" %>
|
||||
</div>
|
||||
|
||||
<div class="text-center space-y-2">
|
||||
<h1 class="font-medium text-gray-900 text-center text-3xl">Import in progress</h1>
|
||||
<p class="text-sm text-gray-500">Your import is in progress. Check the imports menu for status updates or click 'Check Status' to refresh the page for updates. Feel free to continue using the app.</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<%= link_to "Check status", import_path(import), class: "block btn btn--primary text-center w-full" %>
|
||||
<%= link_to "Back to dashboard", root_path, class: "block btn btn--secondary text-center w-full" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
40
app/views/imports/_nav.html.erb
Normal file
40
app/views/imports/_nav.html.erb
Normal file
|
@ -0,0 +1,40 @@
|
|||
<%# locals: (import:) %>
|
||||
|
||||
<% steps = [
|
||||
{ name: "Upload", path: import_upload_path(import), is_complete: import.uploaded?, step_number: 1 },
|
||||
{ name: "Configure", path: import_configuration_path(import), is_complete: import.configured?, step_number: 2 },
|
||||
{ name: "Clean", path: import_clean_path(import), is_complete: import.cleaned?, step_number: 3 },
|
||||
{ name: "Map", path: import_confirm_path(import), is_complete: import.publishable?, step_number: 4 },
|
||||
{ name: "Confirm", path: import_path(import), is_complete: import.complete?, step_number: 5 }
|
||||
] %>
|
||||
|
||||
<ul class="flex items-center gap-2">
|
||||
<% steps.each_with_index do |step, idx| %>
|
||||
<li class="flex items-center gap-2 group">
|
||||
<% is_current = request.path == step[:path] %>
|
||||
|
||||
<% text_class = if is_current
|
||||
"text-gray-900"
|
||||
else
|
||||
step[:is_complete] ? "text-green-600" : "text-gray-500"
|
||||
end %>
|
||||
<% step_class = if is_current
|
||||
"bg-gray-900 text-white"
|
||||
else
|
||||
step[:is_complete] ? "bg-green-600/10 border-alpha-black-25" : "bg-gray-50"
|
||||
end %>
|
||||
|
||||
<%= link_to step[:path], class: "flex items-center gap-3" do %>
|
||||
<div class="flex items-center gap-2 text-sm font-medium <%= text_class %>">
|
||||
<span class="<%= step_class %> w-7 h-7 rounded-full shrink-0 inline-flex items-center justify-center border border-transparent">
|
||||
<%= step[:is_complete] && !is_current ? lucide_icon("check", class: "w-4 h-4") : idx + 1 %>
|
||||
</span>
|
||||
|
||||
<span><%= step[:name] %></span>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="h-px bg-alpha-black-200 w-12 group-last:hidden"></div>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
|
@ -1,18 +0,0 @@
|
|||
<% is_current = request.path == step[:path] %>
|
||||
<% text_class = if is_current
|
||||
"text-gray-900"
|
||||
else
|
||||
step[:complete] ? "text-green-600" : "text-gray-500"
|
||||
end %>
|
||||
<% step_class = if is_current
|
||||
"bg-gray-900 text-white"
|
||||
else
|
||||
step[:complete] ? "bg-green-600/10 border-alpha-black-25" : "bg-gray-50"
|
||||
end %>
|
||||
|
||||
<div class="flex items-center gap-2 text-sm font-medium <%= text_class %>">
|
||||
<span class="<%= step_class %> w-7 h-7 rounded-full shrink-0 inline-flex items-center justify-center border border-transparent">
|
||||
<%= step[:complete] && !is_current ? lucide_icon("check", class: "w-4 h-4") : step_idx + 1 %>
|
||||
</span>
|
||||
<span><%= step[:name] %></span>
|
||||
</div>
|
39
app/views/imports/_ready.html.erb
Normal file
39
app/views/imports/_ready.html.erb
Normal file
|
@ -0,0 +1,39 @@
|
|||
<%# locals: (import:) %>
|
||||
|
||||
<div class="text-center space-y-2 mb-4 mx-auto max-w-md">
|
||||
<h1 class="text-3xl text-gray-900 font-medium"><%= t(".title") %></h1>
|
||||
<p class="text-gray-500 text-sm"><%= t(".description") %></p>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-screen-sm space-y-4">
|
||||
<div class="bg-gray-25 rounded-xl p-1 space-y-1">
|
||||
<div class="flex justify-between items-center text-xs font-medium text-gray-500 uppercase px-5 py-3">
|
||||
<p>item</p>
|
||||
<p class="justify-self-end">count</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white border border-alpha-black-25 rounded-lg shadow-xs text-sm">
|
||||
<% import.dry_run.each do |key, count| %>
|
||||
<% resource = dry_run_resource(key) %>
|
||||
|
||||
<div class="flex items-center justify-between gap-2 bg-white px-5 py-3 rounded-tl-lg rounded-tr-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="<%= resource.bg_class %> w-8 h-8 rounded-full flex justify-center items-center">
|
||||
<%= lucide_icon resource.icon, class: "#{resource.text_class} w-5 h-5 shrink-0" %>
|
||||
</div>
|
||||
|
||||
<p><%= resource.label %></p>
|
||||
</div>
|
||||
|
||||
<p class="justify-self-end"><%= count %></p>
|
||||
</div>
|
||||
|
||||
<% if key != import.dry_run.keys.last %>
|
||||
<div class="h-px bg-alpha-black-50 ml-14 mr-5"></div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= button_to "Publish import", publish_import_path(import), class: "btn btn--primary w-full" %>
|
||||
</div>
|
|
@ -1,26 +0,0 @@
|
|||
<!--TODO: Once we have more styled tables for reference, refactor and DRY this up -->
|
||||
<div class="grid grid-cols-5 border border-alpha-black-200 rounded-md shadow-xs text-sm bg-white">
|
||||
<div class="bg-gray-25 px-3 py-2.5 border-b border-b-alpha-black-200 font-medium rounded-tl-md">date</div>
|
||||
<div class="bg-gray-25 px-3 py-2.5 border-b border-b-alpha-black-200 font-medium">name</div>
|
||||
<div class="bg-gray-25 px-3 py-2.5 border-b border-b-alpha-black-200 font-medium">category</div>
|
||||
<div class="bg-gray-25 px-3 py-2.5 border-b border-b-alpha-black-200 font-medium">tags</div>
|
||||
<div class="bg-gray-25 px-3 py-2.5 border-b border-b-alpha-black-200 font-medium rounded-tr-md">amount</div>
|
||||
|
||||
<div class="px-3 py-2.5 border-b border-b-alpha-black-200">2024-01-01</div>
|
||||
<div class="px-3 py-2.5 border-b border-b-alpha-black-200">Amazon</div>
|
||||
<div class="px-3 py-2.5 border-b border-b-alpha-black-200">Shopping</div>
|
||||
<div class="px-3 py-2.5 border-b border-b-alpha-black-200">Tag1|Tag2</div>
|
||||
<div class="px-3 py-2.5 border-b border-b-alpha-black-200">-24.99</div>
|
||||
|
||||
<div class="px-3 py-2.5 border-b border-b-alpha-black-200">2024-03-01</div>
|
||||
<div class="px-3 py-2.5 border-b border-b-alpha-black-200">Spotify</div>
|
||||
<div class="px-3 py-2.5 border-b border-b-alpha-black-200"></div>
|
||||
<div class="px-3 py-2.5 border-b border-b-alpha-black-200"></div>
|
||||
<div class="px-3 py-2.5 border-b border-b-alpha-black-200">-16.32</div>
|
||||
|
||||
<div class="px-3 py-2.5 border-b border-b-alpha-black-200 rounded-bl-md">2023-01-06</div>
|
||||
<div class="px-3 py-2.5 border-b border-b-alpha-black-200">Acme</div>
|
||||
<div class="px-3 py-2.5 border-b border-b-alpha-black-200">Income</div>
|
||||
<div class="px-3 py-2.5 border-b border-b-alpha-black-200">Tag3</div>
|
||||
<div class="px-3 py-2.5 border-b border-b-alpha-black-200 rounded-br-md">151.22</div>
|
||||
</div>
|
18
app/views/imports/_success.html.erb
Normal file
18
app/views/imports/_success.html.erb
Normal file
|
@ -0,0 +1,18 @@
|
|||
<%# locals: (import:) %>
|
||||
|
||||
<div class="h-full flex flex-col justify-center items-center">
|
||||
<div class="space-y-6 max-w-sm">
|
||||
<div class="mx-auto bg-green-500/5 h-8 w-8 rounded-full flex items-center justify-center">
|
||||
<%= lucide_icon "check", class: "w-5 h-5 text-green-500" %>
|
||||
</div>
|
||||
|
||||
<div class="text-center space-y-2">
|
||||
<h1 class="font-medium text-gray-900 text-center text-3xl">Import successful</h1>
|
||||
<p class="text-sm text-gray-500">Your imported data has been successfully added to the app and is now ready for use.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= link_to "Back to dashboard", root_path, class: "block btn btn--primary text-center w-full" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
37
app/views/imports/_table.html.erb
Normal file
37
app/views/imports/_table.html.erb
Normal file
|
@ -0,0 +1,37 @@
|
|||
<%# locals: (headers: [], rows: [], caption: nil) %>
|
||||
<div class="overflow-x-auto">
|
||||
<div class="border border-alpha-black-200 rounded-md shadow-xs text-sm bg-white w-full">
|
||||
<div class="grid border-b border-b-alpha-black-200" style="grid-template-columns: repeat(<%= headers.length %>, minmax(0, 1fr))">
|
||||
<% headers.each_with_index do |header, index| %>
|
||||
<div class="
|
||||
bg-gray-25 px-3 py-2.5 font-medium whitespace-nowrap overflow-x-auto
|
||||
first:rounded-tl-md last:rounded-tr-md
|
||||
<%= "border-r border-r-alpha-black-200" unless index == headers.length - 1 %>
|
||||
">
|
||||
<%= header %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% rows.each_with_index do |row, row_index| %>
|
||||
<div class="grid <%= "border-b border-b-alpha-black-200" if row_index < rows.length - 1 || caption %>" style="grid-template-columns: repeat(<%= headers.length %>, minmax(0, 1fr))">
|
||||
<% row.each_with_index do |(header, value), col_index| %>
|
||||
<div class="
|
||||
px-3 py-2.5 whitespace-nowrap overflow-x-auto flex items-start
|
||||
<%= "border-r border-r-alpha-black-200" unless col_index == row.length - 1 %>
|
||||
<%= "rounded-bl-md" if !caption && row_index == rows.length - 1 && col_index == 0 %>
|
||||
<%= "rounded-br-md" if !caption && row_index == rows.length - 1 && col_index == row.length - 1 %>
|
||||
">
|
||||
<%= value %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if caption %>
|
||||
<div class="px-3 py-2.5 text-center text-xs text-gray-900 rounded-b-md italic bg-gray-25 overflow-x-auto">
|
||||
<%= caption %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
|
@ -1,65 +0,0 @@
|
|||
<div class="p-4 space-y-4 max-w-[420px]">
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="font-medium text-gray-900"><%= t(".import_transactions") %></h2>
|
||||
<button data-action="modal#close">
|
||||
<%= lucide_icon("x", class: "w-5 h-5 text-gray-900") %>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="text-gray-500 text-sm"><%= t(".description") %></p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl bg-gray-25 p-1">
|
||||
<h3 class="uppercase text-gray-500 text-xs font-medium px-3 py-1.5"><%= t(".sources") %></h3>
|
||||
<ul class="bg-white border border-alpha-black-25 rounded-lg shadow-xs">
|
||||
<li>
|
||||
<% if Current.family.imports.pending.present? %>
|
||||
<%= link_to edit_import_path(Current.family.imports.pending.ordered.first), class: "flex items-center gap-3 p-4 group cursor-pointer", data: { turbo: false } do %>
|
||||
<div class="bg-orange-500/5 rounded-md w-8 h-8 flex items-center justify-center">
|
||||
<%= lucide_icon("loader", class: "w-5 h-5 text-orange-500") %>
|
||||
</div>
|
||||
<span class="text-sm text-gray-900 group-hover:text-gray-700">
|
||||
<%= t(".resume_latest_import") %>
|
||||
</span>
|
||||
<%= lucide_icon("chevron-right", class: "w-5 h-5 text-gray-500 ml-auto") %>
|
||||
<% end %>
|
||||
|
||||
<div class="pl-14 pr-3">
|
||||
<div class="h-px bg-alpha-black-50"></div>
|
||||
</div>
|
||||
</li>
|
||||
<% end %>
|
||||
<li>
|
||||
<%= link_to new_import_path, class: "flex items-center gap-3 p-4 group cursor-pointer", data: { turbo: false } do %>
|
||||
<div class="bg-indigo-500/5 rounded-md w-8 h-8 flex items-center justify-center">
|
||||
<%= lucide_icon("file-spreadsheet", class: "w-5 h-5 text-indigo-500") %>
|
||||
</div>
|
||||
<span class="text-sm text-gray-900 group-hover:text-gray-700">
|
||||
<%= t(".import_from_csv") %>
|
||||
</span>
|
||||
<%= lucide_icon("chevron-right", class: "w-5 h-5 text-gray-500 ml-auto") %>
|
||||
<% end %>
|
||||
|
||||
<div class="pl-14 pr-3">
|
||||
<div class="h-px bg-alpha-black-50"></div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="flex items-center gap-3 p-4 group cursor-not-allowed">
|
||||
<%= image_tag("mint-logo.jpeg", alt: "Mint logo", class: "w-8 h-8 rounded-md") %>
|
||||
<span class="text-sm text-gray-400">
|
||||
<%= t(".import_from_mint") %>
|
||||
</span>
|
||||
<span class="bg-indigo-500/5 rounded-full px-1.5 py-0.5 border border-alpha-black-25 uppercase text-xs font-medium text-indigo-500"><%= t(".soon") %></span>
|
||||
<%= lucide_icon("chevron-right", class: "w-5 h-5 text-gray-300 ml-auto") %>
|
||||
</div>
|
||||
|
||||
<div class="pl-14 pr-3">
|
||||
<div class="h-px bg-alpha-black-50"></div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
|
@ -1,48 +0,0 @@
|
|||
<%= content_for :return_to_path, return_to_path(params, imports_path) %>
|
||||
|
||||
<div class="mx-auto max-w-screen-md w-full py-24">
|
||||
<h1 class="sr-only"><%= t(".clean_import") %></h1>
|
||||
|
||||
<div class="text-center space-y-2 max-w-[400px] mx-auto mb-8">
|
||||
<h2 class="text-3xl text-gray-900 font-medium"><%= t(".clean_and_edit") %></h2>
|
||||
<p class="text-gray-500 text-sm"><%= t(".clean_description") %></p>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-25 rounded-xl p-1 mb-6">
|
||||
<div
|
||||
class="grid items-center uppercase text-xs font-medium text-gray-500 py-3"
|
||||
style="grid-template-columns: repeat(<%= @import.expected_fields.size %>, 1fr);">
|
||||
<% @import.expected_fields.each do |field| %>
|
||||
<div class="px-5"><%= field.label %></div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="bg-white border border-alpha-black-200 rounded-xl shadow-xs divide-y divide-alpha-black-200">
|
||||
<% @import.csv.table.each_with_index do |row, row_index| %>
|
||||
<div
|
||||
class="grid divide-x divide-alpha-black-200"
|
||||
style="grid-template-columns: repeat(<%= @import.expected_fields.size %>, 1fr);">
|
||||
<% row.fields.each_with_index do |value, col_index| %>
|
||||
<%= form_with model: @import,
|
||||
url: clean_import_url(@import),
|
||||
method: :patch,
|
||||
data: { turbo: false, controller: "auto-submit-form", "auto-submit-form-trigger-event-value" => "blur" } do |form| %>
|
||||
<%= form.fields_for :csv_update do |ff| %>
|
||||
<%= ff.hidden_field :row_idx, value: row_index %>
|
||||
<%= ff.hidden_field :col_idx, value: col_index %>
|
||||
<%= ff.text_field :value, value: value,
|
||||
id: "cell-#{row_index}-#{col_index}",
|
||||
class: "#{@import.csv.cell_valid?(row_index, col_index) ? "focus:border-transparent border-transparent" : "border-red-500"} border px-4 py-3.5 text-sm w-full bg-transparent focus:ring-gray-900 focus:ring-2 focus-visible:outline-none #{table_corner_class(row_index, col_index, @import.csv.table, row.fields)}",
|
||||
data: { "auto-submit-form-target" => "auto" } %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if @import.csv.valid? %>
|
||||
<%= link_to "Next", confirm_import_path(@import), class: "px-4 py-2 block w-60 text-center mx-auto rounded-lg bg-gray-900 text-white text-sm font-medium hover:bg-gray-700", data: { turbo: false } %>
|
||||
<% end %>
|
||||
</div>
|
|
@ -1,23 +0,0 @@
|
|||
<%= content_for :return_to_path, return_to_path(params, imports_path) %>
|
||||
|
||||
<div class="mx-auto max-w-[400px] w-full py-24 space-y-4">
|
||||
<h1 class="sr-only"><%= t(".configure_title") %></h1>
|
||||
|
||||
<div class="text-center space-y-2">
|
||||
<h2 class="text-3xl text-gray-900 font-medium"><%= t(".configure_subtitle") %></h2>
|
||||
<p class="text-gray-500 text-sm"><%= t(".configure_description") %></p>
|
||||
</div>
|
||||
|
||||
<%= styled_form_with model: @import, url: configure_import_path(@import), class: "space-y-4" do |form| %>
|
||||
<%= form.fields_for :column_mappings do |mappings| %>
|
||||
<% @import.expected_fields.each do |field| %>
|
||||
<%= mappings.select field.key,
|
||||
options_for_select(@import.available_headers, @import.get_selected_header_for_field(field)),
|
||||
label: field.label,
|
||||
include_blank: field.optional? ? t(".optional") : false %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= form.submit t(".next"), class: "px-4 py-2 block w-full rounded-lg bg-gray-900 text-white text-sm font-medium cursor-pointer hover:bg-gray-700", data: { turbo_confirm: (@import.column_mappings? ? { title: t(".confirm_title"), body: t(".confirm_body"), accept: t(".confirm_accept") } : nil) } %>
|
||||
<% end %>
|
||||
</div>
|
|
@ -1,18 +0,0 @@
|
|||
<%= content_for :return_to_path, return_to_path(params, imports_path) %>
|
||||
|
||||
<div class="mx-auto max-w-screen-md w-full py-24">
|
||||
<h1 class="sr-only"><%= t(".confirm_title") %></h1>
|
||||
|
||||
<div class="text-center space-y-2 max-w-[400px] mx-auto mb-8">
|
||||
<h2 class="text-3xl text-gray-900 font-medium"><%= t(".confirm_subtitle") %></h2>
|
||||
<p class="text-gray-500 text-sm"><%= t(".confirm_description") %></p>
|
||||
</div>
|
||||
|
||||
<div class="mb-8 space-y-4">
|
||||
<%= entries_by_date(@import.dry_run, selectable: false) do |entries| %>
|
||||
<%= render entries, show_tags: true, selectable: false, editable: false %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= button_to "Import " + @import.csv.table.size.to_s + " transactions", confirm_import_path(@import), method: :patch, class: "px-4 py-2 block w-60 text-center mx-auto rounded-lg bg-gray-900 text-white text-sm font-medium hover:bg-gray-700", data: { turbo: false } %>
|
||||
</div>
|
|
@ -1,10 +0,0 @@
|
|||
<%= content_for :return_to_path, return_to_path(params, imports_path) %>
|
||||
|
||||
<div class="mx-auto max-w-[400px] w-full py-56">
|
||||
<h1 class="sr-only"><%= t(".edit_title") %></h1>
|
||||
<div class="space-y-2 mb-6 text-center">
|
||||
<p class="text-3xl font-medium text-gray-900"><%= t(".header_text") %></p>
|
||||
<p class="text-gray-500 text-sm"><%= t(".description_text") %></p>
|
||||
</div>
|
||||
<%= render "form", import: @import %>
|
||||
</div>
|
|
@ -6,7 +6,7 @@
|
|||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-xl font-medium text-gray-900"><%= t(".title") %></h1>
|
||||
|
||||
<%= link_to new_import_path(enable_type_selector: true), class: "rounded-lg bg-gray-900 text-white flex items-center gap-1 justify-center hover:bg-gray-700 px-3 py-2", data: { turbo_frame: :modal } do %>
|
||||
<%= link_to new_import_path, class: "btn btn--primary flex items-center gap-2", data: { turbo_frame: :modal } do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||
<span><%= t(".new") %></span>
|
||||
<% end %>
|
||||
|
@ -19,7 +19,7 @@
|
|||
<h2 class="uppercase px-4 py-2 text-gray-500 text-xs"><%= t(".imports") %> · <%= @imports.size %></h2>
|
||||
|
||||
<div class="border border-alpha-gray-100 rounded-lg bg-white shadow-xs">
|
||||
<%= render @imports.ordered %>
|
||||
<%= render partial: "imports/import", collection: @imports.ordered %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
<%= content_for :return_to_path, return_to_path(params, imports_path) %>
|
||||
|
||||
<div class="mx-auto max-w-[550px] w-full py-24 space-y-4">
|
||||
<h1 class="sr-only"><%= t(".load_title") %></h1>
|
||||
|
||||
<div class="text-center space-y-2">
|
||||
<h2 class="text-3xl text-gray-900 font-medium"><%= t(".subtitle") %></h2>
|
||||
<p class="text-gray-500 text-sm"><%= t(".description") %></p>
|
||||
</div>
|
||||
|
||||
<div data-controller="tabs" data-tabs-active-class="bg-white" data-tabs-default-tab-value="csv-upload-tab">
|
||||
<div class="flex justify-center mb-4">
|
||||
<div class="bg-gray-50 rounded-lg inline-flex p-1 space-x-2 text-sm text-gray-900 font-medium">
|
||||
<button data-id="csv-upload-tab" class="p-2 rounded-lg" data-tabs-target="btn" data-action="click->tabs#select">Upload CSV</button>
|
||||
<button data-id="csv-paste-tab" class="p-2 rounded-lg" data-tabs-target="btn" data-action="click->tabs#select">Copy & Paste</button>
|
||||
</div>
|
||||
</div>
|
||||
<div data-tabs-target="tab" id="csv-upload-tab">
|
||||
<%= render partial: "imports/csv_upload", locals: { import: @import } %>
|
||||
</div>
|
||||
<div data-tabs-target="tab" id="csv-paste-tab" class="hidden">
|
||||
<%= render partial: "imports/csv_paste", locals: { import: @import } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,16 +1,108 @@
|
|||
<%= content_for :return_to_path, return_to_path(params, imports_path) %>
|
||||
<%= modal do %>
|
||||
<div class="p-4 space-y-4 max-w-[420px]">
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="font-medium text-gray-900"><%= t(".title") %></h2>
|
||||
<button data-action="modal#close" tabindex="-1">
|
||||
<%= lucide_icon("x", class: "w-5 h-5 text-gray-900") %>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<% if params[:enable_type_selector].present? %>
|
||||
<%= modal do %>
|
||||
<%= render "type_selector" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<p class="text-gray-500 text-sm"><%= t(".description") %></p>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-[400px] w-full py-56">
|
||||
<h1 class="sr-only">New import</h1>
|
||||
<div class="space-y-2 mb-6 text-center">
|
||||
<p class="text-3xl font-medium text-gray-900"><%= t(".header_text") %></p>
|
||||
<p class="text-gray-500 text-sm"><%= t(".description_text") %></p>
|
||||
<div class="rounded-xl bg-gray-25 p-1">
|
||||
<h3 class="uppercase text-gray-500 text-xs font-medium px-3 py-1.5"><%= t(".sources") %></h3>
|
||||
<ul class="bg-white border border-alpha-black-25 rounded-lg shadow-xs">
|
||||
<li>
|
||||
<% if @pending_import.present? %>
|
||||
<%= link_to import_path(@pending_import), class: "flex items-center justify-between p-4 group cursor-pointer", data: { turbo: false } do %>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="bg-orange-500/5 rounded-md w-8 h-8 flex items-center justify-center">
|
||||
<%= lucide_icon("loader", class: "w-5 h-5 text-orange-500") %>
|
||||
</div>
|
||||
<span class="text-sm text-gray-900 group-hover:text-gray-700">
|
||||
<%= t(".resume") %>
|
||||
</span>
|
||||
</div>
|
||||
<%= lucide_icon("chevron-right", class: "w-5 h-5 text-gray-500") %>
|
||||
<% end %>
|
||||
|
||||
<div class="pl-14 pr-3">
|
||||
<div class="h-px bg-alpha-black-50"></div>
|
||||
</div>
|
||||
</li>
|
||||
<% end %>
|
||||
<li>
|
||||
<%= button_to imports_path(import: { type: "TransactionImport" }), class: "flex items-center justify-between p-4 group cursor-pointer w-full", data: { turbo: false } do %>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="bg-indigo-500/5 rounded-md w-8 h-8 flex items-center justify-center">
|
||||
<%= lucide_icon("file-spreadsheet", class: "w-5 h-5 text-indigo-500") %>
|
||||
</div>
|
||||
<span class="text-sm text-gray-900 group-hover:text-gray-700">
|
||||
<%= t(".import_transactions") %>
|
||||
</span>
|
||||
</div>
|
||||
<%= lucide_icon("chevron-right", class: "w-5 h-5 text-gray-500") %>
|
||||
<% end %>
|
||||
|
||||
<div class="pl-14 pr-3">
|
||||
<div class="h-px bg-alpha-black-50"></div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<%= button_to imports_path(import: { type: "TradeImport" }), class: "flex items-center justify-between p-4 group cursor-pointer w-full", data: { turbo: false } do %>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="bg-yellow-500/5 rounded-md w-8 h-8 flex items-center justify-center">
|
||||
<%= lucide_icon("square-percent", class: "w-5 h-5 text-yellow-500") %>
|
||||
</div>
|
||||
<span class="text-sm text-gray-900 group-hover:text-gray-700">
|
||||
<%= t(".import_portfolio") %>
|
||||
</span>
|
||||
</div>
|
||||
<%= lucide_icon("chevron-right", class: "w-5 h-5 text-gray-500") %>
|
||||
<% end %>
|
||||
|
||||
<div class="pl-14 pr-3">
|
||||
<div class="h-px bg-alpha-black-50"></div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<%= button_to imports_path(import: { type: "AccountImport" }), class: "flex items-center justify-between p-4 group cursor-pointer w-full", data: { turbo: false } do %>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="bg-violet-500/5 rounded-md w-8 h-8 flex items-center justify-center">
|
||||
<%= lucide_icon("building", class: "w-5 h-5 text-violet-500") %>
|
||||
</div>
|
||||
<span class="text-sm text-gray-900 group-hover:text-gray-700">
|
||||
<%= t(".import_accounts") %>
|
||||
</span>
|
||||
</div>
|
||||
<%= lucide_icon("chevron-right", class: "w-5 h-5 text-gray-500") %>
|
||||
<% end %>
|
||||
|
||||
<div class="pl-14 pr-3">
|
||||
<div class="h-px bg-alpha-black-50"></div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<%= button_to imports_path(import: { type: "MintImport" }), class: "flex items-center justify-between p-4 group w-full", data: { turbo: false } do %>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= image_tag("mint-logo.jpeg", alt: "Mint logo", class: "w-8 h-8 rounded-md") %>
|
||||
<span class="text-sm text-gray-900">
|
||||
<%= t(".import_mint") %>
|
||||
</span>
|
||||
</div>
|
||||
<%= lucide_icon("chevron-right", class: "w-5 h-5 text-gray-500") %>
|
||||
<% end %>
|
||||
|
||||
<div class="pl-14 pr-3">
|
||||
<div class="h-px bg-alpha-black-50"></div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<%= render "form", import: @import %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
<div class="mx-auto md:w-2/3 w-full flex">
|
||||
<div class="mx-auto">
|
||||
<% if notice.present? %>
|
||||
<p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block" id="notice"><%= notice %></p>
|
||||
<% end %>
|
||||
<%= content_for :header_nav do %>
|
||||
<%= render "imports/nav", import: @import %>
|
||||
<% end %>
|
||||
|
||||
<%= render @import %>
|
||||
<%= content_for :previous_path, import_confirm_path(@import) %>
|
||||
|
||||
<%= link_to "Edit this import", edit_import_path(@import), class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
|
||||
<div class="inline-block ml-2">
|
||||
<%= button_to "Destroy this import", import_path(@import), method: :delete, class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 font-medium" %>
|
||||
</div>
|
||||
<%= link_to "Back to imports", imports_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
|
||||
</div>
|
||||
</div>
|
||||
<% if @import.importing? %>
|
||||
<%= render "imports/importing", import: @import %>
|
||||
<% elsif @import.complete? %>
|
||||
<%= render "imports/success", import: @import %>
|
||||
<% elsif @import.failed? %>
|
||||
<%= render "imports/failure", import: @import %>
|
||||
<% else %>
|
||||
<%= render "imports/ready", import: @import %>
|
||||
<% end %>
|
||||
|
|
|
@ -1,34 +1,23 @@
|
|||
<%= content_for :content do %>
|
||||
<div class="flex items-center justify-between p-8">
|
||||
<%= link_to root_path do %>
|
||||
<%= image_tag "logo.svg", alt: "Maybe", class: "h-[22px]" %>
|
||||
<% end %>
|
||||
<nav>
|
||||
<div>
|
||||
<ul class="flex items-center gap-2">
|
||||
<% nav_steps(@import).each_with_index do |step, idx| %>
|
||||
<li class="group flex items-center gap-2">
|
||||
<% if step[:path].present? %>
|
||||
<%= link_to step[:path], class: "flex items-center gap-3" do %>
|
||||
<%= render partial: "nav_step", locals: { step: step, step_idx: idx } %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= render partial: "nav_step", locals: { step: step, step_idx: idx } %>
|
||||
<% end %>
|
||||
<% if idx < nav_steps.size %>
|
||||
<div class="h-px bg-alpha-black-200 w-12 group-last:hidden"></div>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<%= link_to content_for(:return_to_path) do %>
|
||||
<%= lucide_icon("x", class: "text-gray-500 w-8 h-8 hover:bg-gray-100 rounded-full p-2") %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex flex-col h-dvh">
|
||||
<header class="flex items-center justify-between p-8">
|
||||
<%= link_to content_for(:previous_path) || imports_path do %>
|
||||
<%= lucide_icon "arrow-left", class: "w-5 h-5 text-gray-500" %>
|
||||
<% end %>
|
||||
|
||||
<%= yield %>
|
||||
<nav>
|
||||
<%= yield :header_nav %>
|
||||
</nav>
|
||||
|
||||
<%= link_to imports_path do %>
|
||||
<%= lucide_icon "x", class: "text-gray-500 w-5 h-5" %>
|
||||
<% end %>
|
||||
</header>
|
||||
|
||||
<main class="flex-grow px-8 pt-12 pb-32 overflow-y-auto">
|
||||
<%= yield %>
|
||||
</main>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= render template: "layouts/application" %>
|
||||
|
|
|
@ -8,10 +8,18 @@
|
|||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= link_to new_account_path, class: "flex items-center gap-1 btn btn--primary", data: { turbo_frame: "modal" } do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||
<span><%= t(".new") %></span>
|
||||
<% end %>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= contextual_menu do %>
|
||||
<div class="w-48 p-1 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
||||
<%= contextual_menu_modal_action_item t(".import"), new_import_path, icon: "hard-drive-upload" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= link_to new_account_path, class: "flex items-center gap-1 btn btn--primary", data: { turbo_frame: "modal" } do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||
<span><%= t(".new") %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<% if @accounts.empty? %>
|
||||
|
|
|
@ -29,6 +29,10 @@
|
|||
<li>
|
||||
<%= sidebar_link_to t(".accounts_label"), accounts_path, icon: "layers" %>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<%= sidebar_link_to t(".imports_label"), imports_path, icon: "download" %>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
|
@ -47,9 +51,6 @@
|
|||
<li>
|
||||
<%= sidebar_link_to t(".merchants_label"), merchants_path, icon: "store" %>
|
||||
</li>
|
||||
<li>
|
||||
<%= sidebar_link_to t(".imports_label"), imports_path, icon: "download" %>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue