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
24
app/controllers/tags/deletions_controller.rb
Normal file
24
app/controllers/tags/deletions_controller.rb
Normal file
|
@ -0,0 +1,24 @@
|
|||
class Tags::DeletionsController < ApplicationController
|
||||
layout "with_sidebar"
|
||||
|
||||
before_action :set_tag
|
||||
before_action :set_replacement_tag, only: :create
|
||||
|
||||
def new
|
||||
end
|
||||
|
||||
def create
|
||||
@tag.replace_and_destroy! @replacement_tag
|
||||
redirect_back_or_to tags_path, notice: t(".deleted")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_tag
|
||||
@tag = Current.family.tags.find_by(id: params[:tag_id])
|
||||
end
|
||||
|
||||
def set_replacement_tag
|
||||
@replacement_tag = Current.family.tags.find_by(id: params[:replacement_tag_id])
|
||||
end
|
||||
end
|
36
app/controllers/tags_controller.rb
Normal file
36
app/controllers/tags_controller.rb
Normal file
|
@ -0,0 +1,36 @@
|
|||
class TagsController < ApplicationController
|
||||
layout "with_sidebar"
|
||||
|
||||
before_action :set_tag, only: %i[ edit update ]
|
||||
|
||||
def index
|
||||
@tags = Current.family.tags.alphabetically
|
||||
end
|
||||
|
||||
def new
|
||||
@tag = Current.family.tags.new color: Tag::COLORS.sample
|
||||
end
|
||||
|
||||
def create
|
||||
Current.family.tags.create!(tag_params)
|
||||
redirect_to tags_path, notice: t(".created")
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
@tag.update!(tag_params)
|
||||
redirect_to tags_path, notice: t(".updated")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_tag
|
||||
@tag = Current.family.tags.find(params[:id])
|
||||
end
|
||||
|
||||
def tag_params
|
||||
params.require(:tag).permit(:name, :color)
|
||||
end
|
||||
end
|
|
@ -72,8 +72,8 @@ class TransactionsController < ApplicationController
|
|||
|
||||
def create
|
||||
@transaction = Current.family.accounts
|
||||
.find(params[:transaction][:account_id])
|
||||
.transactions.build(transaction_params.merge(amount: amount))
|
||||
.find(params[:transaction][:account_id])
|
||||
.transactions.build(transaction_params.merge(amount: amount))
|
||||
|
||||
respond_to do |format|
|
||||
if @transaction.save
|
||||
|
@ -88,11 +88,20 @@ class TransactionsController < ApplicationController
|
|||
def update
|
||||
respond_to do |format|
|
||||
sync_start_date = if transaction_params[:date]
|
||||
[ @transaction.date, Date.parse(transaction_params[:date]) ].compact.min
|
||||
[ @transaction.date, Date.parse(transaction_params[:date]) ].compact.min
|
||||
else
|
||||
@transaction.date
|
||||
end
|
||||
|
||||
if params[:transaction][:tag_id].present?
|
||||
tag = Current.family.tags.find(params[:transaction][:tag_id])
|
||||
@transaction.tags << tag unless @transaction.tags.include?(tag)
|
||||
end
|
||||
|
||||
if params[:transaction][:remove_tag_id].present?
|
||||
@transaction.tags.delete(params[:transaction][:remove_tag_id])
|
||||
end
|
||||
|
||||
if @transaction.update(transaction_params)
|
||||
@transaction.account.sync_later(sync_start_date)
|
||||
|
||||
|
@ -121,6 +130,7 @@ class TransactionsController < ApplicationController
|
|||
end
|
||||
|
||||
private
|
||||
|
||||
def delete_search_param(params, key, value: nil)
|
||||
if value
|
||||
params[key]&.delete(value)
|
||||
|
@ -153,8 +163,7 @@ class TransactionsController < ApplicationController
|
|||
params[:transaction][:nature].to_s.inquiry
|
||||
end
|
||||
|
||||
# Only allow a list of trusted parameters through.
|
||||
def transaction_params
|
||||
params.require(:transaction).permit(:name, :date, :amount, :currency, :notes, :excluded, :category_id, :merchant_id)
|
||||
params.require(:transaction).permit(:name, :date, :amount, :currency, :notes, :excluded, :category_id, :merchant_id, :tag_id, :remove_tag_id).except(:tag_id, :remove_tag_id)
|
||||
end
|
||||
end
|
||||
|
|
7
app/helpers/tags_helper.rb
Normal file
7
app/helpers/tags_helper.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
module TagsHelper
|
||||
def null_tag
|
||||
Tag.new \
|
||||
name: "Uncategorized",
|
||||
color: Tag::UNCATEGORIZED_COLOR
|
||||
end
|
||||
end
|
|
@ -1,7 +1,7 @@
|
|||
import { Controller } from "@hotwired/stimulus";
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = [ "replacementCategoryField", "submitButton" ]
|
||||
static targets = ["replacementField", "submitButton"]
|
||||
static classes = [ "dangerousAction", "safeAction" ]
|
||||
static values = {
|
||||
submitTextWhenReplacing: String,
|
||||
|
@ -9,7 +9,7 @@ export default class extends Controller {
|
|||
}
|
||||
|
||||
updateSubmitButton() {
|
||||
if (this.replacementCategoryFieldTarget.value) {
|
||||
if (this.replacementFieldTarget.value) {
|
||||
this.submitButtonTarget.value = this.submitTextWhenReplacingValue
|
||||
this.#markSafe()
|
||||
} else {
|
|
@ -1,5 +1,6 @@
|
|||
class Family < ApplicationRecord
|
||||
has_many :users, dependent: :destroy
|
||||
has_many :tags, dependent: :destroy
|
||||
has_many :accounts, dependent: :destroy
|
||||
has_many :transactions, through: :accounts
|
||||
has_many :imports, through: :accounts
|
||||
|
|
|
@ -111,16 +111,24 @@ class Import < ApplicationRecord
|
|||
def generate_transactions
|
||||
transactions = []
|
||||
category_cache = {}
|
||||
tag_cache = {}
|
||||
|
||||
csv.table.each do |row|
|
||||
category_name = row["category"]
|
||||
category_name = row["category"].presence
|
||||
tag_strings = row["tags"].presence&.split("|") || []
|
||||
tags = []
|
||||
|
||||
category = category_cache[category_name] ||= account.family.transaction_categories.find_or_initialize_by(name: category_name) if row["category"].present?
|
||||
tag_strings.each do |tag_string|
|
||||
tags << tag_cache[tag_string] ||= account.family.tags.find_or_initialize_by(name: tag_string)
|
||||
end
|
||||
|
||||
category = category_cache[category_name] ||= account.family.transaction_categories.find_or_initialize_by(name: category_name)
|
||||
|
||||
txn = account.transactions.build \
|
||||
name: row["name"].presence || FALLBACK_TRANSACTION_NAME,
|
||||
date: Date.iso8601(row["date"]),
|
||||
category: category,
|
||||
tags: tags,
|
||||
amount: BigDecimal(row["amount"]) * -1, # User inputs amounts with opposite signage of our internal representation
|
||||
currency: account.currency
|
||||
|
||||
|
@ -144,12 +152,16 @@ class Import < ApplicationRecord
|
|||
key: "category",
|
||||
label: "Category"
|
||||
|
||||
tags_field = Import::Field.new \
|
||||
key: "tags",
|
||||
label: "Tags"
|
||||
|
||||
amount_field = Import::Field.new \
|
||||
key: "amount",
|
||||
label: "Amount",
|
||||
validator: ->(value) { Import::Field.bigdecimal_validator(value) }
|
||||
|
||||
[ date_field, name_field, category_field, amount_field ]
|
||||
[ date_field, name_field, category_field, tags_field, amount_field ]
|
||||
end
|
||||
|
||||
def define_column_mapping_keys
|
||||
|
|
25
app/models/tag.rb
Normal file
25
app/models/tag.rb
Normal file
|
@ -0,0 +1,25 @@
|
|||
class Tag < ApplicationRecord
|
||||
belongs_to :family
|
||||
has_many :taggings, dependent: :destroy
|
||||
has_many :transactions, through: :taggings, source: :taggable, source_type: "Transaction"
|
||||
|
||||
validates :name, presence: true, uniqueness: { scope: :family }
|
||||
|
||||
scope :alphabetically, -> { order(:name) }
|
||||
|
||||
COLORS = %w[#e99537 #4da568 #6471eb #db5a54 #df4e92 #c44fe9 #eb5429 #61c9ea #805dee #6ad28a]
|
||||
|
||||
UNCATEGORIZED_COLOR = "#737373"
|
||||
|
||||
def replace_and_destroy!(replacement)
|
||||
transaction do
|
||||
raise ActiveRecord::RecordInvalid, "Replacement tag cannot be the same as the tag being destroyed" if replacement == self
|
||||
|
||||
if replacement
|
||||
taggings.update_all tag_id: replacement.id
|
||||
end
|
||||
|
||||
destroy!
|
||||
end
|
||||
end
|
||||
end
|
4
app/models/tagging.rb
Normal file
4
app/models/tagging.rb
Normal file
|
@ -0,0 +1,4 @@
|
|||
class Tagging < ApplicationRecord
|
||||
belongs_to :tag
|
||||
belongs_to :taggable, polymorphic: true
|
||||
end
|
|
@ -5,6 +5,9 @@ class Transaction < ApplicationRecord
|
|||
belongs_to :category, optional: true
|
||||
belongs_to :merchant, optional: true
|
||||
|
||||
has_many :taggings, as: :taggable, dependent: :destroy
|
||||
has_many :tags, through: :taggings
|
||||
|
||||
validates :name, :date, :amount, :account, presence: true
|
||||
|
||||
monetize :amount
|
||||
|
|
|
@ -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