diff --git a/app/models/category.rb b/app/models/category.rb
index 94d5097e1..39a740f7d 100644
--- a/app/models/category.rb
+++ b/app/models/category.rb
@@ -9,6 +9,7 @@ class Category < ApplicationRecord
belongs_to :parent, class_name: "Category", optional: true
validates :name, :color, :lucide_icon, :family, presence: true
+ validates :color, format: { with: /\A#[0-9A-Fa-f]{6}\z/ }
validates :name, uniqueness: { scope: :family_id }
validate :category_level_limit
diff --git a/app/views/categories/_form.html.erb b/app/views/categories/_form.html.erb
index 3b9e6f1b4..1f3e5fbeb 100644
--- a/app/views/categories/_form.html.erb
+++ b/app/views/categories/_form.html.erb
@@ -30,7 +30,7 @@
- <%= f.text_field :color , data: { category_target: "colorInput"}, inline: true %>
+ <%= f.text_field :color, data: { category_target: "colorInput" }, inline: true, pattern: "^#[0-9A-Fa-f]{6}$" %>
<%= icon "palette", size: "2xl", data: { action: "click->category#toggleSections" } %>
diff --git a/test/models/budget_test.rb b/test/models/budget_test.rb
index f681bebc2..13f8f5a2b 100644
--- a/test/models/budget_test.rb
+++ b/test/models/budget_test.rb
@@ -264,7 +264,7 @@ class BudgetTest < ActiveSupport::TestCase
source_budget.update!(budgeted_spending: 4000, expected_income: 6000)
# Create a category only in the source budget
- temp_category = Category.create!(name: "Temp #{Time.now.to_f}", family: family, color: "#aaa")
+ temp_category = Category.create!(name: "Temp #{Time.now.to_f}", family: family, color: "#aaaaaa")
source_budget.budget_categories.create!(category: temp_category, budgeted_spending: 100, currency: "USD")
target_budget = Budget.find_or_bootstrap(family, start_date: 1.month.ago)
@@ -283,7 +283,7 @@ class BudgetTest < ActiveSupport::TestCase
target_budget = Budget.find_or_bootstrap(family, start_date: 1.month.ago)
# Add a new category only to the target
- new_category = Category.create!(name: "New #{Time.now.to_f}", family: family, color: "#bbb")
+ new_category = Category.create!(name: "New #{Time.now.to_f}", family: family, color: "#bbbbbb")
target_budget.budget_categories.create!(category: new_category, budgeted_spending: 0, currency: "USD")
target_budget.copy_from!(source_budget)
diff --git a/test/models/category_test.rb b/test/models/category_test.rb
index da4f9a48a..fc4036600 100644
--- a/test/models/category_test.rb
+++ b/test/models/category_test.rb
@@ -40,4 +40,19 @@ class CategoryTest < ActiveSupport::TestCase
assert names.all? { |name| name.is_a?(String) }
assert_equal names, names.uniq # No duplicates
end
+
+ test "should accept valid 6-digit hex colors" do
+ [ "#FFFFFF", "#000000", "#123456", "#ABCDEF", "#abcdef" ].each do |color|
+ category = Category.new(name: "Category #{color}", color: color, lucide_icon: "shapes", family: @family)
+ assert category.valid?, "#{color} should be valid"
+ end
+ end
+
+ test "should reject invalid colors" do
+ [ "invalid", "#123", "#1234567", "#GGGGGG", "red", "ffffff", "#ffff", "" ].each do |color|
+ category = Category.new(name: "Category #{color}", color: color, lucide_icon: "shapes", family: @family)
+ assert_not category.valid?, "#{color} should be invalid"
+ assert_includes category.errors[:color], "is invalid"
+ end
+ end
end