mirror of
https://github.com/we-promise/sure.git
synced 2026-04-16 10:34:09 +00:00
* Feat: Add QIF (Quicken Interchange Format) import functionality - Add the ability to import QIF files for users coming from Quicken - Includes categories and tags - Comprehensive tests for QifImport, including parsing, row generation, and import functionality. - Ensure handling of hierarchical categories (ex "Home:Home Improvement" is imported as Parent:Child) * Fix QIF import issues raised in code review - Fix two-digit year windowing in QIF date parser (e.g. '99 → 1999, not 2099) - Fix ArgumentError from invalid `undef: :raise` encoding option - Nil-safe `leaf_category_name` with blank guard and `.to_s` coercion - Memoize `qif_account_type` to avoid re-parsing the full QIF file - Add strong parameters (`selection_params`) to QifCategorySelectionsController - Wrap all mutations in DB transactions in uploads and category-selections controllers - Skip unchanged tag rows (only write rows where tags actually differ) - Replace hardcoded strings with i18n keys across QIF views and nav - Fix potentially colliding checkbox/label IDs in category selection view - Improve keyboard accessibility: use semantic `<label>` for file picker area Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix QIF import test count and Brakeman mass assignment warning - Update ImportsControllerTest to expect 4 disabled import options (was 3), accounting for the new QIF import type added in this branch - Remove :account_id from upload_params permit list; it was never accessed through strong params (always via params.dig with Current.family scope), so this resolves the Brakeman high-confidence mass assignment warning Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix: QIF import security, safety, and i18n issues raised in code review - Added french, spanish and german translations for newly added i18n keys - Replace params.dig(:import, :account_id) with a proper strong-params accessor (import_account_id) in UploadsController to satisfy Rails parameter filtering requirements - Guard ImportsController#show against QIF imports reaching the publish screen before a file has been uploaded, preventing an unrescued error on publish - Gate the QIF "Clean" nav step link on import.uploaded? to prevent routing to CleansController with an unconfigured import (which would raise "Unknown import type: QifImport" via ImportsHelper) - Replace hard-coded "txn" pluralize calls in the category/tag selection view with t(".txn_count") and add pluralization keys to the locale file - Localize all hard-coded strings in the QIF upload section of uploads/show.html.erb and add corresponding en.yml keys - Convert the CSV upload drop zone from a clickable <div> (JS-only) to a semantic <label> element, making it keyboard-accessible without JavaScript * Fix: missing translations keys * Add icon mapping and random color assignment to new categories * fix a lint issue * Add a warning about splits and some plumbing for future support. Updated locales. --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
127 lines
6.3 KiB
Plaintext
127 lines
6.3 KiB
Plaintext
<%= content_for :header_nav do %>
|
||
<%= render "imports/nav", import: @import %>
|
||
<% end %>
|
||
|
||
<%= content_for :previous_path, imports_path %>
|
||
|
||
<% if @import.is_a?(QifImport) %>
|
||
<%# ── QIF upload – fixed format, account required ── %>
|
||
<div class="space-y-4 mx-auto max-w-md">
|
||
<div class="text-center space-y-2">
|
||
<h1 class="text-3xl text-primary font-medium"><%= t(".qif_title") %></h1>
|
||
<p class="text-secondary text-sm"><%= t(".qif_description") %></p>
|
||
</div>
|
||
|
||
<%= styled_form_with model: @import, scope: :import, url: import_upload_path(@import), multipart: true, class: "space-y-4" do |form| %>
|
||
<%= form.select :account_id,
|
||
@import.family.accounts.visible.pluck(:name, :id),
|
||
{ label: t(".qif_account_label"), include_blank: t(".qif_account_placeholder"), selected: @import.account_id },
|
||
required: true %>
|
||
|
||
<label for="import_import_file" class="flex flex-col items-center justify-center w-full h-64 border border-secondary border-dashed rounded-xl cursor-pointer" data-controller="file-upload" data-file-upload-target="uploadArea">
|
||
<div class="flex flex-col items-center justify-center pt-5 pb-6">
|
||
<div data-file-upload-target="uploadText" class="flex flex-col items-center">
|
||
<%= icon("plus", size: "lg", class: "mb-4 mx-auto") %>
|
||
<p class="mb-2 text-md text-gray text-center">
|
||
<span class="font-medium text-primary"><%= t(".browse") %></span> <%= t(".qif_file_prompt") %>
|
||
</p>
|
||
<p class="text-xs text-secondary"><%= t(".qif_file_hint") %></p>
|
||
</div>
|
||
|
||
<div class="flex flex-col gap-4 items-center hidden mb-2" data-file-upload-target="fileName">
|
||
<span class="text-primary">
|
||
<%= icon("file-text", size: "lg", color: "current") %>
|
||
</span>
|
||
<p class="text-md font-medium text-primary"></p>
|
||
</div>
|
||
|
||
<%= form.file_field :import_file,
|
||
accept: ".qif",
|
||
id: "import_import_file",
|
||
class: "hidden",
|
||
"data-file-upload-target": "input" %>
|
||
</div>
|
||
</label>
|
||
|
||
<%= form.submit t(".qif_submit"), disabled: @import.complete? %>
|
||
<% end %>
|
||
</div>
|
||
|
||
<% else %>
|
||
<%# ── Standard CSV upload ── %>
|
||
<div class="space-y-4" data-controller="drag-and-drop-import">
|
||
<!-- Overlay -->
|
||
<%= render "imports/drag_drop_overlay", title: "Drop CSV to upload", subtitle: "Your file will be uploaded automatically" %>
|
||
|
||
<div class="space-y-4 mx-auto max-w-md">
|
||
<div class="text-center space-y-2">
|
||
<h1 class="text-3xl text-primary font-medium"><%= t(".title") %></h1>
|
||
<p class="text-secondary text-sm"><%= t(".description") %></p>
|
||
</div>
|
||
|
||
<%= render DS::Tabs.new(active_tab: params[:tab] || "csv-upload", url_param_key: "tab", testid: "import-tabs") do |tabs| %>
|
||
<% tabs.with_nav do |nav| %>
|
||
<% nav.with_btn(id: "csv-upload", label: "Upload CSV") %>
|
||
<% nav.with_btn(id: "csv-paste", label: "Copy & Paste") %>
|
||
<% end %>
|
||
|
||
<% tabs.with_panel(tab_id: "csv-upload") do %>
|
||
<%= styled_form_with model: @import, scope: :import, url: import_upload_path(@import), multipart: true, class: "space-y-2", data: { drag_and_drop_import_target: "form" } do |form| %>
|
||
<%= form.select :col_sep, Import::SEPARATORS, label: true %>
|
||
|
||
<% if @import.type == "TransactionImport" || @import.type == "TradeImport" %>
|
||
<%= form.select :account_id, @import.family.accounts.visible.pluck(:name, :id), { label: "Account (optional)", include_blank: "Multi-account import", selected: @import.account_id } %>
|
||
<% end %>
|
||
|
||
<label for="import_import_file_csv" class="flex flex-col items-center justify-center w-full h-64 border border-secondary border-dashed rounded-xl cursor-pointer" data-controller="file-upload" data-file-upload-target="uploadArea">
|
||
<div class="flex flex-col items-center justify-center pt-5 pb-6">
|
||
<div data-file-upload-target="uploadText" class="flex flex-col items-center">
|
||
<%= icon("plus", size: "lg", class: "mb-4 mx-auto") %>
|
||
<p class="mb-2 text-md text-gray text-center">
|
||
<span class="font-medium text-primary"><%= t(".browse") %></span> <%= t(".csv_file_prompt") %>
|
||
</p>
|
||
</div>
|
||
|
||
<div class="flex flex-col gap-4 items-center hidden mb-2" data-file-upload-target="fileName">
|
||
<span class="text-primary">
|
||
<%= icon("file-text", size: "lg", color: "current") %>
|
||
</span>
|
||
<p class="text-md font-medium text-primary"></p>
|
||
</div>
|
||
|
||
<%= form.file_field :import_file, id: "import_import_file_csv", class: "hidden", "data-auto-submit-form-target": "auto", "data-file-upload-target": "input", "data-drag-and-drop-import-target": "input" %>
|
||
</div>
|
||
</label>
|
||
|
||
<%= form.submit "Upload CSV", disabled: @import.complete? %>
|
||
<% end %>
|
||
<% end %>
|
||
|
||
<% tabs.with_panel(tab_id: "csv-paste") do %>
|
||
<%= styled_form_with model: @import, scope: :import, url: import_upload_path(@import), multipart: true, class: "space-y-2" do |form| %>
|
||
<%= form.select :col_sep, Import::SEPARATORS, label: true %>
|
||
|
||
<% if @import.type == "TransactionImport" || @import.type == "TradeImport" %>
|
||
<%= form.select :account_id, @import.family.accounts.visible.pluck(:name, :id), { label: "Account (optional)", include_blank: "Multi-account import", selected: @import.account_id } %>
|
||
<% end %>
|
||
|
||
<%= form.text_area :raw_file_str,
|
||
rows: 10,
|
||
required: true,
|
||
placeholder: "Paste your CSV file contents here",
|
||
"data-auto-submit-form-target": "auto" %>
|
||
|
||
<%= form.submit "Upload CSV", disabled: @import.complete? %>
|
||
<% end %>
|
||
<% end %>
|
||
<% end %>
|
||
</div>
|
||
|
||
<div class="flex justify-center">
|
||
<span class="text-secondary text-sm">
|
||
<%= link_to "Download a sample CSV", "/imports/#{@import.id}/upload/sample_csv", class: "text-primary underline", data: { turbo: false } %> to see the required CSV format
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<% end %>
|