chore: init commit

in worker.../repo/GITFOLDER.zip is the .git folder.
This commit is contained in:
2026-08-11 14:44:09 -04:00
parent 0012380fd3
commit 392781f7aa
781 changed files with 65944 additions and 0 deletions

View File

@@ -0,0 +1,409 @@
# frozen_string_literal: true
module Admin
# rubocop:disable Metrics/ClassLength
class FormBuilder < ActionView::Helpers::FormBuilder
# Tailwind CSS classes for form elements
INPUT_CLASSES = "block w-full rounded-md border-0 py-2 px-3 text-gray-900 ring-1 ring-inset ring-gray-300 " \
"placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 " \
"sm:text-sm sm:leading-6"
INPUT_ERROR_CLASSES = "block w-full rounded-md border-0 py-2 px-3 text-red-900 ring-1 ring-inset " \
"ring-red-300 placeholder:text-red-300 focus:ring-2 focus:ring-inset " \
"focus:ring-red-500 sm:text-sm sm:leading-6"
LABEL_CLASSES = "block text-sm font-medium leading-6 text-gray-900"
ERROR_CLASSES = "mt-2 text-sm text-red-600"
HINT_CLASSES = "mt-1 text-sm text-gray-500"
# Override text_field to include Tailwind styling and error handling
def text_field(attribute, options = {})
field_wrapper(attribute, options) do
super(attribute, input_options(attribute, options))
end
end
# Email field with proper type and styling
def email_field(attribute, options = {})
field_wrapper(attribute, options) do
super(attribute, input_options(attribute, options))
end
end
# Password field
def password_field(attribute, options = {})
field_wrapper(attribute, options) do
super(attribute, input_options(attribute, options))
end
end
# Number field with optional step
def number_field(attribute, options = {})
field_wrapper(attribute, options) do
options[:step] ||= "any" unless options.key?(:step)
super(attribute, input_options(attribute, options))
end
end
# Currency field with proper formatting
# Usage: f.currency_field :price_cents, multiplier: 0.01, decimals: 2
def currency_field(attribute, options = {})
multiplier = options.delete(:multiplier) || 0.01
decimals = options.delete(:decimals) || 2
field_wrapper(attribute, options) do
value = object.public_send(attribute)
display_value = value ? (value * multiplier).round(decimals) : nil
number_field(
attribute,
options.merge(
value: display_value,
step: (1.0 / (10**decimals)),
class: input_class(attribute),
data: { currency_multiplier: (1.0 / multiplier).to_i }
)
)
end
end
# Text area with proper styling
def text_area(attribute, options = {})
field_wrapper(attribute, options) do
options[:rows] ||= 4
options[:class] = "#{input_class(attribute)} #{options[:class] || ''}"
super(attribute, options)
end
end
# Rich text area for ActionText fields
def rich_text_area(attribute, options = {})
field_wrapper(attribute, options) do
options[:class] = "trix-content #{options[:class] || ''}"
super(attribute, options)
end
end
# Override select to support label and hint options
def select(attribute, choices = nil, options = {}, html_options = {}, &)
# Extract custom options if provided as hash (when called with label/hint)
return super if choices.is_a?(Hash) && options.empty? && html_options.empty?
return super unless options.is_a?(Hash) && (options.key?(:label) || options.key?(:hint))
# Called as: select(:attr, choices, label: "...", hint: "...", include_blank: ...)
render_select_with_wrapper(attribute, choices, options, html_options, &)
end
# Select field with collection
# Usage: f.select_field :status, collection: [['Active', 'active'], ['Inactive', 'inactive']]
# Usage: f.select_field :grade, collection: (1..12).map { |g| [g.ordinalize, g] }
def select_field(attribute, options = {})
collection = options.delete(:collection) || []
include_blank = options.delete(:include_blank)
field_wrapper(attribute, options) do
select(
attribute, collection,
{ include_blank: include_blank },
input_options(attribute, options)
)
end
end
# Boolean field (checkbox) with proper styling
def boolean_field(attribute, options = {})
hint = options.delete(:hint)
label_text = options.delete(:label) || attribute.to_s.humanize
@template.content_tag(:div, class: "relative flex items-start py-4") do
@template.content_tag(:div, class: "flex h-6 items-center") do
check_box(
attribute,
class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-600"
)
end +
@template.content_tag(:div, class: "ml-3 text-sm leading-6") do
label(attribute, label_text, class: "font-medium text-gray-900") +
(hint ? @template.content_tag(:p, hint, class: "text-gray-500") : "".html_safe)
end
end
end
# Collection checkboxes with proper styling
def collection_check_boxes(attribute, collection, value_method, text_method, options = {})
label_text = options.delete(:label) || attribute.to_s.humanize
hint = options.delete(:hint)
wrapper_class = options.delete(:wrapper_class) || ""
@template.content_tag(:div, class: "mb-6 #{wrapper_class}") do
build_checkbox_collection_label(label_text, hint) +
build_checkbox_collection_items(attribute, collection, value_method, text_method) +
error_message(attribute)
end
end
# Date field
def date_field(attribute, options = {})
field_wrapper(attribute, options) do
super(attribute, input_options(attribute, options))
end
end
# DateTime field
def datetime_field(attribute, options = {})
field_wrapper(attribute, options) do
options[:class] = "#{input_class(attribute)} #{options[:class] || ''}"
super(attribute, options)
end
end
# Association select field (for BelongsTo associations)
# Usage: f.association_select :classroom_id, collection: Classroom.all
# Usage: f.association_select :school_year_id, collection: SchoolYear.all, label_method: :display_name
def association_select(attribute, options = {})
collection = options.delete(:collection)
label_method = options.delete(:label_method) || :to_s
value_method = options.delete(:value_method) || :id
include_blank = options.delete(:include_blank) || "Select..."
raise ArgumentError, "collection is required for association_select" unless collection
choices = collection.map do |item|
build_select_choice(item, label_method, value_method)
end
field_wrapper(attribute, options) do
select(
attribute, choices,
{ include_blank: include_blank },
input_options(attribute, options)
)
end
end
# Read-only field (displays value, not an input)
# Usage: f.read_only_field :created_at
# Usage: f.read_only_field :total_amount, value: "$#{object.total_amount}"
def read_only_field(attribute, options = {})
label_text = options.delete(:label) || attribute.to_s.humanize
value = options.delete(:value) || format_value(object.public_send(attribute))
hint = options.delete(:hint)
@template.content_tag(:div, class: "py-4") do
@template.content_tag(:dt, label_text, class: "text-sm font-medium text-gray-500") +
@template.content_tag(:dd, class: "mt-1 text-sm text-gray-900") do
value.to_s.html_safe # rubocop:disable Rails/OutputSafety
end +
(hint ? @template.content_tag(:p, hint, class: HINT_CLASSES) : "".html_safe)
end
end
# Submit button with Tailwind styling
def submit_button(text = "Save", options = {})
options[:class] = "inline-flex justify-center rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold " \
"text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 " \
"focus-visible:outline-offset-2 focus-visible:outline-blue-600 " \
"#{options[:class]}"
submit(text, options)
end
# Cancel button (link styled as button)
def cancel_button(text = "Cancel", url:, options: {})
options[:class] = "inline-flex justify-center rounded-md bg-white px-4 py-2 text-sm font-semibold " \
"text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 " \
"#{options[:class]}"
@template.link_to(text, url, options)
end
# Displays base errors (errors not tied to a specific attribute)
def base_errors
return "".html_safe unless object&.errors && object.errors[:base].any?
@template.content_tag(:div, class: "mb-6 p-4 bg-red-50 border border-red-200 rounded-lg") do
@template.content_tag(:p, class: "text-sm text-red-600") do
@template.content_tag(:i, "", class: "fas fa-exclamation-circle mr-1") +
object.errors[:base].join(", ")
end
end
end
private
def build_select_choice(item, label_method, value_method)
label = if label_method.respond_to?(:call)
label_method.call(item)
else
item.public_send(label_method)
end
value = if value_method.respond_to?(:call)
value_method.call(item)
else
item.public_send(value_method)
end
[label, value]
end
# Build checkbox collection label and hint
def build_checkbox_collection_label(label_text, hint)
@template.content_tag(:label, label_text, class: LABEL_CLASSES) +
(hint ? @template.content_tag(:p, hint, class: HINT_CLASSES) : "".html_safe)
end
# Build checkbox collection items
def build_checkbox_collection_items(attribute, collection, value_method, text_method)
@template.content_tag(:div, class: "mt-2 space-y-2") do
collection.map do |item|
build_single_checkbox(attribute, item, value_method, text_method)
end.join.html_safe # rubocop:disable Rails/OutputSafety
end
end
# Build a single checkbox item
def build_single_checkbox(attribute, item, value_method, text_method)
value = item.send(value_method)
text = item.send(text_method)
checkbox_id = "#{object_name}_#{attribute}_#{value}"
checked = Array(object.send(attribute)).include?(value)
@template.content_tag(:div, class: "relative flex items-start") do
build_checkbox_input(attribute, value, checked, checkbox_id) +
build_checkbox_label(checkbox_id, text)
end
end
# Build checkbox input element
def build_checkbox_input(attribute, value, checked, checkbox_id)
@template.content_tag(:div, class: "flex h-6 items-center") do
@template.check_box_tag(
"#{object_name}[#{attribute}][]",
value,
checked,
id: checkbox_id,
class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-600"
)
end
end
# Build checkbox label element
def build_checkbox_label(checkbox_id, text)
@template.content_tag(:div, class: "ml-3 text-sm leading-6") do
@template.label_tag(checkbox_id, text, class: "font-medium text-gray-900")
end
end
# Renders select field with label and hint wrapper
def render_select_with_wrapper(attribute, choices, options, html_options, &)
label_text = options.delete(:label)
hint = options.delete(:hint)
wrapper_class = options.delete(:wrapper_class)
# Separate standard select options from html options
select_options = extract_select_options(options)
remaining_html_options = html_options.merge(options)
@template.content_tag(:div, class: "mb-6 #{wrapper_class}") do
build_label(attribute, label_text) +
build_hint(hint) +
build_select_field(attribute, choices, select_options, remaining_html_options, &) +
error_message(attribute)
end
end
# Build label element
def build_label(attribute, label_text)
return "".html_safe unless label_text
label(attribute, label_text, class: LABEL_CLASSES)
end
# Build hint element
def build_hint(hint)
return "".html_safe unless hint
@template.content_tag(:p, hint, class: HINT_CLASSES)
end
# Build select field element
def build_select_field(attribute, choices, select_options, html_options, &)
@template.content_tag(:div, class: "mt-2") do
ActionView::Helpers::FormBuilder.instance_method(:select).bind(self).call(
attribute, choices, select_options, html_options.merge(class: input_class(attribute)), &
)
end
end
# Extract standard select options from the options hash
def extract_select_options(options)
select_options = {}
select_options[:include_blank] = options.delete(:include_blank) if options.key?(:include_blank)
select_options[:prompt] = options.delete(:prompt) if options.key?(:prompt)
select_options[:disabled] = options.delete(:disabled) if options.key?(:disabled)
select_options[:selected] = options.delete(:selected) if options.key?(:selected)
select_options
end
# Wraps a field with label, input, and error message
def field_wrapper(attribute, options = {}, &)
label_text = options.delete(:label) || attribute.to_s.humanize
hint = options.delete(:hint)
wrapper_class = options.delete(:wrapper_class) || ""
@template.content_tag(:div, class: "mb-6 #{wrapper_class}") do
label(attribute, label_text, class: LABEL_CLASSES) +
(hint ? @template.content_tag(:p, hint, class: HINT_CLASSES) : "".html_safe) +
@template.content_tag(:div, class: "mt-2", &) +
error_message(attribute)
end
end
# Returns the appropriate CSS class for an input based on validation state
def input_class(attribute)
if object&.errors && object.errors[attribute].any?
INPUT_ERROR_CLASSES
else
INPUT_CLASSES
end
end
# Merges input options with default classes
def input_options(attribute, options = {})
options[:class] = "#{input_class(attribute)} #{options[:class] || ''}"
options
end
# Displays validation error for an attribute
def error_message(attribute)
return "".html_safe unless object&.errors && object.errors[attribute].any?
@template.content_tag(:p, class: ERROR_CLASSES, id: "#{attribute}-error") do
@template.content_tag(:i, "", class: "fas fa-exclamation-circle mr-1") +
object.errors[attribute].first
end
end
# Formats a value for display in read-only fields
def format_value(value)
case value
when TrueClass
'<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ' \
'bg-green-100 text-green-800">Yes</span>'
when FalseClass
'<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ' \
'bg-gray-100 text-gray-800">No</span>'
when Time, DateTime, Date
value.strftime("%B %d, %Y")
when nil
'<span class="text-gray-400">—</span>'
else
value.to_s
end
end
end
# rubocop:enable Metrics/ClassLength
end