mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-02 20:15:22 +02:00
Create tagging system (#792)
* Repro * Fix * Update signage * Create tagging system * Add tags to transaction imports * Build tagging UI * Cleanup * More cleanup
This commit is contained in:
parent
41c991384a
commit
457247da8e
38 changed files with 607 additions and 90 deletions
|
@ -63,6 +63,6 @@
|
|||
<% else %>
|
||||
<%= previous_setting("Billing", settings_billing_path) %>
|
||||
<% end %>
|
||||
<%= next_setting("Categories", transaction_categories_path) %>
|
||||
<%= next_setting("Tags", tags_path) %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -46,6 +46,9 @@
|
|||
<div class="h-px bg-alpha-black-100 w-full"></div>
|
||||
</div>
|
||||
<ul class="space-y-1">
|
||||
<li>
|
||||
<%= sidebar_link_to t(".tags_label"), tags_path, icon: "tags" %>
|
||||
</li>
|
||||
<li>
|
||||
<%= sidebar_link_to t(".categories_label"), transaction_categories_path, icon: "tags" %>
|
||||
</li>
|
||||
|
|
10
app/views/tags/_badge.html.erb
Normal file
10
app/views/tags/_badge.html.erb
Normal file
|
@ -0,0 +1,10 @@
|
|||
<%# locals: (tag:) %>
|
||||
<% tag ||= null_category %>
|
||||
|
||||
<span class="border text-sm font-medium px-2.5 py-1 rounded-full content-center"
|
||||
style="
|
||||
background-color: color-mix(in srgb, <%= tag.color %> 5%, white);
|
||||
border-color: color-mix(in srgb, <%= tag.color %> 10%, white);
|
||||
color: <%= tag.color %>;">
|
||||
<%= tag.name %>
|
||||
</span>
|
38
app/views/tags/_form.html.erb
Normal file
38
app/views/tags/_form.html.erb
Normal file
|
@ -0,0 +1,38 @@
|
|||
<%= form_with model: tag, data: { turbo: false } do |form| %>
|
||||
<div class="flex flex-col space-y-4 w-96" data-controller="color-select" data-color-select-selection-value="<%= tag.color %>">
|
||||
<fieldset class="relative">
|
||||
<span data-color-select-target="decoration" class="pointer-events-none absolute inset-y-3.5 left-3 flex items-center pl-1 block w-1 rounded-lg"></span>
|
||||
<%= form.text_field :name,
|
||||
value: tag.name,
|
||||
autofocus: "",
|
||||
required: true,
|
||||
placeholder: "Enter tag name",
|
||||
class: "rounded-lg w-full focus:ring-black focus:border-transparent placeholder:text-gray-500 pl-6" %>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<%= form.hidden_field :color, data: { color_select_target: "input" } %>
|
||||
|
||||
<ul role="radiogroup" class="flex justify-between items-center py-2">
|
||||
<% Tag::COLORS.each do |color| %>
|
||||
<li tabindex="0"
|
||||
role="radio"
|
||||
data-action="click->color-select#select keydown.enter->color-select#select keydown.space->color-select#select"
|
||||
data-value="<%= color %>"
|
||||
class="flex shrink-0 justify-center items-center w-5 h-5 cursor-pointer hover:bg-gray-200 rounded-full">
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</fieldset>
|
||||
|
||||
<section>
|
||||
<%= hidden_field_tag :tag_id, params[:tag_id] %>
|
||||
|
||||
<% if tag.persisted? %>
|
||||
<%= form.submit t(".update") %>
|
||||
<% else %>
|
||||
<%= form.submit t(".create") %>
|
||||
<% end %>
|
||||
</section>
|
||||
</div>
|
||||
<% end %>
|
23
app/views/tags/_tag.html.erb
Normal file
23
app/views/tags/_tag.html.erb
Normal file
|
@ -0,0 +1,23 @@
|
|||
<div id="<%= dom_id(tag) %>" class="flex justify-between mx-4 py-5 border-b last:border-b-0 border-alpha-black-50">
|
||||
<%= render "badge", tag: tag %>
|
||||
|
||||
<%= 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_tag_path(tag),
|
||||
class: "block w-full py-2 px-3 space-x-2 text-gray-900 hover:bg-gray-50 flex items-center rounded-lg",
|
||||
data: { turbo_frame: :modal } do %>
|
||||
<%= lucide_icon "pencil-line", class: "w-5 h-5 text-gray-500" %>
|
||||
|
||||
<span><%= t(".edit") %></span>
|
||||
<% end %>
|
||||
|
||||
<%= link_to new_tag_deletion_path(tag),
|
||||
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_frame: :modal } do %>
|
||||
<%= lucide_icon "trash-2", class: "w-5 h-5" %>
|
||||
|
||||
<span><%= t(".delete") %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
33
app/views/tags/deletions/new.html.erb
Normal file
33
app/views/tags/deletions/new.html.erb
Normal file
|
@ -0,0 +1,33 @@
|
|||
<%= modal do %>
|
||||
<article class="mx-auto p-4 w-screen max-w-md">
|
||||
<div class="space-y-2">
|
||||
<header class="flex justify-between">
|
||||
<h2 class="font-medium text-xl"><%= t(".delete_tag") %></h2>
|
||||
<%= lucide_icon "x", class: "w-5 h-5 text-gray-500", data: { action: "click->modal#close" } %>
|
||||
</header>
|
||||
|
||||
<p class="text-gray-500 font-light">
|
||||
<%= t(".explanation", tag_name: @tag.name) %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<%= form_with url: tag_deletions_path(@tag),
|
||||
data: {
|
||||
turbo: false,
|
||||
controller: "deletion",
|
||||
deletion_dangerous_action_class: "form-field__submit bg-white text-red-600 border hover:bg-red-50",
|
||||
deletion_safe_action_class: "form-field__submit border border-transparent",
|
||||
deletion_submit_text_when_not_replacing_value: t(".delete_and_leave_uncategorized", tag_name: @tag.name),
|
||||
deletion_submit_text_when_replacing_value: t(".delete_and_recategorize", tag_name: @tag.name) } do |f| %>
|
||||
<%= f.collection_select :replacement_tag_id,
|
||||
Current.family.tags.alphabetically.without(@tag),
|
||||
:id, :name,
|
||||
{ prompt: t(".replacement_tag_prompt"), label: t(".tag") },
|
||||
{ data: { deletion_target: "replacementField", action: "deletion#updateSubmitButton" } } %>
|
||||
|
||||
<%= f.submit t(".delete_and_leave_uncategorized", tag_name: @tag.name),
|
||||
class: "form-field__submit bg-white text-red-600 border hover:bg-red-50",
|
||||
data: { deletion_target: "submitButton" } %>
|
||||
<% end %>
|
||||
</article>
|
||||
<% end %>
|
10
app/views/tags/edit.html.erb
Normal file
10
app/views/tags/edit.html.erb
Normal file
|
@ -0,0 +1,10 @@
|
|||
<%= modal do %>
|
||||
<article class="mx-auto w-full p-4 space-y-4">
|
||||
<header class="flex justify-between">
|
||||
<h2 class="font-medium text-xl"><%= t(".edit") %></h2>
|
||||
<%= lucide_icon "x", class: "w-5 h-5 text-gray-500", data: { action: "click->modal#close" } %>
|
||||
</header>
|
||||
|
||||
<%= render "form", tag: @tag %>
|
||||
</article>
|
||||
<% end %>
|
49
app/views/tags/index.html.erb
Normal file
49
app/views/tags/index.html.erb
Normal file
|
@ -0,0 +1,49 @@
|
|||
<% content_for :sidebar do %>
|
||||
<%= render "settings/nav" %>
|
||||
<% end %>
|
||||
|
||||
<section class="space-y-4">
|
||||
<header class="flex items-center justify-between">
|
||||
<h1 class="text-gray-900 text-xl font-medium"><%= t(".tags") %></h1>
|
||||
|
||||
<%= link_to new_tag_path, 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 %>
|
||||
<%= lucide_icon "plus", class: "w-5 h-5" %>
|
||||
<p><%= t(".new") %></p>
|
||||
<% end %>
|
||||
</header>
|
||||
|
||||
<div class="bg-white shadow-xs border border-alpha-black-25 rounded-xl p-4">
|
||||
|
||||
<% if @tags.any? %>
|
||||
|
||||
<div class="rounded-xl bg-gray-25 p-1">
|
||||
<h2 class="uppercase px-4 py-2 text-gray-500 text-xs"><%= t(".tags") %> · <%= @tags.size %></h2>
|
||||
|
||||
<div class="border border-alpha-gray-100 rounded-lg bg-white shadow-xs">
|
||||
|
||||
<%= render @tags %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% else %>
|
||||
|
||||
<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(".empty") %></p>
|
||||
<%= link_to new_tag_path, 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>
|
||||
|
||||
<% end %>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="flex justify-between gap-4">
|
||||
<%= previous_setting("Accounts", accounts_path) %>
|
||||
<%= next_setting("Categories", transaction_categories_path) %>
|
||||
</footer>
|
||||
</section>
|
10
app/views/tags/new.html.erb
Normal file
10
app/views/tags/new.html.erb
Normal file
|
@ -0,0 +1,10 @@
|
|||
<%= modal do %>
|
||||
<article class="mx-auto w-full p-4 space-y-4">
|
||||
<header class="flex justify-between">
|
||||
<h2 class="font-medium text-xl"><%= t(".new") %></h2>
|
||||
<%= lucide_icon "x", class: "w-5 h-5 text-gray-500", data: { action: "click->modal#close" } %>
|
||||
</header>
|
||||
|
||||
<%= render "form", tag: @tag %>
|
||||
</article>
|
||||
<% end %>
|
|
@ -14,20 +14,20 @@
|
|||
<%= form_with url: transaction_category_deletions_path(@category),
|
||||
data: {
|
||||
turbo: false,
|
||||
controller: "category-deletion",
|
||||
category_deletion_dangerous_action_class: "form-field__submit bg-white text-red-600 border hover:bg-red-50",
|
||||
category_deletion_safe_action_class: "form-field__submit border border-transparent",
|
||||
category_deletion_submit_text_when_not_replacing_value: t(".delete_and_leave_uncategorized", category_name: @category.name),
|
||||
category_deletion_submit_text_when_replacing_value: t(".delete_and_recategorize", category_name: @category.name) } do |f| %>
|
||||
controller: "deletion",
|
||||
deletion_dangerous_action_class: "form-field__submit bg-white text-red-600 border hover:bg-red-50",
|
||||
deletion_safe_action_class: "form-field__submit border border-transparent",
|
||||
deletion_submit_text_when_not_replacing_value: t(".delete_and_leave_uncategorized", category_name: @category.name),
|
||||
deletion_submit_text_when_replacing_value: t(".delete_and_recategorize", category_name: @category.name) } do |f| %>
|
||||
<%= f.collection_select :replacement_category_id,
|
||||
Current.family.transaction_categories.alphabetically.without(@category),
|
||||
:id, :name,
|
||||
{ prompt: t(".replacement_category_prompt"), label: t(".category") },
|
||||
{ data: { category_deletion_target: "replacementCategoryField", action: "category-deletion#updateSubmitButton" } } %>
|
||||
{ data: { deletion_target: "replacementField", action: "deletion#updateSubmitButton" } } %>
|
||||
|
||||
<%= f.submit t(".delete_and_leave_uncategorized", category_name: @category.name),
|
||||
class: "form-field__submit bg-white text-red-600 border hover:bg-red-50",
|
||||
data: { category_deletion_target: "submitButton" } %>
|
||||
data: { deletion_target: "submitButton" } %>
|
||||
<% end %>
|
||||
</article>
|
||||
<% end %>
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
</div>
|
||||
|
||||
<footer class="flex justify-between gap-4">
|
||||
<%= previous_setting("Accounts", accounts_path) %>
|
||||
<%= previous_setting("Tags", tags_path) %>
|
||||
<%= next_setting("Merchants", transaction_merchants_path) %>
|
||||
</footer>
|
||||
</section>
|
||||
|
|
|
@ -4,37 +4,44 @@
|
|||
<span class="text-lg text-gray-500"><%= @transaction.currency %></span>
|
||||
</h3>
|
||||
<span class="text-sm text-gray-500"><%= @transaction.date.strftime("%A %d %B") %></span>
|
||||
<%= form_with model: @transaction, html: {data: {controller: "auto-submit-form"}} do |f| %>
|
||||
<details class="group" open>
|
||||
<summary class="list-none bg-gray-25 rounded-xl py-1 mt-4 group-open:mb-2">
|
||||
<div class="py-2 px-[11px] flex items-center justify-between font-medium text-xs text-gray-500">
|
||||
Overview
|
||||
<%= lucide_icon("chevron-down", class: "hidden group-open:block text-gray-500 w-5 h-5") %>
|
||||
<%= lucide_icon("chevron-right", class: "group-open:hidden text-gray-500 w-5 h-5") %>
|
||||
</div>
|
||||
</summary>
|
||||
<%= f.date_field :date, label: "Date", max: Date.today, "data-auto-submit-form-target": "auto" %>
|
||||
<div class="h-2"></div>
|
||||
<%= f.collection_select :account_id, Current.family.accounts, :id, :name, { prompt: "Select an Account", label: "Account", class: "text-gray-400" }, {class: "form-field__input cursor-not-allowed text-gray-400", disabled: "disabled"} %>
|
||||
</details>
|
||||
<details class="group" open>
|
||||
<summary class="list-none bg-gray-25 rounded-xl py-1 mt-6 group-open:mb-2">
|
||||
<div class="py-2 px-[11px] flex items-center justify-between font-medium text-xs text-gray-500">
|
||||
Description
|
||||
<%= lucide_icon("chevron-down", class: "hidden group-open:block text-gray-500 w-5 h-5") %>
|
||||
<%= lucide_icon("chevron-right", class: "group-open:hidden text-gray-500 w-5 h-5") %>
|
||||
</div>
|
||||
</summary>
|
||||
|
||||
<details class="group" open>
|
||||
<summary class="list-none bg-gray-25 rounded-xl py-1 mt-4 group-open:mb-2">
|
||||
<div class="py-2 px-[11px] flex items-center justify-between font-medium text-xs text-gray-500">
|
||||
Overview
|
||||
<%= lucide_icon("chevron-down", class: "hidden group-open:block text-gray-500 w-5 h-5") %>
|
||||
<%= lucide_icon("chevron-right", class: "group-open:hidden text-gray-500 w-5 h-5") %>
|
||||
</div>
|
||||
</summary>
|
||||
<%= form_with model: @transaction, html: { data: { controller: "auto-submit-form" } } do |f| %>
|
||||
<div class="space-y-2">
|
||||
<%= f.date_field :date, label: "Date", max: Date.today, "data-auto-submit-form-target": "auto" %>
|
||||
<%= f.collection_select :category_id, Current.family.transaction_categories, :id, :name, { prompt: "Select a category", label: "Category", class: "text-gray-400" }, "data-auto-submit-form-target": "auto" %>
|
||||
<%= f.collection_select :account_id, Current.family.accounts, :id, :name, { prompt: "Select an Account", label: "Account", class: "text-gray-500" }, { class: "form-field__input cursor-not-allowed text-gray-400", disabled: "disabled" } %>
|
||||
</div>
|
||||
<% end %>
|
||||
</details>
|
||||
<details class="group" open>
|
||||
<summary class="list-none bg-gray-25 rounded-xl py-1 mt-6 group-open:mb-2">
|
||||
<div class="py-2 px-[11px] flex items-center justify-between font-medium text-xs text-gray-500">
|
||||
Description
|
||||
<%= lucide_icon("chevron-down", class: "hidden group-open:block text-gray-500 w-5 h-5") %>
|
||||
<%= lucide_icon("chevron-right", class: "group-open:hidden text-gray-500 w-5 h-5") %>
|
||||
</div>
|
||||
</summary>
|
||||
<%= form_with model: @transaction, html: { data: { controller: "auto-submit-form" } } do |f| %>
|
||||
<%= f.text_field :name, label: "Name", "data-auto-submit-form-target": "auto" %>
|
||||
</details>
|
||||
<details class="group" open>
|
||||
<summary class="list-none bg-gray-25 rounded-xl py-1 mt-6 group-open:mb-2">
|
||||
<div class="py-2 px-[11px] flex items-center justify-between font-medium text-xs text-gray-500">
|
||||
<span>Settings</span>
|
||||
<%= lucide_icon("chevron-down", class: "hidden group-open:block text-gray-500 w-5 h-5") %>
|
||||
<%= lucide_icon("chevron-right", class: "group-open:hidden text-gray-500 w-5 h-5") %>
|
||||
</div>
|
||||
</summary>
|
||||
<% end %>
|
||||
</details>
|
||||
<details class="group" open>
|
||||
<summary class="list-none bg-gray-25 rounded-xl py-1 mt-6 group-open:mb-2">
|
||||
<div class="py-2 px-[11px] flex items-center justify-between font-medium text-xs text-gray-500">
|
||||
<span>Settings</span>
|
||||
<%= lucide_icon("chevron-down", class: "hidden group-open:block text-gray-500 w-5 h-5") %>
|
||||
<%= lucide_icon("chevron-right", class: "group-open:hidden text-gray-500 w-5 h-5") %>
|
||||
</div>
|
||||
</summary>
|
||||
<%= form_with model: @transaction, html: { data: { controller: "auto-submit-form" } } do |f| %>
|
||||
<label class="flex items-center cursor-pointer justify-between mx-3">
|
||||
<%= f.check_box :excluded, class: "sr-only peer", "data-auto-submit-form-target": "auto" %>
|
||||
<div class="flex flex-col justify-center text-sm w-[340px] py-3">
|
||||
|
@ -43,16 +50,39 @@
|
|||
</div>
|
||||
<div class="relative w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-100 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
|
||||
</label>
|
||||
</details>
|
||||
<details class="group" open>
|
||||
<summary class="list-none bg-gray-25 rounded-xl py-1 mt-6 mb-2">
|
||||
<div class="py-2 px-[11px] flex items-center justify-between font-medium text-xs text-gray-500">
|
||||
<span>Additional</span>
|
||||
<%= lucide_icon("chevron-down", class: "hidden group-open:block text-gray-500 w-5 h-5") %>
|
||||
<%= lucide_icon("chevron-right", class: "group-open:hidden text-gray-500 w-5 h-5") %>
|
||||
<% end %>
|
||||
</details>
|
||||
<details class="group" open>
|
||||
<summary class="list-none bg-gray-25 rounded-xl py-1 mt-6 mb-2">
|
||||
<div class="py-2 px-[11px] flex items-center justify-between font-medium text-xs text-gray-500">
|
||||
<span>Additional</span>
|
||||
<%= lucide_icon("chevron-down", class: "hidden group-open:block text-gray-500 w-5 h-5") %>
|
||||
<%= lucide_icon("chevron-right", class: "group-open:hidden text-gray-500 w-5 h-5") %>
|
||||
</div>
|
||||
</summary>
|
||||
|
||||
<div class="mb-2">
|
||||
|
||||
<% if @transaction.tags.any? %>
|
||||
<div class="pt-3 pb-2 flex flex-wrap items-center gap-1">
|
||||
<% @transaction.tags.each do |tag| %>
|
||||
<div class="relative">
|
||||
<%= render partial: "tags/badge", locals: { tag: tag } %>
|
||||
<%= button_to transaction_path(@transaction, transaction: { remove_tag_id: tag.id }), method: :patch, "data-turbo": false, class: "absolute -top-2 -right-1 px-0.5 py rounded-full hover:bg-alpha-black-200 border border-alpha-black-100" do %>
|
||||
<%= lucide_icon("x", class: "w-3 h-3") %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</summary>
|
||||
<% end %>
|
||||
|
||||
<%= form_with model: @transaction, html: { data: { controller: "auto-submit-form", turbo: false } } do |f| %>
|
||||
<%= f.collection_select :tag_id, Current.family.tags.alphabetically.excluding(@transaction.tags), :id, :name, { prompt: "Select a tag", label: "Select a tag", class: "placeholder:text-gray-500" }, "data-auto-submit-form-target": "auto", "data-turbo": false %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= form_with model: @transaction, html: { data: { controller: "auto-submit-form" } } do |f| %>
|
||||
<%= f.text_area :notes, label: "Notes", placeholder: "Enter a note", "data-auto-submit-form-target": "auto" %>
|
||||
</details>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</details>
|
||||
<% end %>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue