zutils 3.0.7 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,52 +1,97 @@
1
- <% card_color ||= 'danger' %>
2
- <% search_object ||= @q%>
3
- <% default_field = {name: :id_eq, attrs: {label: false, required: false, placeholder: 'ID', wrapper: false}} %>
4
- <% received_field = defined?(main_field) ? main_field : {} %>
5
- <% main_field = default_field.deep_merge(received_field) %>
1
+ <% card_color ||= 'primary' %>
2
+ <% search_object ||= @q %>
6
3
  <% opened ||= false %>
4
+
7
5
  <%= search_form_for search_object, url: url do |f| %>
8
- <div class="card card-outline card-<%= card_color %>" data-controller='filtering'>
9
- <div class="card-header with-border">
10
- <div class="input-group">
11
- <div class="input-group-prepend">
12
- <button type="button" class="btn btn-<%= card_color %>" data-action="click->filtering#toggle">
13
- <% if opened %>
14
- <i class="fa fa-fw fa-angle-down" data-filtering-target='icon'></i>
15
- <% else %>
16
- <i class="fa fa-fw fa-angle-right" data-filtering-target='icon'></i>
17
- <% end %>
18
- <span class="d-none d-sm-inline-block">
19
- Filtrar
20
- </span>
6
+ <div
7
+ class="card shadow-sm border rounded-2 mb-3 bg-body"
8
+ data-controller="filtering"
9
+ data-filtering-opened-value="<%= opened %>"
10
+ >
11
+ <div class="card-header bg-transparent border-0 pt-3 pb-0 px-3">
12
+ <h6 class="mb-0 fw-bold text-secondary">
13
+ <i class="fa fa-search me-2 opacity-50"></i>
14
+ Pesquisar
15
+ </h6>
16
+ </div>
17
+
18
+ <div class="card-body p-3">
19
+ <div class="d-flex align-items-center w-100">
20
+ <button
21
+ type="button"
22
+ class="btn bg-secondary-subtle px-0 rounded-circle me-2 flex-shrink-0"
23
+ style="width: 42px; height: 42px;"
24
+ data-action="click->filtering#toggle"
25
+ title="Filtros Avançados"
26
+ >
27
+ <i
28
+ class="fa <%= opened ? 'fa-angle-down' : 'fa-sliders-h' %>"
29
+ data-filtering-target="icon"
30
+ ></i>
31
+ </button>
32
+
33
+ <div class="flex-grow-1 position-relative d-flex align-items-center">
34
+ <% main_field_name = defined?(main_field) ? main_field[:name] : :id_eq
35
+ main_attrs = {
36
+ label: false,
37
+ required: false,
38
+ placeholder: 'ID',
39
+ wrapper: false,
40
+ input_html: { class: 'form-control pe-5' }
41
+ }.deep_merge(defined?(main_field) ? (main_field[:attrs] || {}) : {}) %>
42
+
43
+ <%= f.input main_field_name, main_attrs %>
44
+
45
+ <button
46
+ type="submit"
47
+ class="
48
+ btn border-0 position-absolute end-0 me-2 p-2 text-secondary
49
+ d-flex align-items-center justify-content-center
50
+ "
51
+ style="background: transparent; z-index: 5;"
52
+ >
53
+ <i class="fa fa-search fs-5"></i>
21
54
  </button>
22
55
  </div>
23
- <%= f.input (main_field&.dig(:name) || :id_eq), main_field&.dig(:attrs) %>
24
- <div class="input-group-append">
25
- <%= button_tag type: "submit", class: "btn btn-#{card_color}" do %>
26
- <i class="fa fa-search"></i>
27
- <span class="d-none d-sm-inline-block">
28
- Buscar
29
- </span>
30
- <% end %>
31
- </div>
32
- </div>
33
- <div class="row">
34
- <div class="col-md-12 active-filters" data-filtering-target='filters'></div>
35
56
  </div>
57
+
58
+ <div
59
+ class="active-filters d-flex gap-1 flex-wrap pt-2 ps-5"
60
+ data-filtering-target="filters"
61
+ style="margin-top: -5px;"
62
+ ></div>
36
63
  </div>
37
- <div class="card-body" style='<%= 'display: none;' unless opened %>' data-filtering-target='body'>
64
+
65
+ <div
66
+ class="card-body pt-0 collapse <%= 'show' if opened %>"
67
+ data-filtering-target="body"
68
+ >
69
+ <div class="d-flex align-items-center">
70
+ <h6 class="mb-0 fw-bold">
71
+ <i class="fa fa-filter me-2 opacity-50"></i>
72
+ Filtros Detalhados
73
+ </h6>
74
+
75
+ <hr class="flex-grow-1 ms-3 opacity-10 border-black">
76
+ </div>
77
+
38
78
  <div class="row">
39
79
  <% search_fields ||= [] %>
80
+
40
81
  <% search_fields.each do |sf| %>
41
- <% default_attrs = {required: false, wrapper_html: {class: "col-md-3"}} %>
42
- <% received_attrs = sf.dig(:attrs) || {} %>
43
- <% attrs = default_attrs.deep_merge(received_attrs) %>
44
- <%= f.input sf.dig(:name), attrs %>
82
+ <% field_attrs = { required: false, wrapper_html: {class: "col-md-3"} }.deep_merge(sf[:attrs] || {}) %>
83
+
84
+ <%= f.input sf[:name], field_attrs %>
85
+ <% end %>
86
+ </div>
87
+
88
+ <div class="d-flex justify-content-end align-items-center gap-2">
89
+ <%= link_to "Limpar", url, class: "btn btn-link btn-sm text-decoration-none text-muted" %>
90
+
91
+ <%= button_tag type: "submit", class: "btn btn-sm btn-#{card_color} px-4" do %>
92
+ Aplicar
45
93
  <% end %>
46
94
  </div>
47
- <%= button_tag type: "submit", class: "btn btn-#{card_color}" do %>
48
- <i class="fa fa-search"></i> Buscar
49
- <% end %>
50
95
  </div>
51
96
  </div>
52
- <% end %>
97
+ <% end %>
@@ -5,213 +5,106 @@
5
5
  <% nested_resource ||= false %>
6
6
  <% relationships ||= {} %>
7
7
 
8
- <% card_color ||= 'danger' %>
9
- <% hide ||= %w[show delete] %>
10
- <% size ||= "xs" %>
8
+ <% color ||= 'primary' %>
9
+ <% hide ||= %w[new show] %>
10
+ <% size ||= "sm" %>
11
+ <% buttons_size ||= "md" %>
11
12
 
12
- <% new_color ||= 'primary' %>
13
- <% edit_color ||= 'warning' %>
14
- <% delete_color ||= 'danger' %>
13
+ <% edit_color ||= 'btn-warning' %>
14
+ <% delete_color ||= 'btn-outline-danger' %>
15
15
 
16
16
  <% if show_columns.empty? %>
17
17
  <% form_fields ||= (object.class.respond_to?('form_fields') ? object.class.form_fields : {}) %>
18
18
  <% form_ordered_fields ||= (form_fields.empty? ? object.class.column_names : form_fields.keys) %>
19
- <% show_columns = form_ordered_fields - ['id', 'created_at', 'updated_at', 'deleted_at'] - excluded_columns + additional_columns %>
19
+ <% show_columns = form_ordered_fields - ['id','created_at','updated_at','deleted_at'] - excluded_columns + additional_columns %>
20
20
  <% end %>
21
21
 
22
22
  <% unless hide.include? "top_bar" %>
23
- <div class="card card-outline card-<%= card_color %>">
24
- <div class="card-header">
25
- <div class="row align-items-center">
26
- <div class="col-12 col-md-auto">
27
- <h3 class="card-title">
28
- <strong>
29
- <i class="fa fa<%= object.class.icon %>"></i>
30
- Visualizar <%=t object.class.model_name.human %>
31
- </strong>
32
- </h3>
33
- </div>
34
- <div class="col-12 col-md mr-auto">
35
- <% if can? :manage, object %>
36
- <%= render "shared/btn_action_links", size: "xs", object: object, labels: true, hide: hide, new_color: new_color, edit_color: edit_color, delete_color: delete_color %>
37
- <% end %>
38
- </div>
23
+ <div class="card border-0 border-top border-4 border-<%= color %> shadow-sm mb-0 rounded-bottom-0">
24
+ <div class="card-header bg-body-tertiary border-bottom-0">
25
+ <div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
26
+ <h5 class="mb-0">
27
+ <i class="<%= object.class.icon %>"></i>
28
+ <%= t object.class.model_name.human %>
29
+ </h5>
39
30
  </div>
40
31
  </div>
41
32
  </div>
42
33
  <% end %>
34
+
43
35
  <% if lookup_context.find_all("#{object.class.model_name.plural}/_#{object.class.model_name.singular}").any? %>
44
- <%= render object %>
36
+ <div class="mb-3"><%= render object %></div>
45
37
  <% else %>
46
- <div class="card card-outline card-<%= card_color %>">
47
- <div class="card-body p-0">
48
- <table class="table table-bordered table-striped p-0">
38
+ <div class="card shadow-sm border-0 rounded-0 rounded-bottom mb-3">
39
+ <div class="card-body p-1" data-controller="bootstrap-table">
40
+ <table class="table table-striped table-hover align-middle mb-0">
49
41
  <thead>
50
42
  <tr>
51
- <th width='15%'>Campo</th>
43
+ <th data-width="20" data-width-unit="%">Campo</th>
52
44
  <th>Valor</th>
53
45
  </tr>
54
46
  </thead>
55
- <% show_columns.each do |column| %>
56
- <% unless /_currency$/ =~ column %>
57
- <tr>
58
- <th>
59
- <% if /_id$/ =~ column && object.respond_to?(column.split('_id')[0]) %>
60
- <%= object.class.human_attribute_name(column.split("_id")[0]) %>
61
- <% elsif /_cents$/ =~ column %>
62
- <%= object.class.human_attribute_name(column.split("_cents")[0]) %>
63
- <% else %>
64
- <%= object.class.human_attribute_name(column) %>
65
- <% end %>
66
- </th>
67
- <td>
68
- <% if column.to_s.include?('.') %>
69
- <% o = object %>
70
- <% column.split('.').each{|m| o = o.send(m) if o } %>
71
- <%= o %>
72
- <% elsif object.send(column).class == Date or
73
- object.send(column).class == DateTime or
74
- object.send(column).class == ActiveSupport::TimeWithZone or
75
- object.send(column).class == Time %>
76
- <%=l object.send(column) %>
77
- <% elsif object.send(column).respond_to? 'attached?' %>
78
- <% if object.send(column).class == ActiveStorage::Attached::One %>
79
- <% if object.send(column).attached? %>
80
- <%= link_to rails_blob_path(object.send(column)), class: "btn btn-#{card_color} btn-xs", target: '_blank' do %>
81
- <i class="fa fa-file-o"></i> Arquivo
47
+
48
+ <tbody>
49
+ <% show_columns.each do |column| %>
50
+ <% value = object.send(column) %>
51
+
52
+ <% unless /_currency$/ =~ column %>
53
+ <tr>
54
+ <th class="fw-semibold">
55
+ <% if /_id$/ =~ column && object.respond_to?(column.split('_id')[0]) %>
56
+ <%= object.class.human_attribute_name(column.split("_id")[0]) %>
57
+ <% elsif /_cents$/ =~ column %>
58
+ <%= object.class.human_attribute_name(column.split("_cents")[0]) %>
59
+ <% else %>
60
+ <%= object.class.human_attribute_name(column) %>
61
+ <% end %>
62
+ </th>
63
+
64
+ <td>
65
+ <% if column.to_s.include?('.') %>
66
+ <% o = object %>
67
+ <% column.split('.').each{|m| o = o.send(m) if o } %>
68
+ <%= o %>
69
+ <% elsif value.is_a?(Date) ||
70
+ value.is_a?(Time) ||
71
+ value.is_a?(DateTime) ||
72
+ value.is_a?(ActiveSupport::TimeWithZone) %>
73
+ <%= l value %>
74
+ <% elsif value.respond_to? 'attached?' %>
75
+ <% if value.attached? %>
76
+ <%= link_to rails_blob_path(value),
77
+ class: "btn btn-outline-#{color} btn-sm",
78
+ target: '_blank' do %>
79
+ <i class="fa fa-file"></i>
80
+ <span class="d-none d-md-inline">Arquivo</span>
82
81
  <% end %>
83
82
  <% end %>
83
+ <% elsif /_id$/ =~ column && object.respond_to?(column.split('_id')[0]) %>
84
+ <%= object.send(column.split("_id")[0])&.name %>
85
+ <% elsif /_cents$/ =~ column %>
86
+ <%= number_to_currency object.send(column.split("_cents")[0]) %>
87
+ <% else %>
88
+ <%= value %>
84
89
  <% end %>
85
- <% elsif /_id$/ =~ column && object.respond_to?(column.split('_id')[0]) %>
86
- <%= object.send(column.split("_id")[0])&.name %>
87
- <% elsif /_cents$/ =~ column %>
88
- <%= number_to_currency object.send(column.split("_cents")[0]) %>
89
- <% else %>
90
- <%= object.send(column) %>
91
- <% end %>
92
- </td>
93
- </tr>
90
+ </td>
91
+ </tr>
92
+ <% end %>
94
93
  <% end %>
95
- <% end %>
94
+ </tbody>
96
95
  </table>
97
96
  </div>
98
97
  </div>
98
+ <% end %>
99
99
 
100
- <% if relationships.any? %>
101
- <% relationships.each do |key, value| %>
102
- <% if object.association(key.to_s).kind_of?(ActiveRecord::Associations::BelongsToAssociation) ||
103
- object.association(key.to_s).kind_of?(ActiveRecord::Associations::HasOneAssociation) %>
104
- <div class="box card-<%= card_color %>">
105
- <div class="card-header with-border">
106
- <h3 class="card-title">
107
- <%= Kernel.const_get(key.to_s.singularize.camelize).model_name.human %>
108
- </h3>
109
- </div>
110
- <div class="card-body p-0">
111
- <table class="table table-bordered table-striped p-0">
112
- <% unless hide_table_header %>
113
- <thead>
114
- <tr>
115
- <th width='15%'><%= t('field') %></th>
116
- <th><%= t('value') %></th>
117
- </tr>
118
- </thead>
119
- <% end %>
120
- <tbody>
121
- <% value.each do |v| %>
122
- <tr>
123
- <td>
124
- <strong>
125
- <% if /_id$/ =~ v && Kernel.const_get(key.to_s.singularize.camelize).new.respond_to?(v.split('_id')[0]) %>
126
- <%= Kernel.const_get(key.to_s.singularize.camelize).human_attribute_name(v.split('_id')[0]) %>
127
- <% elsif /_cents$/ =~ v %>
128
- <%= Kernel.const_get(key.to_s.singularize.camelize).human_attribute_name(v.split("_cents")[0]) %>
129
- <% else %>
130
- <%= Kernel.const_get(key.to_s.singularize.camelize).human_attribute_name(v) %>
131
- <% end %>
132
- </strong>
133
- </td>
134
- <td>
135
- <% if object.send(key.to_s).send(v).class == Date or
136
- object.send(key.to_s).send(v).class == DateTime or
137
- object.send(key.to_s).send(v).class == ActiveSupport::TimeWithZone or
138
- object.send(key.to_s).send(v).class == Time %>
139
- <%=l object.send(key.to_s).send(v) %>
140
- <% elsif /_id$/ =~ v && object.send(key.to_s).respond_to?(v.split('_id')[0]) %>
141
- <%= object.send(key.to_s).send(v.split("_id")[0])&.name %>
142
- <% elsif /_cents$/ =~ v %>
143
- <%= number_to_currency object.send(key.to_s).send(v.split("_cents")[0]) %>
144
- <% else %>
145
- <%= object.send(key.to_s).send(v) %>
146
- <% end %>
147
- </td>
148
- </tr>
149
- <% end %>
150
- </tbody>
151
- </table>
152
- </div>
153
- </div>
154
- <% else %>
155
- <div class="box card-<%= card_color %>">
156
- <div class="card-header with-border">
157
- <h3 class="card-title">
158
- <%= Kernel.const_get(key.to_s.singularize.camelize).model_name.human.pluralize %>
159
- </h3>
160
- </div>
161
- <div class="card-body p-0">
162
- <table class="table table-bordered table-striped p-0">
163
- <thead>
164
- <tr>
165
- <% value.each do |v| %>
166
- <th>
167
- <% if /_id$/ =~ v && Kernel.const_get(key.to_s.singularize.camelize).new.respond_to?(v.split('_id')[0]) %>
168
- <%= Kernel.const_get(key.to_s.singularize.camelize).human_attribute_name(v.split('_id')[0]) %>
169
- <% else %>
170
- <%= Kernel.const_get(key.to_s.singularize.camelize).human_attribute_name(v) %>
171
- <% end %>
172
- </th>
173
- <% end %>
174
- </tr>
175
- </thead>
176
- <tbody>
177
- <% object.send(key.to_s).each do |r| %>
178
- <tr>
179
- <% value.each do |v| %>
180
- <td>
181
- <% if v.include?('.') %>
182
- <% o = r %>
183
- <% v.split('.').each{|m| o = o.send(m) if o } %>
184
- <%= o %>
185
- <% elsif r.send(v).class == Date or
186
- r.send(v).class == DateTime or
187
- r.send(v).class == ActiveSupport::TimeWithZone or
188
- r.send(v).class == Time %>
189
- <%=l r.send(v) %>
190
- <% elsif r.send(v).respond_to? 'attached?' %>
191
- <% if r.send(v).class == ActiveStorage::Attached::One %>
192
- <% if r.send(v).attached? %>
193
- <%= link_to rails_blob_path(r.send(v)), class: "btn btn-#{card_color} btn-xs", target: '_blank' do %>
194
- <i class="fa fa-file-o"></i> Arquivo
195
- <% end %>
196
- <% end %>
197
- <% end %>
198
- <% elsif /_id$/ =~ v && r.respond_to?(v.split('_id')[0]) %>
199
- <%= r.send(v.split("_id")[0])&.name %>
200
- <% else %>
201
- <%= r.send(v) %>
202
- <% end %>
203
- </td>
204
- <% end %>
205
- </tr>
206
- <% end %>
207
- </tbody>
208
- </table>
209
- </div>
210
- </div>
211
- <% end %>
212
- <% end %>
100
+ <div class="card-footer">
101
+ <% unless hide.include? "bottom_bar" %>
102
+ <%= render "shared/btn_action_links",
103
+ object: object,
104
+ labels: true,
105
+ hide: hide,
106
+ edit_color: edit_color,
107
+ delete_color: delete_color,
108
+ size: buttons_size %>
213
109
  <% end %>
214
- <% end %>
215
- <% unless hide.include? "bottom_bar" %>
216
- <%= render "shared/btn_action_links", object: object, labels: true, hide: hide, new_color: new_color, edit_color: edit_color, delete_color: delete_color %>
217
- <% end %>
110
+ </div>
@@ -0,0 +1,21 @@
1
+ <% title ||= '' %>
2
+ <% size ||= 'lg' %>
3
+ <% hide_header ||= false %>
4
+ <% modal_frame_tag ||= "turbo_modal" %>
5
+
6
+ <%= turbo_frame_tag modal_frame_tag do %>
7
+ <% unless hide_header %>
8
+ <div class="modal-header">
9
+ <h5 class="modal-title"><%= title %></h5>
10
+
11
+ <button
12
+ type="button"
13
+ class="btn-close"
14
+ data-bs-dismiss="modal"
15
+ aria-label="Fechar"
16
+ ></button>
17
+ </div>
18
+ <% end %>
19
+
20
+ <div class="modal-body p-0"><%= yield %></div>
21
+ <% end %>
data/lib/zutils/engine.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  module Zutils
2
2
  class Engine < Rails::Engine
3
- # Make it a Rails Engine
3
+ config.to_prepare do
4
+ ActionView::Base.include(Zutils::Helpers)
5
+ end
4
6
  end
5
7
  end
@@ -0,0 +1,173 @@
1
+ module Zutils
2
+ module Helpers
3
+ def eval_with_rescue(code)
4
+ eval(code)
5
+ rescue Exception => e
6
+ "error"
7
+ end
8
+
9
+ # ── Flash ────────────────────────────────────────────────────────────
10
+ def bootstrap_flash(options = {})
11
+ flash_messages = []
12
+
13
+ begin
14
+ object_name = controller_name.singularize
15
+ obj = instance_variable_get("@#{object_name}")
16
+
17
+ if obj && obj.respond_to?(:errors) && obj.errors.size > 0
18
+ msg = if obj.errors[:base].present?
19
+ obj.errors[:base]
20
+ else
21
+ I18n.t("helpers.links.#{action_name}_error",
22
+ model: Kernel.const_get(object_name.camelize).model_name.human, default: "erro de validação")
23
+ end
24
+ flash[:error] = msg if msg.present?
25
+ end
26
+ rescue
27
+ # ignore
28
+ end
29
+
30
+ flash.each do |type, message|
31
+ message = Array(message).select(&:presence)
32
+ next if message.empty?
33
+ data = case type.to_s
34
+ when "success", "notice"
35
+ { title: options[:success_title] || "Sucesso!", icon: options[:success_icon] || "fas fa-check", type: :success, kind: "success" }
36
+ when "error"
37
+ { title: options[:error_title] || "Erro!", icon: options[:error_icon] || "fas fa-ban", type: :danger, kind: "error" }
38
+ when "alert"
39
+ { title: options[:alert_title] || "Alerta!", icon: options[:alert_icon] || "fas fa-exclamation-triangle", type: :warning, kind: "warning" }
40
+ else
41
+ { title: options[:info_title] || "Aviso", icon: options[:info_icon] || "fas fa-info", type: :info, kind: "info" }
42
+ end
43
+
44
+ next unless data
45
+
46
+ tag_options = {
47
+ class: "alert alert-#{data[:type]} alert-dismissible d-flex align-items-center",
48
+ role: "alert",
49
+ data: { kind: data[:kind], text: message.first.to_s }
50
+ }
51
+
52
+ close_button = content_tag(:button, "", type: "button", class: "btn-close",
53
+ "data-bs-dismiss": "alert", "aria-label": "Fechar")
54
+
55
+ icon = content_tag(:i, "", class: "#{data[:icon]} me-2")
56
+ title = content_tag(:strong, data[:title])
57
+ text = safe_join([title, " ", sanitize(Array(message).first.to_s.gsub("|", "<br>"), tags: %w[br], attributes: [])])
58
+
59
+ content = safe_join([close_button, icon, text])
60
+ flash_messages << content_tag(:div, content, tag_options)
61
+ end
62
+
63
+ flash[:error] = nil
64
+ safe_join(flash_messages, "\n")
65
+ end
66
+
67
+ def render_turbo_stream_flash_messages(local: false)
68
+ partial = local ? "shared/flash" : "shared/flash"
69
+ turbo_stream.prepend(local ? "localflash" : "flash", partial: partial)
70
+ end
71
+
72
+ def render_turbo_stream_flash_messages_only_toastr
73
+ turbo_stream.prepend "flash", partial: "shared/flash_only_toastr"
74
+ end
75
+
76
+ # ── DOM helpers ──────────────────────────────────────────────────────
77
+ def nested_dom_id(*args)
78
+ args.map { |arg| arg.respond_to?(:to_key) ? dom_id(arg) : arg.to_s }.join("_")
79
+ end
80
+
81
+ # ── Menu ─────────────────────────────────────────────────────────────
82
+ def menu_activated?(menu_item)
83
+ check = ->(test) { eval_with_rescue(test) rescue false }
84
+
85
+ check.call(menu_item.dig(:active_test)) ||
86
+ menu_item.dig(:children).to_a.any? { |c| check.call(c[:active_test]) } ||
87
+ menu_item.dig(:children).to_a.flat_map { |c| c.dig(:children).to_a }.any? { |c| check.call(c[:active_test]) }
88
+ end
89
+
90
+ # ── Display helpers (BS5) ────────────────────────────────────────────
91
+ def display(object, field, options = {})
92
+ icon = options[:icon] || "fa fa-stream"
93
+ title = options[:title] || object.class.human_attribute_name(field)
94
+ value = options[:value] || interactive_send(object, field)
95
+ hide_title = options[:hide_title] || false
96
+
97
+ parts = []
98
+ unless hide_title
99
+ parts << content_tag(:strong, tag.i("", class: icon) + " #{title}")
100
+ end
101
+ parts << content_tag(:p, value.presence || "-", class: "text-body-secondary")
102
+ safe_join(parts, "\n")
103
+ end
104
+
105
+ def display_boolean(object, field, options = {})
106
+ bool_value = options[:value] || interactive_send(object, field)
107
+ label = bool_value ? (options[:true_label] || "Sim") : (options[:false_label] || "Não")
108
+ css = bool_value ? "bg-success" : "bg-danger"
109
+
110
+ badge = content_tag(:span, label, class: "badge #{css}")
111
+ display(object, field, options.merge(value: badge))
112
+ end
113
+
114
+ def display_boolean_icon(object, field, options = {})
115
+ bool_value = options[:value] || interactive_send(object, field)
116
+ title = options[:title] || object.class.human_attribute_name(field)
117
+ icon_true = options[:icon_true] || options[:icon] || "fa fa-thumbs-up"
118
+ icon_false = options[:icon_false] || options[:icon] || "fa fa-thumbs-down"
119
+ css = bool_value ? "bg-success" : "bg-danger"
120
+
121
+ icon = bool_value ? tag.i("", class: icon_true) : tag.i("", class: icon_false)
122
+ content = content_tag(:span, icon, class: "badge #{css}",
123
+ data: { "bs-toggle": "tooltip", "bs-title": "#{title}: #{bool_value ? "Sim" : "Não"}" })
124
+ end
125
+
126
+ def interactive_send(obj, field)
127
+ field.to_s.split(".").each { |m| obj = obj.send(m) if obj }
128
+
129
+ case obj
130
+ when Date, Time, DateTime, ActiveSupport::TimeWithZone
131
+ I18n.l(obj)
132
+ else
133
+ obj
134
+ end
135
+ end
136
+
137
+ # ── Section / Header ─────────────────────────────────────────────────
138
+ def section_header(title, icon: "fa-info-circle", color: "primary", border: true)
139
+ content_tag(:h5, class: "text-uppercase small text-#{color} fw-bold mb-4 #{"border-bottom" if border} pb-2") do
140
+ concat tag.i(class: "fa #{icon} me-2")
141
+ concat title
142
+ end
143
+ end
144
+
145
+ def smart_date_tag(date, icon: nil, label: nil, text_class: "text-body")
146
+ return "-" if date.blank?
147
+
148
+ day_month = l(date, format: "%d %b")
149
+ if date.year > Date.today.year
150
+ year_tag = " <small class='opacity-75' style='font-size: 0.85em;'>#{date.year}</small>".html_safe
151
+ else
152
+ year_tag = ""
153
+ end
154
+
155
+ parts = []
156
+ parts << tag.i("", class: "bi bi-#{icon} me-1 text-muted") if icon.present?
157
+ parts << content_tag(:span, "#{label}:", class: "small text-muted me-1") if label.present?
158
+ parts << "#{day_month}#{year_tag}".html_safe
159
+
160
+ content_tag(:span, safe_join(parts), class: text_class)
161
+ end
162
+
163
+ def format_date(value)
164
+ return if value.blank?
165
+
166
+ Date.iso8601(value.to_s).strftime("%d/%m/%Y")
167
+ rescue
168
+ Date.parse(value.to_s).strftime("%d/%m/%Y")
169
+ rescue
170
+ value
171
+ end
172
+ end
173
+ end
@@ -1,3 +1,3 @@
1
1
  module Zutils
2
- VERSION = "3.0.7"
2
+ VERSION = "4.0.0"
3
3
  end
data/lib/zutils.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require "zutils/version"
2
2
  require "zutils/engine"
3
+ require "zutils/helpers"
3
4
 
4
5
  module Zutils
5
- # Your code goes here...
6
- end
6
+ end