mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-03 04:25:21 +02:00
CSV Transaction Imports (#708)
Introduces a basic CSV import module for bulk-importing account transactions. Changes include: - User can load a CSV - User can configure the column mappings for a CSV - Imported CSV shows invalid cells - User can clean up their data directly in the UI - User can see a preview of the import rows and confirm import - Layout refactor + Import nav stepper - System test stability improvements
This commit is contained in:
parent
3d9ff3ad2a
commit
45ae4a9737
71 changed files with 1657 additions and 117 deletions
9
app/views/imports/_empty.html.erb
Normal file
9
app/views/imports/_empty.html.erb
Normal file
|
@ -0,0 +1,9 @@
|
|||
<div class="flex justify-center items-center py-20">
|
||||
<div class="text-center flex flex-col items-center max-w-[300px]">
|
||||
<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 %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||
<span><%= t(".new") %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
7
app/views/imports/_form.html.erb
Normal file
7
app/views/imports/_form.html.erb
Normal file
|
@ -0,0 +1,7 @@
|
|||
<%= form_with model: @import do |form| %>
|
||||
<div class="mb-4">
|
||||
<%= form.collection_select :account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".select_account"), label: t(".account"), 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" %>
|
||||
<% end %>
|
61
app/views/imports/_import.html.erb
Normal file
61
app/views/imports/_import.html.erb
Normal file
|
@ -0,0 +1,61 @@
|
|||
<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 class="flex items-center gap-1 mb-1">
|
||||
<p class="text-sm text-gray-900">
|
||||
<%= t(".label", account: import.account.name) %>
|
||||
</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? %>
|
||||
<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>
|
||||
<% 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),
|
||||
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" %>
|
||||
|
||||
<span><%= t(".edit") %></span>
|
||||
<% end %>
|
||||
|
||||
<%= 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",
|
||||
data: { turbo_confirm: true } do %>
|
||||
<%= lucide_icon "trash-2", class: "w-5 h-5" %>
|
||||
|
||||
<span><%= t(".delete") %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
</div>
|
18
app/views/imports/_nav_step.html.erb
Normal file
18
app/views/imports/_nav_step.html.erb
Normal file
|
@ -0,0 +1,18 @@
|
|||
<% 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>
|
22
app/views/imports/_sample_table.html.erb
Normal file
22
app/views/imports/_sample_table.html.erb
Normal file
|
@ -0,0 +1,22 @@
|
|||
<!--TODO: Once we have more styled tables for reference, refactor and DRY this up -->
|
||||
<div class="grid grid-cols-4 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 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">-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">-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 rounded-br-md">151.22</div>
|
||||
</div>
|
90
app/views/imports/_type_selector.html.erb
Normal file
90
app/views/imports/_type_selector.html.erb
Normal file
|
@ -0,0 +1,90 @@
|
|||
<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-900 group-hover:text-gray-700">
|
||||
<%= 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>
|
||||
<li>
|
||||
<div class="flex items-center gap-3 p-4 group cursor-not-allowed">
|
||||
<%= image_tag("empower-logo.jpeg", alt: "Mint logo", class: "w-8 h-8 border border-alpha-black-100 rounded-md") %>
|
||||
<span class="text-sm text-gray-900 group-hover:text-gray-700">
|
||||
<%= t(".import_from_empower") %>
|
||||
</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>
|
||||
<li>
|
||||
<div class="flex items-center gap-3 p-4 group cursor-not-allowed">
|
||||
<%= image_tag("apple-logo.png", alt: "Mint logo", class: "w-8 h-8 rounded-md") %>
|
||||
<span class="text-sm text-gray-900 group-hover:text-gray-700">
|
||||
<%= t(".import_from_apple") %>
|
||||
</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>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
49
app/views/imports/clean.html.erb
Normal file
49
app/views/imports/clean.html.erb
Normal file
|
@ -0,0 +1,49 @@
|
|||
<%= 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,
|
||||
builder: ActionView::Helpers::FormBuilder,
|
||||
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", data: { turbo: false } %>
|
||||
<% end %>
|
||||
</div>
|
24
app/views/imports/configure.html.erb
Normal file
24
app/views/imports/configure.html.erb
Normal file
|
@ -0,0 +1,24 @@
|
|||
<%= 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>
|
||||
|
||||
<%= form_with model: @import, url: configure_import_path(@import) do |form| %>
|
||||
<div class="mb-4 space-y-4">
|
||||
<%= 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 %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= form.submit t(".next"), class: "px-4 py-2 block w-full rounded-lg bg-gray-900 text-white text-sm font-medium", data: { turbo_confirm: (@import.column_mappings? ? { title: t(".confirm_title"), body: t(".confirm_body"), accept: t(".confirm_accept") } : nil) } %>
|
||||
<% end %>
|
||||
</div>
|
16
app/views/imports/confirm.html.erb
Normal file
16
app/views/imports/confirm.html.erb
Normal file
|
@ -0,0 +1,16 @@
|
|||
<%= 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">
|
||||
<%= render partial: "imports/transactions/transaction_group", collection: @import.dry_run.group_by(&:date) %>
|
||||
</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", data: { turbo: false } %>
|
||||
</div>
|
10
app/views/imports/edit.html.erb
Normal file
10
app/views/imports/edit.html.erb
Normal file
|
@ -0,0 +1,10 @@
|
|||
<%= 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>
|
30
app/views/imports/index.html.erb
Normal file
30
app/views/imports/index.html.erb
Normal file
|
@ -0,0 +1,30 @@
|
|||
<% content_for :sidebar do %>
|
||||
<%= render "settings/nav" %>
|
||||
<% end %>
|
||||
|
||||
<div class="space-y-4">
|
||||
<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: "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 %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||
<span><%= t(".new") %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="bg-white shadow-xs border border-alpha-black-25 rounded-xl p-4">
|
||||
<% if @imports.empty? %>
|
||||
<%= render partial: "imports/empty" %>
|
||||
<% else %>
|
||||
<div class="rounded-xl bg-gray-25 p-1">
|
||||
<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 %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4">
|
||||
<%= previous_setting("Rules", transaction_rules_path) %>
|
||||
<%= next_setting("What's new", changelog_path) %>
|
||||
</div>
|
||||
</div>
|
39
app/views/imports/load.html.erb
Normal file
39
app/views/imports/load.html.erb
Normal file
|
@ -0,0 +1,39 @@
|
|||
<%= content_for :return_to_path, return_to_path(params, imports_path) %>
|
||||
|
||||
<div class="mx-auto max-w-[450px] 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>
|
||||
|
||||
<%= form_with model: @import, url: load_import_path(@import) do |form| %>
|
||||
<div>
|
||||
<%= form.text_area :raw_csv_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 p-4" %>
|
||||
</div>
|
||||
|
||||
<%= form.submit t(".next"), class: "px-4 py-2 block w-full rounded-lg bg-gray-900 text-white text-sm font-medium", data: { turbo_confirm: (@import.raw_csv_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">
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<%= render partial: "imports/sample_table" %>
|
||||
|
||||
</div>
|
||||
</div>
|
16
app/views/imports/new.html.erb
Normal file
16
app/views/imports/new.html.erb
Normal file
|
@ -0,0 +1,16 @@
|
|||
<%= content_for :return_to_path, return_to_path(params, imports_path) %>
|
||||
|
||||
<% if params[:enable_type_selector].present? %>
|
||||
<%= modal do %>
|
||||
<%= render "type_selector" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<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>
|
||||
<%= render "form", import: @import %>
|
||||
</div>
|
15
app/views/imports/show.html.erb
Normal file
15
app/views/imports/show.html.erb
Normal file
|
@ -0,0 +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 %>
|
||||
|
||||
<%= render @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>
|
12
app/views/imports/transactions/_transaction.html.erb
Normal file
12
app/views/imports/transactions/_transaction.html.erb
Normal file
|
@ -0,0 +1,12 @@
|
|||
<%# locals: (transaction:) %>
|
||||
<div class="text-gray-900 flex items-center gap-6 py-4 text-sm font-medium px-4">
|
||||
<%= render partial: "transactions/transaction_name", locals: { name: transaction.name } %>
|
||||
|
||||
<div class="w-48">
|
||||
<%= render partial: "transactions/categories/badge", locals: { category: transaction.category } %>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto">
|
||||
<%= content_tag :p, format_money(-transaction.amount), class: ["whitespace-nowrap", BigDecimal(transaction.amount).negative? ? "text-green-600" : "text-red-600"] %>
|
||||
</div>
|
||||
</div>
|
13
app/views/imports/transactions/_transaction_group.html.erb
Normal file
13
app/views/imports/transactions/_transaction_group.html.erb
Normal file
|
@ -0,0 +1,13 @@
|
|||
<%# locals: (transaction_group:) %>
|
||||
<% date = transaction_group[0] %>
|
||||
<% transactions = transaction_group[1] %>
|
||||
|
||||
<div class="bg-gray-25 rounded-xl p-1 w-full">
|
||||
<div class="py-2 px-4 flex items-center justify-between font-medium text-xs text-gray-500">
|
||||
<h4><%= date.strftime("%b %d, %Y") %> · <%= transactions.size %></h4>
|
||||
<span><%= format_money -transactions.sum { |t| t.amount } %></span>
|
||||
</div>
|
||||
<div class="bg-white shadow-xs rounded-md border border-alpha-black-25 divide-y divide-alpha-black-50">
|
||||
<%= render partial: "imports/transactions/transaction", collection: transactions %>
|
||||
</div>
|
||||
</div>
|
Loading…
Add table
Add a link
Reference in a new issue