From 5b60be4d2baa55bbd40e0526c9b4fc7104a80922 Mon Sep 17 00:00:00 2001 From: Bianca Date: Mon, 1 Sep 2025 13:34:23 -0300 Subject: [PATCH 01/31] =?UTF-8?q?Reposicionando=20o=20c=C3=B3digo=20js=20n?= =?UTF-8?q?o=20form=20de=20quest=C3=B5es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/exams/form/_info.html.haml | 72 ++++++++++++++-------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/app/views/exams/form/_info.html.haml b/app/views/exams/form/_info.html.haml index 7028be17d..3fd249d7f 100644 --- a/app/views/exams/form/_info.html.haml +++ b/app/views/exams/form/_info.html.haml @@ -1,38 +1,3 @@ -= f.input :name -= f.input :description, as: :ckeditor - -= f.simple_fields_for :schedule do |s| - .schedule_dates - .left - = s.input :start_date, as: :string, input_html: { id: 'exam_start_date', value: l(f.object.schedule.start_date.try(:to_date) || Date.today, format: :datepicker) } - .right_form - = s.input :end_date, as: :string, input_html: { id: 'exam_end_date', value: l(f.object.schedule.end_date.try(:to_date) || Date.today, format: :datepicker) } - - .schedule_dates.hour - .left - = f.input :start_hour - .right_form - = f.input :end_hour - -= f.input :duration, input_html: { :"data-tooltip" => t('exams.form.info.duration') } - -.label - = f.label :immediate_result_release, :'data-tooltip' => t('.immediate_result_release_explain') - = f.input :immediate_result_release, as: :boolean, label: false - = link_to (image_tag "#{f.object.immediate_result_release ? 'released' : 'rejected'}.png"), "#void", onclick: 'change(this, ["#exam_result_release", ".exam_result_release", "#exam_attempts", ".exam_attempts", "#exam_attempts_correction", ".exam_attempts_correction"])', onkeydown: 'click_on_keypress(event, this)', :'data-tooltip' => "", :'data-id' => 'immediate_result_release', :'data-active' => t('.immediate_result_release_activated'), :'data-not-active' => t('.immediate_result_release_deactivated'), :'data-val'=> f.object.immediate_result_release - - if @exam.errors[:immediate_result_release].any? - %span.field_with_errors.error= @exam.errors[:immediate_result_release].first - -= f.input :result_release, as: :string, label: t('exams.form.info.result_release') + " (?)", placeholder: t('assignment_webconferences.form.date_placeholder'), label_html: { :'data-tooltip' => t('.result_release_explain')}, input_html: {value: (l(f.object.result_release, format: :mask_with_time_form) rescue '')} - -= render 'groups/codes' - -.form-actions.right_buttons - = button_tag t(:cancel), :type => 'button', :onclick => "jQuery.fancybox.close()", class: 'btn btn_default btn_lightbox cancel', onkeypress: 'click_on_keypress(event, this)' - = button_tag t('.continue'), :type => 'button', :onclick => "go_to_config(event)", class: 'btn btn_main btn_lightbox', id: '_continue', onkeypress: 'go_to_config(event)' - -= javascript_include_tag 'ckeditor/init', 'jquery-ui-timepicker-addon', 'ip' - :javascript $(function(){ @@ -111,3 +76,40 @@ $('#dot-control').removeClass('active'); } } + + += f.input :name += f.input :description, as: :ckeditor + += f.simple_fields_for :schedule do |s| + .schedule_dates + .left + = s.input :start_date, as: :string, input_html: { id: 'exam_start_date', value: l(f.object.schedule.start_date.try(:to_date) || Date.today, format: :datepicker) } + .right_form + = s.input :end_date, as: :string, input_html: { id: 'exam_end_date', value: l(f.object.schedule.end_date.try(:to_date) || Date.today, format: :datepicker) } + + .schedule_dates.hour + .left + = f.input :start_hour + .right_form + = f.input :end_hour + += f.input :duration, input_html: { :"data-tooltip" => t('exams.form.info.duration') } + +.label + = f.label :immediate_result_release, :'data-tooltip' => t('.immediate_result_release_explain') + = f.input :immediate_result_release, as: :boolean, label: false + = link_to (image_tag "#{f.object.immediate_result_release ? 'released' : 'rejected'}.png"), "#void", onclick: 'change(this, ["#exam_result_release", ".exam_result_release", "#exam_attempts", ".exam_attempts", "#exam_attempts_correction", ".exam_attempts_correction"])', onkeydown: 'click_on_keypress(event, this)', :'data-tooltip' => "", :'data-id' => 'immediate_result_release', :'data-active' => t('.immediate_result_release_activated'), :'data-not-active' => t('.immediate_result_release_deactivated'), :'data-val'=> f.object.immediate_result_release + - if @exam.errors[:immediate_result_release].any? + %span.field_with_errors.error= @exam.errors[:immediate_result_release].first + += f.input :result_release, as: :string, label: t('exams.form.info.result_release') + " (?)", placeholder: t('assignment_webconferences.form.date_placeholder'), label_html: { :'data-tooltip' => t('.result_release_explain')}, input_html: {value: (l(f.object.result_release, format: :mask_with_time_form) rescue '')} + += render 'groups/codes' + +.form-actions.right_buttons + = button_tag t(:cancel), :type => 'button', :onclick => "jQuery.fancybox.close()", class: 'btn btn_default btn_lightbox cancel', onkeypress: 'click_on_keypress(event, this)' + = button_tag t('.continue'), :type => 'button', :onclick => "go_to_config(event)", class: 'btn btn_main btn_lightbox', id: '_continue', onkeypress: 'go_to_config(event)' + += javascript_include_tag 'ckeditor/init', 'jquery-ui-timepicker-addon', 'ip' + From 6aa310d7055da41376b50ce22904805157f9eb18 Mon Sep 17 00:00:00 2001 From: biancastephani Date: Mon, 8 Sep 2025 15:58:25 -0300 Subject: [PATCH 02/31] Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Atualização da equipe --- README.md | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/README.md b/README.md index 8b1378917..4b5f90e8b 100644 --- a/README.md +++ b/README.md @@ -1 +1,59 @@ +O Solar 2.0 foi idealizado e cresceu ao longo dos anos com a participação de muita gente. +Atualmente, nossa equipe é composta por: +**Setor de Tecnologias Digitais** +Paulo de Tarso Pequeno Filho (chefe) +Hellanio Ferreira da Costa (suplente) + +**Equipe de Desenvolvimento** +Me. Bianca Stephani Barone Martins (coordenação) +Jailson Pinheiro Lima +José Neilson Viana do Nascimento +Me. Mirele Rodrigues Fernandes +Paula Tatyene Silva Dantas + +Mas todas as pessoas abaixo já participaram da sua idealização e construção. + +**1. Idealização e arquitetura** +Prof. Wellington Wagner Ferreira Sarmento +Me. Patrícia de Sousa Paula +Prof. Dr. Henrique Sérgio Lima Pequeno +Prof. Dr. George Allan Gomes +Ba. Humberto Osório Ovídio Gomes +Prof. Dr. Mauro Cavalcante Pequeno + +**2. Consultoria de Metodologias Ágeis** +Me. Ari Amaral +Christiano Milfont + +**3. Modelagem e documentação** +Lisboa Coutinho Júnior (Vice-Coordenação do Centro de Produção II) + +**4. Concepção visual e usabilidade: Célula de Usabilidade e Design do IUVI** +Profa. Dra. Cátia M. Harriman (coordenadora) +Dr. Andrei Bosco Bezerra Torres +Alexia Bivar +Camila Paiva +Dra. Joelma Peixoto +Katryne Rabelo +Renata Gadelha +Yohana Almeida + +**5. Equipe de Análise e Desenvolvimento** +Dr. Andrei Bosco Bezerra Torres +Afonsina Soares +Bruno Neves +Caio Viktor Ávila +Cristiano Lima +Danniel Albuquerque Araújo +Davi Linhares +Emanoel Lopes +Henrique Bruno +Humberto O. O. Gomes +Dra. Joelma Peixoto +Jordy Gomes +Laerte Neto +Me. Patrícia de Sousa Paula +Pedro Ivo Freire Aragão +Ricardo Palácio +Wedson Lima From d23dead077e6e72c926682929e1a98400fa26c2e Mon Sep 17 00:00:00 2001 From: biancastephani Date: Tue, 9 Sep 2025 15:54:57 -0300 Subject: [PATCH 03/31] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4b5f90e8b..b7c9679a0 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ O Solar 2.0 foi idealizado e cresceu ao longo dos anos com a participação de m Atualmente, nossa equipe é composta por: **Setor de Tecnologias Digitais** -Paulo de Tarso Pequeno Filho (chefe) +Me. Paulo de Tarso Pequeno Filho (chefe) Hellanio Ferreira da Costa (suplente) **Equipe de Desenvolvimento** @@ -15,11 +15,11 @@ Paula Tatyene Silva Dantas Mas todas as pessoas abaixo já participaram da sua idealização e construção. **1. Idealização e arquitetura** -Prof. Wellington Wagner Ferreira Sarmento +Prof. Me. Wellington Wagner Ferreira Sarmento Me. Patrícia de Sousa Paula Prof. Dr. Henrique Sérgio Lima Pequeno Prof. Dr. George Allan Gomes -Ba. Humberto Osório Ovídio Gomes +Humberto Osório Ovídio Gomes Prof. Dr. Mauro Cavalcante Pequeno **2. Consultoria de Metodologias Ágeis** From d625d7c72c750055c830edf38e8f8accb38fc090 Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Mon, 15 Dec 2025 14:25:20 -0300 Subject: [PATCH 04/31] Adiciona recurso para FAQ (Perguntas e Respostas) Cria o recurso res247 que define o controller 'faqs' com a action 'index' para listar perguntas e respostas. --- test/fixtures/resources.yml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/test/fixtures/resources.yml b/test/fixtures/resources.yml index 05ad5a295..dd8fe6499 100644 --- a/test/fixtures/resources.yml +++ b/test/fixtures/resources.yml @@ -1232,4 +1232,23 @@ res244: id: 244 controller: 'discussions' action: 'download_files' - description: 'Download dos arquivos (enunciado) de um fórum' \ No newline at end of file + description: 'Download dos arquivos (enunciado) de um fórum' + +res245: + id: 245 + controller: "schedule_events" + action: 'list' + description: 'Edição - Lista de eventos' + +res246: + id: 246 + controller: "schedule_events" + action: 'index' + description: 'Lista de eventos' + +#FAQ - Perguntas e respostas +res247: + id: 247 + controller: 'faqs' + action: 'index' + description: 'Lista de perguntas e respostas' From 052cd033cc1cf35a6260cb9db22a80b0946cf7cf Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Mon, 15 Dec 2025 14:25:36 -0300 Subject: [PATCH 05/31] Adiciona menu administrativo para FAQ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cria o menu admin_faq no painel administrativo com referência ao recurso 247 (FAQ), permitindo acesso à funcionalidade de perguntas e respostas. --- test/fixtures/menus.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/fixtures/menus.yml b/test/fixtures/menus.yml index 0969159e2..9e784e1d5 100644 --- a/test/fixtures/menus.yml +++ b/test/fixtures/menus.yml @@ -244,6 +244,13 @@ admin_reports: order: 14 resource_id: 226 +admin_faq: + id: 615 + parent_id: 60 + name: menu_admin_faq + order: 15 + resource_id: 247 + admin_notifications: id: 804 parent_id: 60 From 3ecfb188ffdd3442bf853b6652c2d2ef5afaf637 Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Mon, 15 Dec 2025 14:25:57 -0300 Subject: [PATCH 06/31] =?UTF-8?q?Adiciona=20permiss=C3=B5es=20de=20adminis?= =?UTF-8?q?trador=20para=20FAQ?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cria a permissão faq1 que permite ao perfil de administrador (profile_id: 6) acessar o recurso de FAQ (resource_id: 247). --- test/fixtures/permissions_resources.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/fixtures/permissions_resources.yml b/test/fixtures/permissions_resources.yml index e4f8b8d3b..3bee7105e 100644 --- a/test/fixtures/permissions_resources.yml +++ b/test/fixtures/permissions_resources.yml @@ -2593,4 +2593,11 @@ enunciation_assignment_file_editor_uab: enunciation_assignment_file_editor: profile_id: 5 resource_id: 34 + per_id: true + +# FAQ - Perguntas e Respostas + +faq1: + profile_id: 6 + resource_id: 247 per_id: true \ No newline at end of file From 2320ee690814bd9d66f308d02960a9310b92fc13 Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Wed, 17 Dec 2025 13:17:30 -0300 Subject: [PATCH 07/31] Adiciona modelos e migrations para sistema de FAQ --- app/models/application_record.rb | 3 + app/models/faq.rb | 117 ++++++++++++++++++ app/models/faq_translation.rb | 21 ++++ db/migrate/20251216141303_create_faqs.rb | 10 ++ .../20251216141537_create_faq_translations.rb | 12 ++ 5 files changed, 163 insertions(+) create mode 100644 app/models/application_record.rb create mode 100644 app/models/faq.rb create mode 100644 app/models/faq_translation.rb create mode 100644 db/migrate/20251216141303_create_faqs.rb create mode 100644 db/migrate/20251216141537_create_faq_translations.rb diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 000000000..71a1a03cc --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end \ No newline at end of file diff --git a/app/models/faq.rb b/app/models/faq.rb new file mode 100644 index 000000000..ec3bfa8b6 --- /dev/null +++ b/app/models/faq.rb @@ -0,0 +1,117 @@ +class Faq < ApplicationRecord + has_many :faq_translations, dependent: :destroy + #Permite salvar FAQ + traduções em unm único formulário + accepts_nested_attributes_for :faq_translations, allow_destroy: true + validates :order, presence: true, numericality: {only_integer: true} + validates :active, inclusion: {in: [true, false]} + + before_save :reorder_faqs_if_order_changed + + default_scope {order(:order)} + + scope :faq_translations, -> { includes(:faq_translations).where(active: true) } + + # Retorna pergunta no idioma atual + def question + translation_for(I18n.locale)&.question + end + + # Retorna resposta no idioma atual + def answer + translation_for(I18n.locale)&.answer + end + + # Busca tradução para um idioma específico + def translation_for(locale) + faq_translations.find_by(locale: locale.to_s) + end + + # Helpers para formulário admin (garante que PT e EN existem) + def translation_pt + faq_translations.find_or_initialize_by(locale: 'pt_BR') + end + + def translation_en + faq_translations.find_or_initialize_by(locale: 'en_US') + end + + private + + # ========================================================================= + # REORDENAÇÃO AUTOMÁTICA + # ========================================================================= + # Este método é executado automaticamente ANTES de salvar um FAQ (before_save) + # + # OBJETIVO: Quando você muda a ordem de um FAQ, os outros FAQs são + # automaticamente reorganizados para "abrir espaço" na nova posição. + # + # EXEMPLO: + # FAQs atuais: ordem 1, 2, 3, 4, 5 + # Você edita FAQ com ordem 5 e muda para ordem 2 + # Resultado automático: + # - FAQ que era ordem 2 → vira ordem 3 + # - FAQ que era ordem 3 → vira ordem 4 + # - FAQ que era ordem 4 → vira ordem 5 + # - FAQ editado (era 5) → fica ordem 2 + # ========================================================================= + def reorder_faqs_if_order_changed + # order_changed? → retorna true se o campo "order" foi modificado + # persisted? → retorna true se o registro já existe no banco (não é novo) + # Ou seja: só reordena em UPDATES, não em CREATES (novos FAQs) + return unless order_changed? && persisted? + + # order_was → valor ANTIGO da ordem (antes da mudança) + # order → valor NOVO da ordem (depois da mudança) + old_order = order_was + new_order = order + + # Se os valores são iguais, não precisa reordenar nada + return if old_order == new_order + + # ----------------------------------------------------------------------- + # IMPORTANTE: Remove temporariamente o default_scope + # ----------------------------------------------------------------------- + # O model Faq tem um default_scope { order(:order) } que sempre ordena + # os resultados. Ao usar unscoped, removemos essa ordenação temporariamente + # para fazer as queries de UPDATE sem interferência. + # ----------------------------------------------------------------------- + Faq.unscoped do + + # CASO 1: Moveu PARA CIMA (ordem diminuiu) + # Exemplo: mudou de ordem 5 para ordem 2 + if new_order < old_order + # Precisamos "empurrar para baixo" (incrementar +1) todos os FAQs + # que estão entre a nova ordem e a ordem antiga + # + # No exemplo (5 → 2): + # - FAQs com ordem >= 2 E ordem < 5 precisam incrementar + # - Ou seja: ordem 2, 3, 4 viram 3, 4, 5 + # + # WHERE: + # id != ? → exclui o próprio FAQ que está sendo editado + # order >= ? → maior ou igual à nova ordem (2) + # order < ? → menor que a ordem antiga (5) + Faq.where('id != ? AND "order" >= ? AND "order" < ?', id, new_order, old_order) + .update_all('"order" = "order" + 1') # Incrementa +1 (aspas duplas para escapar palavra reservada) + + # CASO 2: Moveu PARA BAIXO (ordem aumentou) + # Exemplo: mudou de ordem 2 para ordem 5 + else + # Precisamos "puxar para cima" (decrementar -1) todos os FAQs + # que estão entre a ordem antiga e a nova ordem + # + # No exemplo (2 → 5): + # - FAQs com ordem > 2 E ordem <= 5 precisam decrementar + # - Ou seja: ordem 3, 4, 5 viram 2, 3, 4 + # + # WHERE: + # id != ? → exclui o próprio FAQ que está sendo editado + # order > ? → maior que a ordem antiga (2) + # order <= ? → menor ou igual à nova ordem (5) + Faq.where('id != ? AND "order" > ? AND "order" <= ?', id, old_order, new_order) + .update_all('"order" = "order" - 1') # Decrementa -1 (aspas duplas para escapar palavra reservada) + end + end + end + +end diff --git a/app/models/faq_translation.rb b/app/models/faq_translation.rb new file mode 100644 index 000000000..dfd0a7cb9 --- /dev/null +++ b/app/models/faq_translation.rb @@ -0,0 +1,21 @@ +class FaqTranslation < ApplicationRecord + belongs_to :faq + + # Validações + # Lista de idiomas suportados (pode adicionar mais conforme necessário) + AVAILABLE_LOCALES = %w[pt_BR en_US es_ES fr_FR de_DE it_IT].freeze + + validates :locale, presence: true, inclusion: { in: AVAILABLE_LOCALES } + validates :question, presence: true + validates :answer, presence: true + + # Garante que não pode ter duas traduções do mesmo idioma para um FAQ + validates :locale, uniqueness: { scope: :faq_id, message: "já existe para este FAQ" } + + # Garante que não pode ter perguntas duplicadas no mesmo idioma (case-insensitive) + validates :question, uniqueness: { + scope: :locale, #verifica dentro do mesmo idioma + case_sensitive: false, #ignora maiusculas e minusculas + message: "já existe neste idioma. Não é permitido perguntas duplicadas." + } +end diff --git a/db/migrate/20251216141303_create_faqs.rb b/db/migrate/20251216141303_create_faqs.rb new file mode 100644 index 000000000..3af0771cc --- /dev/null +++ b/db/migrate/20251216141303_create_faqs.rb @@ -0,0 +1,10 @@ +class CreateFaqs < ActiveRecord::Migration[5.1] + def change + create_table :faqs do |t| + t.integer :order + t.boolean :active + + t.timestamps + end + end +end diff --git a/db/migrate/20251216141537_create_faq_translations.rb b/db/migrate/20251216141537_create_faq_translations.rb new file mode 100644 index 000000000..592b0e84c --- /dev/null +++ b/db/migrate/20251216141537_create_faq_translations.rb @@ -0,0 +1,12 @@ +class CreateFaqTranslations < ActiveRecord::Migration[5.1] + def change + create_table :faq_translations do |t| + t.string :locale + t.string :question + t.text :answer + t.references :faq, foreign_key: true + + t.timestamps + end + end +end From dddf99f01f97bc67a357f554a983bb6e2684d236 Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Wed, 17 Dec 2025 13:18:17 -0300 Subject: [PATCH 08/31] Adiciona controller, views e assets do sistema de FAQ --- app/assets/javascripts/faqs.js.erb | 117 +++++++++++++++++++++++++++++ app/assets/stylesheets/faq.scss | 3 + app/controllers/faqs_controller.rb | 94 +++++++++++++++++++++++ app/views/faqs/_form.html.haml | 46 ++++++++++++ app/views/faqs/edit.html.haml | 6 ++ app/views/faqs/faq.html.haml | 73 ++++++++++++++++++ app/views/faqs/index.html.haml | 23 ++++++ app/views/faqs/new.html.haml | 4 + app/views/faqs/show.html.haml | 37 +++++++++ 9 files changed, 403 insertions(+) create mode 100644 app/assets/javascripts/faqs.js.erb create mode 100644 app/assets/stylesheets/faq.scss create mode 100644 app/controllers/faqs_controller.rb create mode 100644 app/views/faqs/_form.html.haml create mode 100644 app/views/faqs/edit.html.haml create mode 100644 app/views/faqs/faq.html.haml create mode 100644 app/views/faqs/index.html.haml create mode 100644 app/views/faqs/new.html.haml create mode 100644 app/views/faqs/show.html.haml diff --git a/app/assets/javascripts/faqs.js.erb b/app/assets/javascripts/faqs.js.erb new file mode 100644 index 000000000..91b966446 --- /dev/null +++ b/app/assets/javascripts/faqs.js.erb @@ -0,0 +1,117 @@ +<%# @encoding: UTF-8 %> +// FAQ Form - Adicionar/Remover traduções dinamicamente + +$(function() { + + // Verifica se estamos na página de FAQ + if ($('#translations-container').length === 0) { + return; // Não está na página de FAQ, não faz nada + } + + console.log('FAQ Form JavaScript carregado'); + + // Contador para índices únicos dos novos campos + var translationIndex = $('#translations-container .translation-fields').length; + console.log('Número de traduções existentes:', translationIndex); + + // Função para adicionar nova tradução + $('#add-translation').click(function(e) { + e.preventDefault(); + console.log('Botão "Adicionar outra língua" clicado!'); + + // Pega o primeiro campo de tradução como template + var $template = $('#translations-container .translation-fields').first().clone(); + console.log('Template clonado'); + + // Limpa os valores dos campos + $template.find('input[type="text"], textarea, select').val(''); + $template.find('input[type="hidden"]').not('.destroy-flag').val(''); + $template.find('.destroy-flag').val('false'); + + // Atualiza os IDs e nomes dos campos para usar novo índice + $template.find('input, select, textarea').each(function() { + var $field = $(this); + + // Atualiza o name + if ($field.attr('name')) { + var newName = $field.attr('name').replace(/\[\d+\]/, '[' + translationIndex + ']'); + $field.attr('name', newName); + } + + // Atualiza o id + if ($field.attr('id')) { + var newId = $field.attr('id').replace(/_\d+_/, '_' + translationIndex + '_'); + $field.attr('id', newId); + } + }); + + // Atualiza os labels para apontar para os novos IDs + $template.find('label').each(function() { + var $label = $(this); + if ($label.attr('for')) { + var newFor = $label.attr('for').replace(/_\d+_/, '_' + translationIndex + '_'); + $label.attr('for', newFor); + } + }); + + // Adiciona o botão de remover (sempre visível nos novos campos) + if ($template.find('.remove-translation').length === 0) { + $template.prepend( + '' + ); + } + + // Adiciona o novo campo ao container + $('#translations-container').append($template); + + // Incrementa o índice + translationIndex++; + + // Atualiza os botões de remover + updateRemoveButtons(); + }); + + // Função para remover tradução + $(document).on('click', '.remove-translation', function(e) { + e.preventDefault(); + + var $translationField = $(this).closest('.translation-fields'); + var totalFields = $('#translations-container .translation-fields').length; + + // Não permite remover se só tiver 2 campos (mínimo: pt_BR e en_US) + if (totalFields <= 2) { + alert('Você deve manter pelo menos 2 traduções (Português e Inglês).'); + return; + } + + // Se o registro já existe no banco (tem ID), marca para destruição + var $idField = $translationField.find('input[name*="[id]"]'); + if ($idField.length > 0 && $idField.val() !== '') { + $translationField.find('.destroy-flag').val('true'); + $translationField.hide(); + } else { + // Se é um campo novo (não está no banco), apenas remove do DOM + $translationField.remove(); + } + + // Atualiza os botões de remover + updateRemoveButtons(); + }); + + // Função para mostrar/ocultar botões de remover + function updateRemoveButtons() { + var $visibleFields = $('#translations-container .translation-fields:visible'); + var visibleCount = $visibleFields.length; + + if (visibleCount <= 2) { + // Oculta todos os botões de remover se só tiver 2 ou menos campos visíveis + $visibleFields.find('.remove-translation').hide(); + } else { + // Mostra os botões de remover se tiver mais de 2 campos + $visibleFields.find('.remove-translation').show(); + } + } + + // Inicializa: oculta botões de remover se tiver apenas 2 campos + updateRemoveButtons(); +}); diff --git a/app/assets/stylesheets/faq.scss b/app/assets/stylesheets/faq.scss new file mode 100644 index 000000000..9d6d203f8 --- /dev/null +++ b/app/assets/stylesheets/faq.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the Faq controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: https://sass-lang.com/ diff --git a/app/controllers/faqs_controller.rb b/app/controllers/faqs_controller.rb new file mode 100644 index 000000000..6b9cd0818 --- /dev/null +++ b/app/controllers/faqs_controller.rb @@ -0,0 +1,94 @@ +class FaqsController < ApplicationController + #layout 'login', only: [:apresentation] + + # Apenas administradores podem acessar + before_action :require_admin + + # Carrega o FAQ antes destas ações + before_action :set_faq, only: [:show, :edit, :update, :destroy] + + skip_before_action :require_admin, only:[:apresentation] + + # GET /admin/faqs - Lista todos os FAQs + def index + @faqs = Faq.all + end + + # GET /admin/faqs/1 - Mostra um FAQ + def show + end + + # GET /admin/faqs/new - Formulário para novo FAQ + def new + @faq = Faq.new + @faq.faq_translations.build(locale: 'pt_BR') + @faq.faq_translations.build(locale: 'en_US') + end + + # POST /admin/faqs - Cria novo FAQ + def create + @faq = Faq.new(faq_params) + + if @faq.save + redirect_to admin_faq_path(@faq), notice: 'FAQ criado com sucesso.' + else + render :new + end + end + + # GET /admin/faqs/1/edit - Formulário para editar FAQ + def edit + @faq.translation_pt + @faq.translation_en + end + + # PATCH/PUT /admin/faqs/1 - Atualiza FAQ existente + def update + # Atualiza FAQ com dados do formulário + if @faq.update(faq_params) + redirect_to admin_faq_path(@faq), notice: 'FAQ atualizado com sucesso.' + else + render :edit + end + end + + # DELETE /admin/faqs/1 - Deleta FAQ + def destroy + @faq.destroy + redirect_to admin_faq_index_path, notice: 'FAQ removido com sucesso.' + end + + def apresentation + @faq_order = Faq.faq_translations + render :faq + end + + private + # Verifica se usuário é admin (redireciona se não for) + def require_admin + unless current_user.admin? + redirect_to home_path, alert: t(:no_permission) + end + end + + # Carrega FAQ pelo ID (chamado pelo before_action) + def set_faq + @faq = Faq.find(params[:id]) + end + + # Lista de parâmetros permitidos (strong parameters) + # Aceita apenas estes campos do formulário (segurança contra mass assignment) + def faq_params + params.require(:faq).permit( + :order, # Ordem de exibição + :active, # Ativo/Inativo + faq_translations_attributes: [ # Nested attributes das traduções + :id, # ID da tradução (para editar existente) + :locale, # Idioma (pt_BR ou en_US) + :question, # Pergunta + :answer, # Resposta + :_destroy # Flag para deletar tradução (se necessário) + ] + ) + end +end diff --git a/app/views/faqs/_form.html.haml b/app/views/faqs/_form.html.haml new file mode 100644 index 000000000..144405006 --- /dev/null +++ b/app/views/faqs/_form.html.haml @@ -0,0 +1,46 @@ +.new_faq.controller + = simple_form_for @faq, url: (@faq.new_record? ? admin_faq_index_path : admin_faq_path(@faq)), html: { id: 'faq_form' } do |f| + + %h1#lightBoxDialogTitle= t("faqs.#{@faq.persisted? ? 'edit' : 'new'}") + %span.form_requirement= t(:required_fields) + + .form-inputs.block_content.faq_box#basic_info + + -# Campos do FAQ (order e active) - SimpleForm busca labels automaticamente + = f.input :order, required: true, input_html: { value: @faq.order || 0 } + = f.input :active, as: :radio_buttons, required: true + + %hr + %br + + %h2= t('faqs.form.translations') + + -# NESTED FORM: Campos das traduções usando simple_fields_for + #translations-container + = f.simple_fields_for :faq_translations do |translation_form| + .translation-fields{style: "border: solid 1px #ccc; padding: 20px; margin-bottom: 20px; background: #f9f9f9; position: relative;"} + + -# Botão para remover tradução (só aparece se tiver mais de 2) + - if f.object.faq_translations.size > 2 + %button.remove-translation{type: 'button', style: 'position: absolute; top: 10px; right: 10px; background: #d9534f; color: white; border: none; padding: 5px 10px; cursor: pointer; border-radius: 3px;'} + × Remover + + -# SELECT de idioma (pode escolher qualquer um da lista) + = translation_form.input :locale, + collection: [['🇧🇷 Português (Brasil)', 'pt_BR'], ['🇺🇸 English (US)', 'en_US'], ['🇪🇸 Español', 'es_ES'], ['🇫🇷 Français', 'fr_FR'], ['🇩🇪 Deutsch', 'de_DE'], ['🇮🇹 Italiano', 'it_IT']], + required: true + + -# Campos: Pergunta e Resposta - SimpleForm busca labels automaticamente + = translation_form.input :question, required: true + = translation_form.input :answer, as: :text, required: true, input_html: { rows: 5 } + + -# Campo hidden para marcar tradução para destruição + = translation_form.input :_destroy, as: :hidden, input_html: { class: 'destroy-flag' } + + -# Botão para adicionar nova tradução + %button#add-translation.btn.btn_default{type: 'button', style: 'margin-bottom: 20px;'} + + Adicionar outra língua + + .form-actions + = f.button :submit, t('faqs.form.save'), class: 'btn btn_main' + = link_to t('faqs.form.cancel'), admin_faq_index_path, class: 'btn btn_caution' diff --git a/app/views/faqs/edit.html.haml b/app/views/faqs/edit.html.haml new file mode 100644 index 000000000..8f0df2ee1 --- /dev/null +++ b/app/views/faqs/edit.html.haml @@ -0,0 +1,6 @@ += render 'form' + +%br += link_to 'Ver', admin_faq_path(@faq) +\| += link_to 'Voltar', admin_faq_index_path diff --git a/app/views/faqs/faq.html.haml b/app/views/faqs/faq.html.haml new file mode 100644 index 000000000..27b8931fb --- /dev/null +++ b/app/views/faqs/faq.html.haml @@ -0,0 +1,73 @@ += render '/user_sessions/header' +#faq + .content + %br + #link + %a#link-password{href: home_path}= t(:back_home) + + %h2= t(:faq_title) + + .search + = t('faq.search') + = text_field_tag :search_faq, nil, :'aria-label'=>t('faq.search') + + - @faqs.each do |faq| + .question + = link_to '#void', onkeydown: 'click_on_keypress(event, this);', onclick: 'slide_info(this, event)', class: 'title' do + %h4.title= faq.question + .invisible + %p.title_child_first=raw faq.answer + + %h2= t(:video_tutorials) + %p.title_child_first=raw t(:video_tutorials_link) + + %h2= t('faq.file_tutorials') + - if current_user.blank? + %p.title_child_first= raw t('faq.file_tutorials_title', url: tutorials_login_path) + - else + %p.title_child_first=raw t('faq.file_tutorials_link') + + +:javascript + $(function(){ + $('#search_faq').keyup(function(e){ + var words = $(this).val().toLowerCase().split(' '); + + if(words.length == 0 || (words.length == 1 && words[0] == '')){ + $('.question').show(); + }else{ + $('.question').each(function(a){ + var text = $(this).prop('textContent').toLowerCase(); + + var result = words.every(function(a) { + return text.indexOf(a) != -1; + }); + + if(result) + $(this).show(); + else + $(this).hide(); + }); + } + }); + + }); + + function slide_info(obj, event){ + event.preventDefault(); + $(obj).next(".invisible").toggle(); + if ($(obj).next(".invisible").is(":visible")) { + focus_element($(obj).next()); + } + } + + function click_on_keypress(event, element){ + if(event.which == 13) + $(element).click(); + return false; + } + + function focus_element(element){ + $(element).prop('tabindex', 0); + $(element).focus(); + } \ No newline at end of file diff --git a/app/views/faqs/index.html.haml b/app/views/faqs/index.html.haml new file mode 100644 index 000000000..b30ec959d --- /dev/null +++ b/app/views/faqs/index.html.haml @@ -0,0 +1,23 @@ +%h1 Perguntas Frequentes (FAQ) + +%table.table + %thead + %tr + %th Ordem + %th Pergunta (PT) + %th Ativo + %th{colspan: 3} Ações + + %tbody + - @faqs.each do |faq| + %tr + %td= faq.order + %td= faq.translation_pt&.question || "(sem tradução PT)" + %td= faq.active ? "Sim" : "Não" + %td= link_to 'Ver', admin_faq_path(faq) + %td= link_to 'Editar', edit_admin_faq_path(faq) + %td= link_to 'Deletar', admin_faq_path(faq), method: :delete, data: { confirm: 'Tem certeza?' } + +%br + += link_to 'Novo FAQ', new_admin_faq_path, class: 'btn btn-success' diff --git a/app/views/faqs/new.html.haml b/app/views/faqs/new.html.haml new file mode 100644 index 000000000..cbd73b68e --- /dev/null +++ b/app/views/faqs/new.html.haml @@ -0,0 +1,4 @@ += render 'form' + +%br += link_to 'Voltar', admin_faq_index_path diff --git a/app/views/faqs/show.html.haml b/app/views/faqs/show.html.haml new file mode 100644 index 000000000..73021679e --- /dev/null +++ b/app/views/faqs/show.html.haml @@ -0,0 +1,37 @@ +%p#notice= notice + +%h1 FAQ ##{@faq.id} + +%p + %b Ordem: + = @faq.order + +%p + %b Ativo: + = @faq.active ? "Sim" : "Não" + +%hr + +%h2 🇧🇷 Português +%p + %b Pergunta: + = @faq.translation_pt&.question || "(sem tradução)" +%p + %b Resposta: + = simple_format(@faq.translation_pt&.answer || "(sem tradução)") + +%hr + +%h2 🇺🇸 English +%p + %b Question: + = @faq.translation_en&.question || "(no translation)" +%p + %b Answer: + = simple_format(@faq.translation_en&.answer || "(no translation)") + +%hr + += link_to 'Editar', edit_admin_faq_path(@faq) +\| += link_to 'Voltar', admin_faq_index_path From 94c4c0d7eefd86c0b948f3e76cf70bddd0e93754 Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Wed, 17 Dec 2025 13:18:30 -0300 Subject: [PATCH 09/31] Configura rotas e rake tasks para FAQ --- app/controllers/pages_controller.rb | 2 + app/views/pages/faq.html.haml | 222 +--------------------------- config/routes.rb | 5 +- lib/tasks/faq.rake | 154 +++++++++++++++++++ 4 files changed, 168 insertions(+), 215 deletions(-) create mode 100644 lib/tasks/faq.rake diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index d2d2c4d6b..e3135ef46 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -12,6 +12,8 @@ def apps end def faq + # Busca FAQs ativos com traduções (usa scope :faq_translations do model Faq) + @faq_order = Faq.faq_translations end def privacy_policy diff --git a/app/views/pages/faq.html.haml b/app/views/pages/faq.html.haml index e8a30567a..0c5bed8a6 100644 --- a/app/views/pages/faq.html.haml +++ b/app/views/pages/faq.html.haml @@ -11,220 +11,14 @@ = t('faq.search') = text_field_tag :search_faq, nil, :'aria-label'=>t('faq.search') - .question - = link_to '#void', onkeydown: 'click_on_keypress(event, this);', onclick: 'slide_info(this, event)', class: 'title' do - %h4.title=t('faq.questions.whats_solar') - .invisible - %p.title_child_first=raw t('faq.answers.whats_solar') - - .question - = link_to '#void', onkeydown: 'click_on_keypress(event, this);', onclick: 'slide_info(this, event)', class: 'title' do - %h4.title= t('faq.questions.whats_ava') - .invisible - %p.title_child_first= raw t('faq.answers.whats_ava') - - .question - = link_to '#void', onkeydown: 'click_on_keypress(event, this);', onclick: 'slide_info(this, event)', class: 'title' do - %h4.title= t('faq.questions.uab_forgot_login_password') - .invisible - %p.title_child_first= raw t('faq.answers.uab_forgot_login_password') - - .question - = link_to '#', onkeydown: 'click_on_keypress(event, this);', onclick: 'slide_info(this, event)', class: 'title' do - %h4.title= t('faq.questions.uab_selfregistration') - .invisible - %p.title_child_first= raw t('faq.answers.uab_selfregistration') - .question - = link_to '#', onkeydown: 'click_on_keypress(event, this);', onclick: 'slide_info(this, event)', class: 'title' do - %h4.title= t('faq.questions.email_confirmation') - .invisible - %p.title_child_first= raw t('faq.answers.email_confirmation') - .question - = link_to '#void', onkeydown: 'click_on_keypress(event, this);', onclick: 'slide_info(this, event)', class: 'title' do - %h4.title= t('faq.questions.forgot_login_password_ma') - .invisible - %p.title_child_first= raw t('faq.answers.forgot_login_password_ma') - - .question - = link_to '#void', onkeydown: 'click_on_keypress(event, this);', onclick: 'slide_info(this, event)', class: 'title' do - %h4.title= t('faq.questions.forgot_login_password_sync1') - .invisible - %p.title_child_first= raw t('faq.answers.forgot_login_password_sync') - - .question - = link_to '#void', onkeydown: 'click_on_keypress(event, this);', onclick: 'slide_info(this, event)', class: 'title' do - %h4.title= t('faq.questions.forgot_login_password_sync2') - .invisible - %p.title_child_first= raw t('faq.answers.forgot_login_password_sync') - - .question - = link_to '#void', onkeydown: 'click_on_keypress(event, this);', onclick: 'slide_info(this, event)', class: 'title' do - %h4.title= t('faq.questions.forgot_login_password_sync3') - .invisible - %p.title_child_first= raw t('faq.answers.forgot_login_password_sync') - - .question - = link_to '#void', onkeydown: 'click_on_keypress(event, this);', onclick: 'slide_info(this, event)', class: 'title' do - %h4.title= t('faq.questions.forgot_login_password') - .invisible - %p.title_child_first= raw t('faq.answers.forgot_login_password') - - .question - = link_to '#void', onkeydown: 'click_on_keypress(event, this);', onclick: 'slide_info(this, event)', class: 'title' do - %h4.title= t('faq.questions.recovery_password_email') - .invisible - %p.title_child_first= raw t('faq.answers.recovery_password_email') - - .question - = link_to '#void', onkeydown: 'click_on_keypress(event, this);', onclick: 'slide_info(this, event)', class: 'title' do - %h4.title= t('faq.questions.verify_registration') - .invisible - %p.title_child_first= raw t('faq.answers.verify_registration') - - .question - = link_to '#void', onkeydown: 'click_on_keypress(event, this);', onclick: 'slide_info(this, event)', class: 'title' do - %h4.title= t('faq.questions.update_data') - .invisible - %h4.title_child_first= raw t('faq.answers.not_ma') - %p.title_child_second= raw t('faq.answers.update_data') - %h4.title_child_first= raw t('faq.answers.ma') - %p.title_child_second=raw t('faq.answers.update_data_ma') - - .question - = link_to '#void', onkeydown: 'click_on_keypress(event, this);', onclick: 'slide_info(this, event)', class: 'title' do - %h4.title= t('faq.questions.update_data_ma_cant') - .invisible - %p.title_child_first= raw t('faq.answers.update_data_ma_cant') - %h4.title_child_first= raw t('faq.answers.have_access') - %p.title_child_second= raw t('faq.answers.update_data_ma') - %h4.title_child_first= raw t('faq.answers.dont_have_access') - %p.title_child_second=raw t('faq.answers.update_data_ma_cant2') - - .question - = link_to '#void', onkeydown: 'click_on_keypress(event, this);', onclick: 'slide_info(this, event)', class: 'title' do - %h4.title= t('faq.questions.invalid_token') - .invisible - %p.title_child_first= raw t('faq.answers.invalid_token') - - .question - = link_to '#void', onkeydown: 'click_on_keypress(event, this);', onclick: 'slide_info(this, event)', class: 'title' do - %h4.title= t('faq.questions.how_to_enroll') - .invisible - %p.title_child_first= raw t('faq.answers.how_to_enroll') - - .question - = link_to '#void', onkeydown: 'click_on_keypress(event, this);', onclick: 'slide_info(this, event)', class: 'title' do - %h4.title= t('faq.questions.cant_access_uc') - .invisible - %h4.title_child_first= raw t('faq.questions.cant_access_uc_date') - %p.title_child_second= raw t('faq.answers.cant_access_uc_date') - %h4.title_child_first= raw t('faq.questions.cant_access_uc_too_many') - %p.title_child_second=raw t('faq.answers.cant_access_uc_too_many') - %h4.title_child_first= raw t('faq.questions.cant_access_uc_canceled') - %p.title_child_second=raw t('faq.answers.cant_access_uc_canceled') - %h4.title_child_first= raw t('faq.questions.cant_access_uc_deactivated') - %p.title_child_second=raw t('faq.answers.cant_access_uc_deactivated') - %h4.title_child_first= raw t('faq.questions.cant_access_uc_javascript') - %p.title_child_second=raw t('faq.answers.cant_access_uc_javascript') - %h4.title_child_first= raw t('faq.questions.cant_access_uc_what_else') - %p.title_child_second=raw t('faq.answers.cant_access_uc_what_else') - - .question - = link_to '#void', onkeydown: 'click_on_keypress(event, this);', onclick: 'slide_info(this, event)', class: 'title' do - %h4.title= t('faq.questions.activity') - .invisible - %h4.title_child_first= raw t('faq.questions.activity_date') - %p.title_child_second= raw t('faq.answers.activity_date') - %h4.title_child_first= raw t('faq.questions.activity_error') - %p.title_child_second=raw t('faq.answers.activity_error') - %h4.title_child_first= raw t('faq.questions.activity_no_error') - %p.title_child_second=raw t('faq.answers.activity_no_error') - - .question - = link_to '#void', onkeydown: 'click_on_keypress(event, this);', onclick: 'slide_info(this, event)', class: 'title' do - %h4.title= t('faq.questions.activity_lost_date') - .invisible - %p.title_child_first= raw t('faq.answers.activity_lost_date') - - .question - = link_to '#coid', onkeydown: 'click_on_keypress(event, this);', onclick: 'slide_info(this, event)', class: 'title' do - %h4.title= t('faq.questions.maintenance') - .invisible - %p.title_child_first= raw t('faq.answers.maintenance') - - .question - = link_to '#void', onkeydown: 'click_on_keypress(event, this);', onclick: 'slide_info(this, event)', class: 'title' do - %h4.title= t('faq.questions.uc_profile') - .invisible - %h4.title_child_first= raw t('faq.questions.cant_access_uc_date') - %p.title_child_second= raw t('faq.answers.cant_access_uc_date') - %h4.title_child_first= raw t('faq.questions.cant_access_uc_too_many') - %p.title_child_second=raw t('faq.answers.cant_access_uc_too_many') - %h4.title_child_first= raw t('faq.questions.cant_access_uc_canceled') - %p.title_child_second=raw t('faq.answers.cant_access_uc_canceled') - %h4.title_child_first= raw t('faq.questions.cant_access_uc_deactivated') - %p.title_child_second=raw t('faq.answers.cant_access_uc_deactivated') - %h4.title_child_first= raw t('faq.questions.cant_access_uc_javascript') - %p.title_child_second=raw t('faq.answers.cant_access_uc_javascript') - %h4.title_child_first= raw t('faq.questions.cant_access_uc_what_else') - %p.title_child_second=raw t('faq.answers.cant_access_uc_what_else') - - .question - = link_to '#void', onkeydown: 'click_on_keypress(event, this);', onclick: 'slide_info(this, event)', class: 'title' do - %h4.title= t('faq.questions.lesson_notes') - .invisible - %h4.title_child_first= raw t('faq.questions.lesson_notes_inside') - %p.title_child_second= raw t('faq.answers.lesson_notes_inside') - %h4.title_child_first= raw t('faq.questions.lesson_notes_outside') - %p.title_child_second=raw t('faq.answers.lesson_notes_outside') - - .question - = link_to '#void', onkeydown: 'click_on_keypress(event, this);', onclick: 'slide_info(this, event)', class: 'title' do - %h4.title= t('faq.questions.uc_document') - .invisible - %p.title_child_first= raw t('faq.answers.uc_document') - - .question - = link_to '#void', onkeydown: 'click_on_keypress(event, this);', onclick: 'slide_info(this, event)', class: 'title' do - %h4.title= t('faq.questions.course_document') - .invisible - %p.title_child_first= raw t('faq.answers.course_document') - - .question - = link_to '#void', onkeydown: 'click_on_keypress(event, this);', onclick: 'slide_info(this, event)', class: 'title' do - %h4.title= t('faq.questions.photo') - .invisible - %p.title_child_first= raw t('faq.answers.photo') - - .question - = link_to '#void', onkeydown: 'click_on_keypress(event, this);', onclick: 'slide_info(this, event)', class: 'title' do - %h4.title= t('faq.questions.nick_login') - .invisible - %p.title_child_first= raw t('faq.answers.nick_login') - - .question - = link_to '#void', onkeydown: 'click_on_keypress(event, this);', onclick: 'slide_info(this, event)', class: 'title' do - %h4.title= t('faq.questions.finished_ucs') - .invisible - %p.title_child_first= raw t('faq.answers.finished_ucs') - - .question - = link_to '#void', onkeydown: 'click_on_keypress(event, this);', onclick: 'slide_info(this, event)', class: 'title' do - %h4.title= t('faq.questions.idea') - .invisible - %p.title_child_first= raw t('faq.answers.idea') - - .question - = link_to '#void', onkeydown: 'click_on_keypress(event, this);', onclick: 'slide_info(this, event)', class: 'title' do - %h4.title= t('faq.questions.error') - .invisible - %p.title_child_first= raw t('faq.answers.error') - - .question - = link_to '#void', onkeydown: 'click_on_keypress(event, this);', onclick: 'slide_info(this, event)', class: 'title' do - %h4.title= t('faq.questions.cant_find_faq') - .invisible - %p.title_child_first= raw t('faq.answers.cant_find_faq') + - @faq_order.each do |faq| + - current_translation = faq.faq_translations.find_by(locale: I18n.locale.to_s) + - if current_translation.present? + .question + = link_to '#void', onkeydown: 'click_on_keypress(event, this);', onclick: 'slide_info(this, event)', class: 'title' do + %h4.title= current_translation.question + .invisible + %p.title_child_first= raw(current_translation.answer) %h2= t(:video_tutorials) %p.title_child_first=raw t(:video_tutorials_link) diff --git a/config/routes.rb b/config/routes.rb index b75636b97..353ae13cc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,6 @@ Solar::Application.routes.draw do + #resources :faqs dá conflito com faq # Download webconferences post '/download', to: 'webconferences#download', as: 'download_webconference' @@ -60,8 +61,10 @@ end end - scope "/admin" do + get "/faqs/apresentation", to: "faqs#apresentation", as: 'faqs_questions' + scope "/admin" do + resources :faqs, as: 'admin_faq' resources :blacklist, except: [:show, :edit, :update], controller: :user_blacklist do get :search, on: :collection end diff --git a/lib/tasks/faq.rake b/lib/tasks/faq.rake new file mode 100644 index 000000000..423cced7b --- /dev/null +++ b/lib/tasks/faq.rake @@ -0,0 +1,154 @@ +namespace :faq do + desc "Popula a tabela de FAQs com as perguntas e respostas dos arquivos de locale" + task populate: :environment do + puts "Iniciando população de FAQs..." + + # Lista de chaves das perguntas (baseado no arquivo faq.html.haml) + faq_keys = [ + 'whats_solar', + 'whats_ava', + 'uab_forgot_login_password', + 'uab_selfregistration', + 'email_confirmation', + 'forgot_login_password_ma', + 'forgot_login_password_sync1', + 'forgot_login_password_sync2', + 'forgot_login_password_sync3', + 'forgot_login_password', + 'recovery_password_email', + 'verify_registration', + 'update_data', + 'update_data_ma_cant', + 'invalid_token', + 'how_to_enroll', + 'cant_access_uc', + 'activity', + 'activity_lost_date', + 'maintenance', + 'uc_profile', + 'lesson_notes', + 'uc_document', + 'course_document', + 'photo', + 'nick_login', + 'finished_ucs', + 'idea', + 'error', + 'cant_find_faq' + ] + + created_count = 0 + skipped_count = 0 + + faq_keys.each_with_index do |key, index| + # Busca as traduções nos arquivos de locale + question_pt = I18n.t("faq.questions.#{key}", locale: :pt_BR, default: nil) + answer_pt = I18n.t("faq.answers.#{key}", locale: :pt_BR, default: nil) + question_en = I18n.t("faq.questions.#{key}", locale: :en_US, default: nil) + answer_en = I18n.t("faq.answers.#{key}", locale: :en_US, default: nil) + + # Verifica se as traduções existem + if question_pt.nil? || answer_pt.nil? + puts " ⚠️ AVISO: Tradução PT não encontrada para '#{key}' - pulando..." + skipped_count += 1 + next + end + + # Cria o FAQ + faq = Faq.create!( + order: index + 1, + active: true + ) + + # Cria tradução PT + faq.faq_translations.create!( + locale: 'pt_BR', + question: question_pt, + answer: answer_pt + ) + + # Cria tradução EN (se existir) + if question_en.present? && answer_en.present? + faq.faq_translations.create!( + locale: 'en_US', + question: question_en, + answer: answer_en + ) + else + puts " ⚠️ AVISO: Tradução EN não encontrada para '#{key}' - criando apenas PT" + end + + puts " ✓ FAQ #{index + 1} criado: #{question_pt[0..50]}..." + created_count += 1 + end + + puts "\n" + "="*60 + puts "População concluída!" + puts "FAQs criados: #{created_count}" + puts "FAQs pulados: #{skipped_count}" + puts "Total de registros em Faq: #{Faq.count}" + puts "Total de registros em FaqTranslation: #{FaqTranslation.count}" + puts "="*60 + end + + desc "Remove FAQs duplicados (mantém apenas o mais antigo)" + task remove_duplicates: :environment do + puts "Buscando FAQs duplicados..." + + removed_count = 0 + + ['pt_BR', 'en_US'].each do |locale| + puts "\nVerificando duplicatas em #{locale}..." + + # Busca todas as perguntas e agrupa por texto + translations = FaqTranslation.where(locale: locale) + grouped = translations.group_by(&:question) + + grouped.each do |question, duplicates| + if duplicates.count > 1 + puts " Encontrado #{duplicates.count} duplicatas: '#{question[0..50]}...'" + + # Mantém o mais antigo, remove os outros + to_keep = duplicates.min_by(&:created_at) + to_remove = duplicates - [to_keep] + + to_remove.each do |translation| + faq = translation.faq + translation.destroy + + # Se o FAQ ficar sem traduções, remove ele também + if faq.faq_translations.count == 0 + puts " - Removendo FAQ ##{faq.id} (ficou sem traduções)" + faq.destroy + else + puts " - Removendo tradução duplicada (FAQ ##{faq.id})" + end + + removed_count += 1 + end + end + end + end + + puts "\n" + "="*60 + puts "Limpeza concluída!" + puts "Traduções duplicadas removidas: #{removed_count}" + puts "Total atual em Faq: #{Faq.count}" + puts "Total atual em FaqTranslation: #{FaqTranslation.count}" + puts "="*60 + end + + desc "Remove todos os FAQs do banco de dados" + task clear: :environment do + print "Tem certeza que deseja remover TODOS os FAQs? (s/N): " + input = STDIN.gets.chomp + + if input.downcase == 's' + count = Faq.count + Faq.destroy_all + puts "✓ #{count} FAQs removidos com sucesso!" + else + puts "Operação cancelada." + end + end +end From fc41f4a8125b299f1eff9c6d0630fedd98bcf833 Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Wed, 17 Dec 2025 13:18:47 -0300 Subject: [PATCH 10/31] =?UTF-8?q?Adiciona=20tradu=C3=A7=C3=B5es=20das=20FA?= =?UTF-8?q?Qs=20em=20portugu=C3=AAs=20e=20ingl=C3=AAs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/locales/en_US.yml | 37 ++++++++++++++++++++++++++++++++ config/locales/pt_BR.yml | 46 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/config/locales/en_US.yml b/config/locales/en_US.yml index 7cf2a615d..b56070741 100644 --- a/config/locales/en_US.yml +++ b/config/locales/en_US.yml @@ -1664,6 +1664,7 @@ en_US: menu_admin_notifications: General notifications menu_events: Events menu_admin_report_webconference: UAB web conferencing report + menu_admin_faq: Frequently Asked Questions places_nav_panel_course_hint: "Type the course's name or part of it" places_nav_panel_semester_hint: "Type the semester or part of it" @@ -5192,3 +5193,39 @@ en_US: general_message: It was not possible to execute action. date_range_expired: It was not possible to execute action. Date range expired. no_acu: Is not possible to create a comment to this student at this group. + + # FAQ Translations + faqs: + new: New FAQ + edit: Edit FAQ + index: Frequently Asked Questions + form: + translations: FAQ Translations + save: Save FAQ + cancel: Cancel + + simple_form: + labels: + faq: + order: Display order + active: FAQ active (visible on site) + faq_translation: + question: Question + answer: Answer + placeholders: + faq_translation: + question: Type the question... + answer: Type the answer... + + activerecord: + models: + faq: FAQ + faq_translation: FAQ Translation + attributes: + faq: + order: Order + active: Active + faq_translation: + locale: Language + question: Question + answer: Answer diff --git a/config/locales/pt_BR.yml b/config/locales/pt_BR.yml index 3be130e85..85208d026 100644 --- a/config/locales/pt_BR.yml +++ b/config/locales/pt_BR.yml @@ -1936,7 +1936,8 @@ pt_BR: menu_admin_notifications: Avisos Gerais menu_events: "Eventos" menu_admin_report_webconference: Relatório webconferências UAB - + # FAQ adicionado + menu_admin_faq: FAQ # Componente de filtro/busca de allocationTags (places_nav_panel) places_nav_panel_course_hint: "Digite o nome do curso ou parte dele" places_nav_panel_semester_hint: "Digite o semestre" @@ -5189,6 +5190,7 @@ pt_BR: idea: "Tenho uma ideia para o Solar, como posso sugerí-la para implementação?" error: "Sou responsável por uma disciplina e um portfólio enviado pelo aluno aparece em branco. Pode ser erro do sistema?" cant_find_faq: "Não encontrei minha dúvida neste FAQ?" + still_pending: "Discente alcançou frequências e notas satisfatórias para aprovação, mas o Resultado apresenta situação 'Pendente', o que fazer?" uab_selfregistration: "O que é o autocadastro no Sigaa? Como posso realizar o autocadastro?" email_confirmation: "Realizei meu autocadastro, como faço para utilizar o Solar?" answers: @@ -5234,6 +5236,12 @@ pt_BR: idea: "O Solar 2.0 dispõe de um botão de 'Sugestões para o Solar 2.0'. Para acessá-lo, basta realizar o login, que o botão ficará disponível no canto superior direito da tela logo abaixo do relógio.
Lá você poderá enviar sugestões, reclamações e similares." error: "O Solar 2.0 não altera dados nem arquivos previamente enviados pelos usuários sem comunicá-los ou sem prévia autorização. Portanto, se o portfolio do aluno está em branco, sugerimos que entre em contato com ele para solicitar um re-envio do arquivo correto." cant_find_faq: "Entre em contato com o atendimento@virtual.ufc.br relatando sua situação. Lembre-se de informar:
‣ Nome
‣ Cpf
‣ Email
‣ O maior número de informações sobre o ocorrido. Se possível, imagens (prints de tela) também." + still_pending: "Destacamos que só é possível definir a situação quando as atividades da turma e disciplina estão configuradas como avaliativas e/ou de frequência.
Caso a turma esteja devidamente configurada, você pode:" + still_pending_automatic: "‣ Aguardar a data de definição automática de situação" + still_pending_automatic_details: "Ao configurar as atividades que serão contabilizadas para média e frequência, o Solar define uma data para as situações serem definidas. Esta data é escolhida de acordo com a data de encerramento da última atividade avaliativa, que não seja AF. Portanto, as situações ficarão como 'Pendente' até que esta data chegue.

Exemplo: há as seguintes atividades em uma disciplina:
  • Fórum avaliativo encerrando em 01/12/2025
  • Fórum não avaliativo encerrando em 04/12/2025
  • Prova online avaliativa encerrando em 03/12/2025
  • Trabalho/Portfólio avaliativo e AF encerrando em 05/12/2025
" + still_pending_automatic_details2: "O sistema vai entender que a data de liberação das situações/resultados é a data da última atividade avaliativa/de frequência, que não seja AF acrescida de 2 dias. Ou seja, a Prova online avaliativa, do dia 03/12/2025, acrescida de dois dias.
A data de liberação automática das situações neste exemplo seria 05/12/2025." + still_pending_manually: "‣ Definir manualmente a situação" + still_pending_manually_details: "Para liberar as situações antes da data prevista pelo sistema, basta acessar o menu 'Acompanhamento' dentro da turma desejada e clicar no botão 'Definir situações'." uab_selfregistration: "Todos os dados que existiam no Módulo Acadêmico, antigo sistema acadêmico utilizado pela UAB, foram transferidos para o Sigaa, sistema acadêmico atual. Isso implica, portanto, na transferência, dentre outros, dos dados cadastrados para cada usuário, aluno ou professor. Entretanto, os dados de acesso não são transferidos para evitar conflitos com os demais usuários previamente existentes no Sigaa. Por este motivo, é necessário que todo usuário que existia no Módulo Acadêmico faça seu autocadastro no Sigaa e, em seguida, confirme seu email, para poder definir seus novos dados de acesso como, por exemplo, login e senha.

Para realizar seu autocadastro, basta acessar https://si3.ufc.br/sigaa/public/cadastro/discente.jsf e informar seu cpf, nome completo e data de nascimento conforme constavam no Módulo Acadêmico e, também, informar email, login e senha. Você pode, se assim quiser, utilizar os mesmos email, login e senha que utilizava no Módulo Acadêmico. Se estes já estiverem em uso por outro usuário ou não atenderem aos novos padrões, o sistema não irá salvar e irá comunicá-lo para escolher novos dados." email_confirmation: "Uma vez feito o autocadastro no SIGAA, você precisa acessar o email informado e clicar no link de confirmação de cadastro.
Caso não tenha recebido o email ou tenha apagado, é possível solicitá-lo novamente através da url https://si3.ufc.br/admin/public/recuperar_codigo.jsf.
Lembre-se de verificar a caixa de spam!
Uma vez confirmado o cadastro, basta acessar o Solar com o mesmo login e senha definidos no SIGAA." ckeditor: @@ -5514,3 +5522,39 @@ pt_BR: general_message: Não foi possível realizar ação. date_range_expired: Não foi possível realizar ação. Período encerrado. no_acu: Não é possível criar um comentário para este aluno nesta turma. + + # Traduções FAQ + faqs: + new: Novo FAQ + edit: Editar FAQ + index: Perguntas Frequentes + form: + translations: Traduções do FAQ + save: Salvar FAQ + cancel: Cancelar + + simple_form: + labels: + faq: + order: Ordem de exibição + active: FAQ ativo (visível no site) + faq_translation: + question: Pergunta + answer: Resposta + placeholders: + faq_translation: + question: Digite a pergunta... + answer: Digite a resposta... + + activerecord: + models: + faq: FAQ + faq_translation: Tradução do FAQ + attributes: + faq: + order: Ordem + active: Ativo + faq_translation: + locale: Idioma + question: Pergunta + answer: Resposta From ab630e86ddb2e30cd820031006929c67331bd2a8 Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Wed, 17 Dec 2025 13:19:06 -0300 Subject: [PATCH 11/31] Adiciona testes e fixtures para sistema de FAQ --- test/application_system_test_case.rb | 5 +++++ test/controllers/faq_controller_test.rb | 7 +++++++ test/factories/faq_translations.rb | 10 ++++++++++ test/factories/faqs.rb | 8 ++++++++ test/fixtures/menus.yml | 1 + test/fixtures/menus_contexts.yml | 4 ++++ test/models/faq_test.rb | 7 +++++++ test/models/faq_translation_test.rb | 7 +++++++ 8 files changed, 49 insertions(+) create mode 100644 test/application_system_test_case.rb create mode 100644 test/controllers/faq_controller_test.rb create mode 100644 test/factories/faq_translations.rb create mode 100644 test/factories/faqs.rb create mode 100644 test/models/faq_test.rb create mode 100644 test/models/faq_translation_test.rb diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb new file mode 100644 index 000000000..d19212abd --- /dev/null +++ b/test/application_system_test_case.rb @@ -0,0 +1,5 @@ +require "test_helper" + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :chrome, screen_size: [1400, 1400] +end diff --git a/test/controllers/faq_controller_test.rb b/test/controllers/faq_controller_test.rb new file mode 100644 index 000000000..2adea4b32 --- /dev/null +++ b/test/controllers/faq_controller_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class FaqControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/factories/faq_translations.rb b/test/factories/faq_translations.rb new file mode 100644 index 000000000..d1b7f042b --- /dev/null +++ b/test/factories/faq_translations.rb @@ -0,0 +1,10 @@ +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :faq_translation do + locale "MyString" + question "MyString" + answer "MyText" + faq nil + end +end diff --git a/test/factories/faqs.rb b/test/factories/faqs.rb new file mode 100644 index 000000000..8d6d2f3ce --- /dev/null +++ b/test/factories/faqs.rb @@ -0,0 +1,8 @@ +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :faq do + order 1 + active false + end +end diff --git a/test/fixtures/menus.yml b/test/fixtures/menus.yml index 9e784e1d5..0c9eeb026 100644 --- a/test/fixtures/menus.yml +++ b/test/fixtures/menus.yml @@ -244,6 +244,7 @@ admin_reports: order: 14 resource_id: 226 +#FAQ admin_faq: id: 615 parent_id: 60 diff --git a/test/fixtures/menus_contexts.yml b/test/fixtures/menus_contexts.yml index 09b302eaa..545fcfdc3 100644 --- a/test/fixtures/menus_contexts.yml +++ b/test/fixtures/menus_contexts.yml @@ -210,3 +210,7 @@ edition_repositories: edition_content_uc: menu_id: 132 context_id: 2 + +faq: + menu_id: 615 + context_id: 1 \ No newline at end of file diff --git a/test/models/faq_test.rb b/test/models/faq_test.rb new file mode 100644 index 000000000..0249270ce --- /dev/null +++ b/test/models/faq_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class FaqTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/faq_translation_test.rb b/test/models/faq_translation_test.rb new file mode 100644 index 000000000..4a8b5258d --- /dev/null +++ b/test/models/faq_translation_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class FaqTranslationTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end From ce667334f68a2a49636af50db8c008e2000e41b9 Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Wed, 17 Dec 2025 13:21:43 -0300 Subject: [PATCH 12/31] =?UTF-8?q?Atualiza=20depend=C3=AAncias=20e=20fixtur?= =?UTF-8?q?es=20do=20menu=20administrativo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Gemfile | 2 +- app/assets/javascripts/application.js.erb | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 7c2898a62..3e235b9b0 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source "http://rubygems.org" -ruby "2.7.2" +ruby "2.7.8" #gem "rails", "~> 3.2.16" gem "rails", "5.1.7" diff --git a/app/assets/javascripts/application.js.erb b/app/assets/javascripts/application.js.erb index b099c69a3..a38b190e7 100755 --- a/app/assets/javascripts/application.js.erb +++ b/app/assets/javascripts/application.js.erb @@ -47,6 +47,8 @@ // = require initialize-jspanel +// = require faqs + /** * General */ From ef55bf6e6485b53e034728387d00f4087c30b3ac Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Fri, 19 Dec 2025 12:48:45 -0300 Subject: [PATCH 13/31] =?UTF-8?q?Adiciona=20tradu=C3=A7=C3=B5es=20para=20m?= =?UTF-8?q?enu=20admin=20de=20FAQ?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/locales/en_US.yml | 23 ++++++++++++++++++++++- config/locales/pt_BR.yml | 23 ++++++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/config/locales/en_US.yml b/config/locales/en_US.yml index b56070741..3a480011a 100644 --- a/config/locales/en_US.yml +++ b/config/locales/en_US.yml @@ -5198,7 +5198,28 @@ en_US: faqs: new: New FAQ edit: Edit FAQ - index: Frequently Asked Questions + index: + title: Frequently Asked Questions (FAQ) + new: New FAQ + edit: Edit + delete: Delete + show: View + preview: Preview + order: Order + question: Question + answer: Answer + active: Active + no_translation: (no translation) + activate: Activate + deactivate: Deactivate + no_data: No FAQs registered + confirm_delete: Are you sure you want to delete this FAQ? + faq_row: + answer: Answer + no_translation: (no translation) + deactivate: Deactivate + activate: Activate + show: View form: translations: FAQ Translations save: Save FAQ diff --git a/config/locales/pt_BR.yml b/config/locales/pt_BR.yml index 85208d026..04430448c 100644 --- a/config/locales/pt_BR.yml +++ b/config/locales/pt_BR.yml @@ -5527,7 +5527,28 @@ pt_BR: faqs: new: Novo FAQ edit: Editar FAQ - index: Perguntas Frequentes + index: + title: Perguntas Frequentes (FAQ) + new: Novo FAQ + edit: Editar + delete: Deletar + show: Visualizar + preview: Prévia + order: Ordem + question: Pergunta + answer: Resposta + active: Ativo + no_translation: (sem tradução) + activate: Ativar + deactivate: Desativar + no_data: Nenhuma FAQ cadastrada + confirm_delete: Tem certeza que deseja deletar esta FAQ? + faq_row: + answer: Resposta + no_translation: (sem tradução) + deactivate: Desativar + activate: Ativar + show: Visualizar form: translations: Traduções do FAQ save: Salvar FAQ From 23e7beaa818d27382437f3271560a1f57eeff0e8 Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Fri, 19 Dec 2025 12:48:46 -0300 Subject: [PATCH 14/31] Adiciona rota para toggle_active de FAQ --- config/routes.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index 353ae13cc..ab252ba64 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -64,7 +64,9 @@ get "/faqs/apresentation", to: "faqs#apresentation", as: 'faqs_questions' scope "/admin" do - resources :faqs, as: 'admin_faq' + resources :faqs, as: 'admin_faq' do + patch :toggle_active, on: :member + end resources :blacklist, except: [:show, :edit, :update], controller: :user_blacklist do get :search, on: :collection end From da388f4fcfaa2c7abab237812bed5eccbe5880f5 Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Fri, 19 Dec 2025 12:48:46 -0300 Subject: [PATCH 15/31] =?UTF-8?q?Adiciona=20m=C3=A9todo=20toggle=5Factive?= =?UTF-8?q?=20no=20controller=20de=20FAQ?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/faqs_controller.rb | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/controllers/faqs_controller.rb b/app/controllers/faqs_controller.rb index 6b9cd0818..205a09e73 100644 --- a/app/controllers/faqs_controller.rb +++ b/app/controllers/faqs_controller.rb @@ -5,7 +5,7 @@ class FaqsController < ApplicationController before_action :require_admin # Carrega o FAQ antes destas ações - before_action :set_faq, only: [:show, :edit, :update, :destroy] + before_action :set_faq, only: [:show, :edit, :update, :destroy, :toggle_active] skip_before_action :require_admin, only:[:apresentation] @@ -58,6 +58,16 @@ def destroy redirect_to admin_faq_index_path, notice: 'FAQ removido com sucesso.' end + # PATCH /admin/faqs/1/toggle_active - Alterna status ativo/inativo via AJAX + def toggle_active + @faq.update(active: !@faq.active) + + respond_to do |format| + format.js + format.json { render json: { success: true, active: @faq.active } } + end + end + def apresentation @faq_order = Faq.faq_translations render :faq From 3ae18813e1dddaecc09ead6f3aa70c81b2f1dee0 Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Fri, 19 Dec 2025 12:48:47 -0300 Subject: [PATCH 16/31] Adiciona partial e view JS para FAQ --- app/views/faqs/_faq_row.html.haml | 17 +++++++++++++++++ app/views/faqs/toggle_active.js.haml | 2 ++ 2 files changed, 19 insertions(+) create mode 100644 app/views/faqs/_faq_row.html.haml create mode 100644 app/views/faqs/toggle_active.js.haml diff --git a/app/views/faqs/_faq_row.html.haml b/app/views/faqs/_faq_row.html.haml new file mode 100644 index 000000000..3450754af --- /dev/null +++ b/app/views/faqs/_faq_row.html.haml @@ -0,0 +1,17 @@ +%td{style: 'text-align: center;'} + = check_box_tag "ckb_faq[]", faq.id, false, class: 'ckb_faq', :'data-faq-id' => faq.id +%td= faq.order +%td + = link_to '#void', onclick: 'toggle_answer(this, event)', onkeydown: 'click_on_keypress(event, this);', class: 'faq-question' do + = faq.question || t('.no_translation') + = content_tag(:i, nil, class: 'icon-arrow-down', style: 'margin-left: 10px;', :'aria-hidden' => 'true') + .faq-answer.invisible{style: 'margin-top: 10px; padding: 10px; background: #f5f5f5; border-left: 3px solid #ccc;'} + %strong= t('.answer', default: 'Resposta:') + %p= raw(faq.answer || t('.no_translation')) +%td.center{style: 'width: 70px;'} + - img_status = faq.active ? 'released.png' : 'rejected.png' + - tooltip_text = faq.active ? t('.deactivate', default: 'Desativar') : t('.activate', default: 'Ativar') + = link_to image_tag(img_status), toggle_active_admin_faq_path(faq), method: :patch, remote: true, class: 'change_faq_status', :'data-faq-id' => faq.id, :'data-tooltip' => tooltip_text +%td{style: 'text-align: center;'} + = link_to admin_faq_path(faq), class: 'btn', :'data-tooltip' => t('.show', default: 'Visualizar') do + = content_tag(:i, nil, class: 'icon-eye', :'aria-hidden' => 'true') diff --git a/app/views/faqs/toggle_active.js.haml b/app/views/faqs/toggle_active.js.haml new file mode 100644 index 000000000..3e2744c88 --- /dev/null +++ b/app/views/faqs/toggle_active.js.haml @@ -0,0 +1,2 @@ +$("tr.faq-row[data-faq-id='#{@faq.id}']").html('#{j render partial: 'faq_row', locals: { faq: @faq }}'); +flash_message('Status atualizado com sucesso!', 'notice'); From 3eb8dcf929fd8c9d5865c392fde8fe091d1182e7 Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Fri, 19 Dec 2025 12:48:47 -0300 Subject: [PATCH 17/31] =?UTF-8?q?Atualiza=20p=C3=A1gina=20index=20de=20FAQ?= =?UTF-8?q?=20com=20accordion=20e=20toggle=20de=20status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/faqs/index.html.haml | 173 ++++++++++++++++++++++++++++----- 1 file changed, 150 insertions(+), 23 deletions(-) diff --git a/app/views/faqs/index.html.haml b/app/views/faqs/index.html.haml index b30ec959d..c146e4f84 100644 --- a/app/views/faqs/index.html.haml +++ b/app/views/faqs/index.html.haml @@ -1,23 +1,150 @@ -%h1 Perguntas Frequentes (FAQ) - -%table.table - %thead - %tr - %th Ordem - %th Pergunta (PT) - %th Ativo - %th{colspan: 3} Ações - - %tbody - - @faqs.each do |faq| - %tr - %td= faq.order - %td= faq.translation_pt&.question || "(sem tradução PT)" - %td= faq.active ? "Sim" : "Não" - %td= link_to 'Ver', admin_faq_path(faq) - %td= link_to 'Editar', edit_admin_faq_path(faq) - %td= link_to 'Deletar', admin_faq_path(faq), method: :delete, data: { confirm: 'Tem certeza?' } - -%br - -= link_to 'Novo FAQ', new_admin_faq_path, class: 'btn btn-success' +.block_wrapper.list_faqs + .block_title + %h2= t('.title', default: 'Perguntas Frequentes (FAQ)') + + .block_content_toolbar + .block_toolbar_left.btn-group + = link_to content_tag(:i, nil, class: 'icon-plus'), new_admin_faq_path, class: 'btn btn_main', :'data-tooltip' => t('.new', default: 'Novo FAQ') + + .block_toolbar_right + .btn-group + = link_to content_tag(:i, nil, class: 'icon-edit'), '#void', class: 'btn btn_edit', :'data-link-edit' => edit_admin_faq_path(id: ':id'), disabled: true, :'data-tooltip' => t('.edit', default: 'Editar') + = link_to content_tag(:i, nil, class: 'icon-trash'), '#void', class: 'btn btn_del delete_faq', :'data-link-delete' => admin_faq_path(id: ':id'), disabled: true, :'data-tooltip' => t('.delete', default: 'Deletar') + + .block_content.responsive-table + %table.tb_list.tb_faqs + %thead{style: (@faqs.blank? ? 'display: none' : '')} + %tr.lines + %th.no_sort{style: 'text-align: center; width: 25px;'}= check_box_tag :all_faqs, false, false, :'data-children-names' => 'ckb_faq', class: 'all_faqs' + %th{style: 'width: 60px;'}= t('.order', default: 'Ordem') + %th{align: 'left'}= t('.question', default: 'Pergunta') + %th.no_sort{style: 'width: 80px; text-align: center;'}= t('.active', default: 'Ativo') + %th.no_sort{style: 'width: 60px; text-align: center;'}= t('.preview', default: 'Prévia') + %tbody + - @faqs.order(:order).each do |faq| + %tr.faq-row{"data-faq-id" => faq.id} + = render partial: 'faq_row', locals: { faq: faq } + .text_none.empty_message{class: (@faqs.blank? ? '' : 'hide_message')}= t('.no_data', default: 'Nenhuma FAQ cadastrada') + +:javascript + $(function(){ + // Toggle accordion para mostrar/esconder resposta + window.toggle_answer = function(element, event) { + event.preventDefault(); + var $answer = $(element).next('.faq-answer'); + var $icon = $(element).find('i'); + + $answer.toggle(); + + // Trocar ícone de seta + if ($answer.is(':visible')) { + $icon.removeClass('icon-arrow-down').addClass('icon-arrow-up'); + } else { + $icon.removeClass('icon-arrow-up').addClass('icon-arrow-down'); + } + }; + + // Função para suportar Enter em links + window.click_on_keypress = function(event, element) { + if (event.which == 13) { + $(element).click(); + } + }; + + // O toggle de ativo/inativo agora usa remote: true do Rails + + // Selecionar todos os checkboxes + $('.all_faqs').on('change', function() { + var isChecked = $(this).is(':checked'); + $('.ckb_faq').prop('checked', isChecked); + toggleActionButtons(); + }); + + // Habilitar/desabilitar botões de ação baseado na seleção + $('.ckb_faq').on('change', function() { + toggleActionButtons(); + }); + + function toggleActionButtons() { + var checkedBoxes = $('.ckb_faq:checked'); + var $editBtn = $('.btn_edit[data-link-edit]'); + var $deleteBtn = $('.delete_faq[data-link-delete]'); + + if (checkedBoxes.length === 0) { + // Nenhum selecionado - desabilitar botões + $editBtn.attr('disabled', true).attr('href', '#void'); + $deleteBtn.attr('disabled', true).attr('href', '#void'); + } else if (checkedBoxes.length === 1) { + // Exatamente um selecionado - habilitar editar e deletar + var faqId = checkedBoxes.first().val(); + var editUrl = $editBtn.data('link-edit').replace(':id', faqId); + var deleteUrl = $deleteBtn.data('link-delete').replace(':id', faqId); + + $editBtn.attr('disabled', false).attr('href', editUrl); + $deleteBtn.attr('disabled', false).attr('href', deleteUrl); + } else { + // Múltiplos selecionados - desabilitar editar, habilitar deletar + $editBtn.attr('disabled', true).attr('href', '#void'); + $deleteBtn.attr('disabled', false); + } + } + + // Confirmar deleção + $('.delete_faq').on('click', function(e) { + if ($(this).attr('disabled')) { + e.preventDefault(); + return false; + } + + var checkedBoxes = $('.ckb_faq:checked'); + if (checkedBoxes.length === 0) { + e.preventDefault(); + return false; + } + + if (!confirm('Tem certeza que deseja deletar esta(s) FAQ(s)?')) { + e.preventDefault(); + return false; + } + + if (checkedBoxes.length === 1) { + // Deletar um único item via link + return true; + } else { + // Deletar múltiplos itens + e.preventDefault(); + var faqIds = []; + checkedBoxes.each(function() { + faqIds.push($(this).val()); + }); + + // Deletar múltiplos FAQs + deleteFaqs(faqIds); + } + }); + + function deleteFaqs(faqIds) { + var deleteUrl = $('.delete_faq').data('link-delete'); + var promises = []; + + faqIds.forEach(function(faqId) { + var url = deleteUrl.replace(':id', faqId); + promises.push( + $.ajax({ + url: url, + type: 'DELETE', + headers: { + 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') + } + }) + ); + }); + + $.when.apply($, promises).done(function() { + flash_message('FAQ(s) deletada(s) com sucesso!', 'notice'); + location.reload(); + }).fail(function() { + flash_message('Erro ao deletar FAQ(s)', 'alert'); + }); + } + }); From ca50f82f7016849c03d48107b29056a33759d133 Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Mon, 22 Dec 2025 07:44:31 -0300 Subject: [PATCH 18/31] =?UTF-8?q?Adiciona=20funcionalidade=20de=20ordena?= =?UTF-8?q?=C3=A7=C3=A3o=20de=20FAQs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/assets/javascripts/faqs.js.erb | 47 ++++++++++++++++++++++++++++++ app/controllers/faqs_controller.rb | 18 +++++++++++- app/views/faqs/_faq_row.html.haml | 10 ++++--- 3 files changed, 70 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/faqs.js.erb b/app/assets/javascripts/faqs.js.erb index 91b966446..2ec77fd2e 100644 --- a/app/assets/javascripts/faqs.js.erb +++ b/app/assets/javascripts/faqs.js.erb @@ -115,3 +115,50 @@ $(function() { // Inicializa: oculta botões de remover se tiver apenas 2 campos updateRemoveButtons(); }); + +// FAQ Ordering - Ordenação com setas de acessibilidade +jQuery(function ($) { + // Só executa se estiver na página de listagem de FAQs + if ($(".tb_faqs").length === 0) { + return; + } + + $(".up, .down").sort_row({ + announceSelector: '#faq_announce', + + getItemData: function($row) { + return { + id: $row.data('faq').id, + question: $row.data('question').trim() + }; + }, + + isValidTarget: function($row) { + return !!$row.data('faq'); + }, + + buildUrl: function(item, $targetRow) { + var $table = $targetRow.closest('table'); + var urlTemplate = $table.data('reorderUrl'); + var targetData = $targetRow.data('faq'); + return urlTemplate.replace(':id', item.id).replace(':change_id', targetData.id); + }, + + formatBoundaryMessage: function(item, direction) { + if (direction === 'up') { + return 'A FAQ "' + item.question + '" já está no topo da lista'; + } else { + return 'A FAQ "' + item.question + '" já está no final da lista'; + } + }, + + formatMoveMessage: function(item, position) { + return 'FAQ "' + item.question + '" movida para a posição ' + position; + }, + + handleError: function(error) { + console.error("Falha ao atualizar a ordem:", error); + alert('Erro ao atualizar a ordem da FAQ'); + } + }); +}); diff --git a/app/controllers/faqs_controller.rb b/app/controllers/faqs_controller.rb index 205a09e73..e9ff24ba2 100644 --- a/app/controllers/faqs_controller.rb +++ b/app/controllers/faqs_controller.rb @@ -5,7 +5,7 @@ class FaqsController < ApplicationController before_action :require_admin # Carrega o FAQ antes destas ações - before_action :set_faq, only: [:show, :edit, :update, :destroy, :toggle_active] + before_action :set_faq, only: [:show, :edit, :update, :destroy, :toggle_active, :order] skip_before_action :require_admin, only:[:apresentation] @@ -68,6 +68,22 @@ def toggle_active end end + # PUT /admin/faqs/1/order/:change_id - Troca a ordem de dois FAQs + def order + faq1 = @faq + faq2 = Faq.find(params[:change_id]) + + Faq.transaction do + faq1.order, faq2.order = faq2.order, faq1.order + faq1.save! + faq2.save! + end + + render json: { success: true } + rescue => e + render json: { success: false, error: e.message }, status: :unprocessable_entity + end + def apresentation @faq_order = Faq.faq_translations render :faq diff --git a/app/views/faqs/_faq_row.html.haml b/app/views/faqs/_faq_row.html.haml index 3450754af..8627be3b8 100644 --- a/app/views/faqs/_faq_row.html.haml +++ b/app/views/faqs/_faq_row.html.haml @@ -1,10 +1,12 @@ %td{style: 'text-align: center;'} = check_box_tag "ckb_faq[]", faq.id, false, class: 'ckb_faq', :'data-faq-id' => faq.id -%td= faq.order +%td.center + .arrow_up= button_tag content_tag(:i, nil, class: 'icon-arrow-up-triangle'), class: 'up btn_arrow', :"aria-label" => t('faqs.index.up_aria', question: faq.question || 'FAQ'), :"data-tooltip" => t('faqs.index.up', default: 'Mover para cima') + .arrow_down= button_tag content_tag(:i, nil, class: 'icon-arrow-down-triangle'), class: 'down btn_arrow', :"aria-label" => t('faqs.index.down_aria', question: faq.question || 'FAQ'), :"data-tooltip" => t('faqs.index.down', default: 'Mover para baixo') %td - = link_to '#void', onclick: 'toggle_answer(this, event)', onkeydown: 'click_on_keypress(event, this);', class: 'faq-question' do - = faq.question || t('.no_translation') - = content_tag(:i, nil, class: 'icon-arrow-down', style: 'margin-left: 10px;', :'aria-hidden' => 'true') + = link_to '#void', onclick: 'toggle_answer(this, event)', onkeydown: 'click_on_keypress(event, this);', class: 'faq-question', style: 'display: flex; align-items: center;' do + = content_tag(:i, nil, class: 'icon-arrow-right', style: 'margin-right: 8px; transition: transform 0.3s;', :'aria-hidden' => 'true') + %span= faq.question || t('.no_translation') .faq-answer.invisible{style: 'margin-top: 10px; padding: 10px; background: #f5f5f5; border-left: 3px solid #ccc;'} %strong= t('.answer', default: 'Resposta:') %p= raw(faq.answer || t('.no_translation')) From e89bb5da802e3ca2a7d21fc005564c605237be30 Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Mon, 22 Dec 2025 07:44:32 -0300 Subject: [PATCH 19/31] =?UTF-8?q?Atualiza=20interface=20da=20lista=20de=20?= =?UTF-8?q?FAQs=20com=20sele=C3=A7=C3=A3o=20m=C3=BAltipla?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/faqs/index.html.haml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/views/faqs/index.html.haml b/app/views/faqs/index.html.haml index c146e4f84..ba783b6ab 100644 --- a/app/views/faqs/index.html.haml +++ b/app/views/faqs/index.html.haml @@ -12,20 +12,22 @@ = link_to content_tag(:i, nil, class: 'icon-trash'), '#void', class: 'btn btn_del delete_faq', :'data-link-delete' => admin_faq_path(id: ':id'), disabled: true, :'data-tooltip' => t('.delete', default: 'Deletar') .block_content.responsive-table - %table.tb_list.tb_faqs + %table.tb_list.tb_faqs{data: { reorder_url: change_order_admin_faq_path(':id', ':change_id') }} %thead{style: (@faqs.blank? ? 'display: none' : '')} %tr.lines %th.no_sort{style: 'text-align: center; width: 25px;'}= check_box_tag :all_faqs, false, false, :'data-children-names' => 'ckb_faq', class: 'all_faqs' - %th{style: 'width: 60px;'}= t('.order', default: 'Ordem') + %th.no_sort{style: 'width: 60px;'}= t('.order', default: 'Ordem') %th{align: 'left'}= t('.question', default: 'Pergunta') %th.no_sort{style: 'width: 80px; text-align: center;'}= t('.active', default: 'Ativo') %th.no_sort{style: 'width: 60px; text-align: center;'}= t('.preview', default: 'Prévia') %tbody - @faqs.order(:order).each do |faq| - %tr.faq-row{"data-faq-id" => faq.id} + %tr.faq-row{"data-faq-id" => faq.id, "data-faq" => {id: faq.id}.to_json, "data-question" => faq.question || t('.no_translation')} = render partial: 'faq_row', locals: { faq: faq } .text_none.empty_message{class: (@faqs.blank? ? '' : 'hide_message')}= t('.no_data', default: 'Nenhuma FAQ cadastrada') + #faq_announce.visuallyhidden{"aria-live" => "polite"} + :javascript $(function(){ // Toggle accordion para mostrar/esconder resposta @@ -36,11 +38,11 @@ $answer.toggle(); - // Trocar ícone de seta + // Rotacionar seta (direita -> baixo) if ($answer.is(':visible')) { - $icon.removeClass('icon-arrow-down').addClass('icon-arrow-up'); + $icon.removeClass('icon-arrow-right').addClass('icon-arrow-down'); } else { - $icon.removeClass('icon-arrow-up').addClass('icon-arrow-down'); + $icon.removeClass('icon-arrow-down').addClass('icon-arrow-right'); } }; From a66b9b7c9c175a594f1be20caf0ad9ef1fb10ef9 Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Mon, 22 Dec 2025 07:44:32 -0300 Subject: [PATCH 20/31] =?UTF-8?q?Adiciona=20rota=20para=20altera=C3=A7?= =?UTF-8?q?=C3=A3o=20de=20ordem=20de=20FAQs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/routes.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index 76b78db25..7224d4416 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -65,7 +65,10 @@ scope "/admin" do resources :faqs, as: 'admin_faq' do - patch :toggle_active, on: :member + member do + patch :toggle_active + put "order/:change_id", action: :order, as: :change_order + end end resources :blacklist, except: [:show, :edit, :update], controller: :user_blacklist do get :search, on: :collection From a98eb1b58ad20fda2d4e62ceb4654d1258f195b6 Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Mon, 22 Dec 2025 07:44:32 -0300 Subject: [PATCH 21/31] =?UTF-8?q?Atualiza=20tradu=C3=A7=C3=B5es=20da=20int?= =?UTF-8?q?erface=20de=20FAQs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/locales/en_US.yml | 9 +++++++++ config/locales/pt_BR.yml | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/config/locales/en_US.yml b/config/locales/en_US.yml index 25c5bc69a..0cfddf638 100644 --- a/config/locales/en_US.yml +++ b/config/locales/en_US.yml @@ -5302,6 +5302,13 @@ en_US: deactivate: Deactivate no_data: No FAQs registered confirm_delete: Are you sure you want to delete this FAQ? + up: Move up + down: Move down + up_aria: Move FAQ "%{question}" up + down_aria: Move FAQ "%{question}" down + reached_top: FAQ "%{question}" is already at the top of the list + reached_bottom: FAQ "%{question}" is already at the bottom of the list + change_position: FAQ "%{question}" moved to position %{row_position} faq_row: answer: Answer no_translation: (no translation) @@ -5312,6 +5319,8 @@ en_US: translations: FAQ Translations save: Save FAQ cancel: Cancel + error: + order: Error updating FAQ order simple_form: labels: diff --git a/config/locales/pt_BR.yml b/config/locales/pt_BR.yml index 7bf3c20fa..171b06fb7 100644 --- a/config/locales/pt_BR.yml +++ b/config/locales/pt_BR.yml @@ -5632,6 +5632,13 @@ pt_BR: deactivate: Desativar no_data: Nenhuma FAQ cadastrada confirm_delete: Tem certeza que deseja deletar esta FAQ? + up: Mover para cima + down: Mover para baixo + up_aria: Mover FAQ "%{question}" para cima + down_aria: Mover FAQ "%{question}" para baixo + reached_top: A FAQ "%{question}" já está no topo da lista + reached_bottom: A FAQ "%{question}" já está no final da lista + change_position: FAQ "%{question}" movida para a posição %{row_position} faq_row: answer: Resposta no_translation: (sem tradução) @@ -5642,6 +5649,8 @@ pt_BR: translations: Traduções do FAQ save: Salvar FAQ cancel: Cancelar + error: + order: Erro ao atualizar a ordem da FAQ simple_form: labels: From deffb43974a909b13a31d4c7a2b21184f54482be Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Mon, 22 Dec 2025 07:52:55 -0300 Subject: [PATCH 22/31] =?UTF-8?q?Corrige=20problema=20de=20sele=C3=A7?= =?UTF-8?q?=C3=A3o=20ap=C3=B3s=20toggle=20de=20status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/faqs/toggle_active.js.haml | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/app/views/faqs/toggle_active.js.haml b/app/views/faqs/toggle_active.js.haml index 3e2744c88..46c2b4658 100644 --- a/app/views/faqs/toggle_active.js.haml +++ b/app/views/faqs/toggle_active.js.haml @@ -1,2 +1,23 @@ -$("tr.faq-row[data-faq-id='#{@faq.id}']").html('#{j render partial: 'faq_row', locals: { faq: @faq }}'); +// Salvar estado do checkbox antes de atualizar +var $row = $("tr.faq-row[data-faq-id='#{@faq.id}']"); +var wasChecked = $row.find('.ckb_faq').is(':checked'); + +// Atualizar conteúdo da linha +$row.html('#{j render partial: 'faq_row', locals: { faq: @faq }}'); + +// Restaurar estado do checkbox +if (wasChecked) { + $row.find('.ckb_faq').prop('checked', true); +} + +// Reativar event listeners para o novo checkbox +$row.find('.ckb_faq').on('change', function() { + toggleActionButtons(); +}); + +// Atualizar botões de ação +if (typeof toggleActionButtons === 'function') { + toggleActionButtons(); +} + flash_message('Status atualizado com sucesso!', 'notice'); From 66400efe7f6b5e409131327966515b2bb860d7d8 Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Mon, 22 Dec 2025 07:54:38 -0300 Subject: [PATCH 23/31] Corrige sintaxe HAML no arquivo de toggle --- app/views/faqs/toggle_active.js.haml | 37 ++++++++++++++-------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/app/views/faqs/toggle_active.js.haml b/app/views/faqs/toggle_active.js.haml index 46c2b4658..dcfdfbc46 100644 --- a/app/views/faqs/toggle_active.js.haml +++ b/app/views/faqs/toggle_active.js.haml @@ -1,23 +1,24 @@ -// Salvar estado do checkbox antes de atualizar -var $row = $("tr.faq-row[data-faq-id='#{@faq.id}']"); -var wasChecked = $row.find('.ckb_faq').is(':checked'); +:plain + // Salvar estado do checkbox antes de atualizar + var $row = $("tr.faq-row[data-faq-id='#{@faq.id}']"); + var wasChecked = $row.find('.ckb_faq').is(':checked'); -// Atualizar conteúdo da linha -$row.html('#{j render partial: 'faq_row', locals: { faq: @faq }}'); + // Atualizar conteúdo da linha + $row.html('#{j render partial: 'faq_row', locals: { faq: @faq }}'); -// Restaurar estado do checkbox -if (wasChecked) { - $row.find('.ckb_faq').prop('checked', true); -} + // Restaurar estado do checkbox + if (wasChecked) { + $row.find('.ckb_faq').prop('checked', true); + } -// Reativar event listeners para o novo checkbox -$row.find('.ckb_faq').on('change', function() { - toggleActionButtons(); -}); + // Reativar event listeners para o novo checkbox + $row.find('.ckb_faq').on('change', function() { + toggleActionButtons(); + }); -// Atualizar botões de ação -if (typeof toggleActionButtons === 'function') { - toggleActionButtons(); -} + // Atualizar botões de ação + if (typeof toggleActionButtons === 'function') { + toggleActionButtons(); + } -flash_message('Status atualizado com sucesso!', 'notice'); + flash_message('Status atualizado com sucesso!', 'notice'); From b70260ad36ee0586f99fe8762bea1e037830bb28 Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Mon, 22 Dec 2025 07:55:54 -0300 Subject: [PATCH 24/31] =?UTF-8?q?Ajusta=20interpola=C3=A7=C3=A3o=20Ruby=20?= =?UTF-8?q?no=20arquivo=20JS=20do=20toggle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/faqs/toggle_active.js.haml | 31 +++++++++++----------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/app/views/faqs/toggle_active.js.haml b/app/views/faqs/toggle_active.js.haml index dcfdfbc46..a4bfc4c0b 100644 --- a/app/views/faqs/toggle_active.js.haml +++ b/app/views/faqs/toggle_active.js.haml @@ -1,24 +1,17 @@ -:plain - // Salvar estado do checkbox antes de atualizar - var $row = $("tr.faq-row[data-faq-id='#{@faq.id}']"); - var wasChecked = $row.find('.ckb_faq').is(':checked'); +- # Salvar estado do checkbox antes de atualizar +var $row = $("tr.faq-row[data-faq-id='#{@faq.id}']"); +var wasChecked = $row.find('.ckb_faq').is(':checked'); - // Atualizar conteúdo da linha - $row.html('#{j render partial: 'faq_row', locals: { faq: @faq }}'); +- # Atualizar conteúdo da linha +$row.html('#{j render partial: 'faq_row', locals: { faq: @faq }}'); - // Restaurar estado do checkbox - if (wasChecked) { - $row.find('.ckb_faq').prop('checked', true); - } +- # Restaurar estado do checkbox +if (wasChecked) { $row.find('.ckb_faq').prop('checked', true); } - // Reativar event listeners para o novo checkbox - $row.find('.ckb_faq').on('change', function() { - toggleActionButtons(); - }); +- # Reativar event listeners para o novo checkbox +$row.find('.ckb_faq').on('change', function() { toggleActionButtons(); }); - // Atualizar botões de ação - if (typeof toggleActionButtons === 'function') { - toggleActionButtons(); - } +- # Atualizar botões de ação +if (typeof toggleActionButtons === 'function') { toggleActionButtons(); } - flash_message('Status atualizado com sucesso!', 'notice'); +flash_message('Status atualizado com sucesso!', 'notice'); From a8b35322ce4261a2bbc9fb5a9c7f56077857d179 Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Mon, 22 Dec 2025 07:57:27 -0300 Subject: [PATCH 25/31] =?UTF-8?q?Torna=20fun=C3=A7=C3=A3o=20toggleActionBu?= =?UTF-8?q?ttons=20global=20para=20acesso=20via=20AJAX?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/faqs/index.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/faqs/index.html.haml b/app/views/faqs/index.html.haml index ba783b6ab..3c291662d 100644 --- a/app/views/faqs/index.html.haml +++ b/app/views/faqs/index.html.haml @@ -67,7 +67,7 @@ toggleActionButtons(); }); - function toggleActionButtons() { + window.toggleActionButtons = function() { var checkedBoxes = $('.ckb_faq:checked'); var $editBtn = $('.btn_edit[data-link-edit]'); var $deleteBtn = $('.delete_faq[data-link-delete]'); @@ -89,7 +89,7 @@ $editBtn.attr('disabled', true).attr('href', '#void'); $deleteBtn.attr('disabled', false); } - } + }; // Confirmar deleção $('.delete_faq').on('click', function(e) { From f019e9546e74c60a0090d3d43fab7c533b50dc3c Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Mon, 22 Dec 2025 08:04:09 -0300 Subject: [PATCH 26/31] =?UTF-8?q?Altera=20=C3=ADcone=20do=20accordion=20de?= =?UTF-8?q?=20seta=20para=20chevron?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/faqs/_faq_row.html.haml | 2 +- app/views/faqs/index.html.haml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/faqs/_faq_row.html.haml b/app/views/faqs/_faq_row.html.haml index 8627be3b8..f518dc354 100644 --- a/app/views/faqs/_faq_row.html.haml +++ b/app/views/faqs/_faq_row.html.haml @@ -5,7 +5,7 @@ .arrow_down= button_tag content_tag(:i, nil, class: 'icon-arrow-down-triangle'), class: 'down btn_arrow', :"aria-label" => t('faqs.index.down_aria', question: faq.question || 'FAQ'), :"data-tooltip" => t('faqs.index.down', default: 'Mover para baixo') %td = link_to '#void', onclick: 'toggle_answer(this, event)', onkeydown: 'click_on_keypress(event, this);', class: 'faq-question', style: 'display: flex; align-items: center;' do - = content_tag(:i, nil, class: 'icon-arrow-right', style: 'margin-right: 8px; transition: transform 0.3s;', :'aria-hidden' => 'true') + = content_tag(:i, nil, class: 'icon-arrow-down-triangle', style: 'margin-right: 8px; transition: transform 0.3s;', :'aria-hidden' => 'true') %span= faq.question || t('.no_translation') .faq-answer.invisible{style: 'margin-top: 10px; padding: 10px; background: #f5f5f5; border-left: 3px solid #ccc;'} %strong= t('.answer', default: 'Resposta:') diff --git a/app/views/faqs/index.html.haml b/app/views/faqs/index.html.haml index 3c291662d..5d9d6bbf3 100644 --- a/app/views/faqs/index.html.haml +++ b/app/views/faqs/index.html.haml @@ -38,11 +38,11 @@ $answer.toggle(); - // Rotacionar seta (direita -> baixo) + // Alternar chevron (baixo -> cima) if ($answer.is(':visible')) { - $icon.removeClass('icon-arrow-right').addClass('icon-arrow-down'); + $icon.removeClass('icon-arrow-down-triangle').addClass('icon-arrow-up-triangle'); } else { - $icon.removeClass('icon-arrow-down').addClass('icon-arrow-right'); + $icon.removeClass('icon-arrow-up-triangle').addClass('icon-arrow-down-triangle'); } }; From fa9b98df6da9c30069df16e28a172e17d24f86df Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Mon, 22 Dec 2025 08:11:01 -0300 Subject: [PATCH 27/31] Esconde tooltip ao alternar status do FAQ --- app/views/faqs/toggle_active.js.haml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/views/faqs/toggle_active.js.haml b/app/views/faqs/toggle_active.js.haml index a4bfc4c0b..3a2f92795 100644 --- a/app/views/faqs/toggle_active.js.haml +++ b/app/views/faqs/toggle_active.js.haml @@ -2,6 +2,9 @@ var $row = $("tr.faq-row[data-faq-id='#{@faq.id}']"); var wasChecked = $row.find('.ckb_faq').is(':checked'); +- # Esconder tooltip antes de atualizar +$row.find('.change_faq_status').qtip('hide'); + - # Atualizar conteúdo da linha $row.html('#{j render partial: 'faq_row', locals: { faq: @faq }}'); From 9d7cad571d1db5f8e02ce810eac434b5a068b771 Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Fri, 9 Jan 2026 13:34:50 -0300 Subject: [PATCH 28/31] =?UTF-8?q?Implementar=20formul=C3=A1rio=20FAQ=20em?= =?UTF-8?q?=20popup=20com=20fancybox?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Configurar controller para desabilitar layout nas actions new/edit/show - Atualizar formulário para funcionar como popup - Adicionar CKEditor ao campo de resposta - Implementar submit via AJAX com função faq_save() - Configurar links para abrir em popup (Novo/Editar/Visualizar) - Formatar view de visualização para popup - Adicionar traduções para mensagens de sucesso --- app/assets/javascripts/faqs.js.erb | 71 +++++++++++++++++++++++++++++ app/controllers/faqs_controller.rb | 17 ++++--- app/views/faqs/_faq_row.html.haml | 4 +- app/views/faqs/_form.html.haml | 72 ++++++++++++++++-------------- app/views/faqs/index.html.haml | 4 +- app/views/faqs/show.html.haml | 72 +++++++++++++++--------------- config/locales/en_US.yml | 13 +++++- config/locales/pt_BR.yml | 14 +++++- 8 files changed, 184 insertions(+), 83 deletions(-) diff --git a/app/assets/javascripts/faqs.js.erb b/app/assets/javascripts/faqs.js.erb index 2ec77fd2e..92cc46baa 100644 --- a/app/assets/javascripts/faqs.js.erb +++ b/app/assets/javascripts/faqs.js.erb @@ -1,4 +1,12 @@ <%# @encoding: UTF-8 %> + +// FAQ Save - Função para salvar FAQ via AJAX (usada no fancybox) +function faq_save(){ + $('form#faq_form').serialize_and_submit({ + replace_list: $('.list_faqs') + }); +} + // FAQ Form - Adicionar/Remover traduções dinamicamente $(function() { @@ -14,6 +22,17 @@ $(function() { var translationIndex = $('#translations-container .translation-fields').length; console.log('Número de traduções existentes:', translationIndex); + // Função para inicializar CKEditor em um campo específico + function initCKEditor(textareaId) { + if (typeof CKEDITOR !== 'undefined') { + // Se já existe instância, destrói antes de recriar + if (CKEDITOR.instances[textareaId]) { + CKEDITOR.instances[textareaId].destroy(); + } + CKEDITOR.replace(textareaId); + } + } + // Função para adicionar nova tradução $('#add-translation').click(function(e) { e.preventDefault(); @@ -23,11 +42,16 @@ $(function() { var $template = $('#translations-container .translation-fields').first().clone(); console.log('Template clonado'); + // Remove qualquer div do CKEditor que possa ter sido clonada + $template.find('.cke').remove(); + // Limpa os valores dos campos $template.find('input[type="text"], textarea, select').val(''); $template.find('input[type="hidden"]').not('.destroy-flag').val(''); $template.find('.destroy-flag').val('false'); + var newAnswerId; + // Atualiza os IDs e nomes dos campos para usar novo índice $template.find('input, select, textarea').each(function() { var $field = $(this); @@ -42,6 +66,11 @@ $(function() { if ($field.attr('id')) { var newId = $field.attr('id').replace(/_\d+_/, '_' + translationIndex + '_'); $field.attr('id', newId); + + // Guarda o ID do campo answer para inicializar CKEditor depois + if ($field.attr('name') && $field.attr('name').indexOf('[answer]') > -1) { + newAnswerId = newId; + } } }); @@ -64,6 +93,13 @@ $(function() { // Adiciona o novo campo ao container $('#translations-container').append($template); + // Inicializa o CKEditor no novo campo de resposta + if (newAnswerId) { + setTimeout(function() { + initCKEditor(newAnswerId); + }, 100); + } + // Incrementa o índice translationIndex++; @@ -84,6 +120,15 @@ $(function() { return; } + // Encontra o textarea do campo answer e destrói a instância do CKEditor + var $answerField = $translationField.find('textarea[name*="[answer]"]'); + if ($answerField.length > 0 && $answerField.attr('id')) { + var editorId = $answerField.attr('id'); + if (typeof CKEDITOR !== 'undefined' && CKEDITOR.instances[editorId]) { + CKEDITOR.instances[editorId].destroy(); + } + } + // Se o registro já existe no banco (tem ID), marca para destruição var $idField = $translationField.find('input[name*="[id]"]'); if ($idField.length > 0 && $idField.val() !== '') { @@ -116,6 +161,32 @@ $(function() { updateRemoveButtons(); }); +// FAQ Fancybox - Configurar links para abrir formulários em popup +jQuery(function ($) { + // Configurar link "Novo FAQ" para abrir em fancybox + $(".link_new_faq").call_fancybox({ + width: '70%' + }); + + // Configurar link "Visualizar FAQ" para abrir em fancybox + $(".link_show_faq").call_fancybox({ + width: '70%' + }); + + // Configurar link "Editar FAQ" para abrir em fancybox + $(".btn_edit[data-link-edit]").on('click', function(e) { + e.preventDefault(); // Previne navegação padrão do link + + var checkedBoxes = $('.ckb_faq:checked'); + if (checkedBoxes.length === 1 && !$(this).attr('disabled')) { + var faqId = checkedBoxes.first().val(); + var editUrl = $(this).data('link-edit').replace(':id', faqId); + $(this).call_fancybox({href: editUrl, open: true, width: '70%'}); + } + return false; + }); +}); + // FAQ Ordering - Ordenação com setas de acessibilidade jQuery(function ($) { // Só executa se estiver na página de listagem de FAQs diff --git a/app/controllers/faqs_controller.rb b/app/controllers/faqs_controller.rb index e9ff24ba2..138cb594a 100644 --- a/app/controllers/faqs_controller.rb +++ b/app/controllers/faqs_controller.rb @@ -1,6 +1,8 @@ class FaqsController < ApplicationController #layout 'login', only: [:apresentation] + layout false, except: [:index, :apresentation] + # Apenas administradores podem acessar before_action :require_admin @@ -29,9 +31,10 @@ def new def create @faq = Faq.new(faq_params) - if @faq.save - redirect_to admin_faq_path(@faq), notice: 'FAQ criado com sucesso.' - else + begin + @faq.save! + render json: {success: true, notice: t('faqs.success.created')} + rescue render :new end end @@ -44,10 +47,10 @@ def edit # PATCH/PUT /admin/faqs/1 - Atualiza FAQ existente def update - # Atualiza FAQ com dados do formulário - if @faq.update(faq_params) - redirect_to admin_faq_path(@faq), notice: 'FAQ atualizado com sucesso.' - else + begin + @faq.update!(faq_params) + render json: {success: true, notice: t('faqs.success.updated')} + rescue render :edit end end diff --git a/app/views/faqs/_faq_row.html.haml b/app/views/faqs/_faq_row.html.haml index f518dc354..00e8198cd 100644 --- a/app/views/faqs/_faq_row.html.haml +++ b/app/views/faqs/_faq_row.html.haml @@ -4,7 +4,7 @@ .arrow_up= button_tag content_tag(:i, nil, class: 'icon-arrow-up-triangle'), class: 'up btn_arrow', :"aria-label" => t('faqs.index.up_aria', question: faq.question || 'FAQ'), :"data-tooltip" => t('faqs.index.up', default: 'Mover para cima') .arrow_down= button_tag content_tag(:i, nil, class: 'icon-arrow-down-triangle'), class: 'down btn_arrow', :"aria-label" => t('faqs.index.down_aria', question: faq.question || 'FAQ'), :"data-tooltip" => t('faqs.index.down', default: 'Mover para baixo') %td - = link_to '#void', onclick: 'toggle_answer(this, event)', onkeydown: 'click_on_keypress(event, this);', class: 'faq-question', style: 'display: flex; align-items: center;' do + = link_to '#void', onclick: 'toggle_answer(this, event)', onkeydown: 'click_on_keypress(event, this);', class: 'faq-question', style: 'display: flex; align-items: center; text-decoration:none' do = content_tag(:i, nil, class: 'icon-arrow-down-triangle', style: 'margin-right: 8px; transition: transform 0.3s;', :'aria-hidden' => 'true') %span= faq.question || t('.no_translation') .faq-answer.invisible{style: 'margin-top: 10px; padding: 10px; background: #f5f5f5; border-left: 3px solid #ccc;'} @@ -15,5 +15,5 @@ - tooltip_text = faq.active ? t('.deactivate', default: 'Desativar') : t('.activate', default: 'Ativar') = link_to image_tag(img_status), toggle_active_admin_faq_path(faq), method: :patch, remote: true, class: 'change_faq_status', :'data-faq-id' => faq.id, :'data-tooltip' => tooltip_text %td{style: 'text-align: center;'} - = link_to admin_faq_path(faq), class: 'btn', :'data-tooltip' => t('.show', default: 'Visualizar') do + = link_to admin_faq_path(faq), class: 'btn link_show_faq', :'data-tooltip' => t('.show', default: 'Visualizar') do = content_tag(:i, nil, class: 'icon-eye', :'aria-hidden' => 'true') diff --git a/app/views/faqs/_form.html.haml b/app/views/faqs/_form.html.haml index 144405006..327ebce63 100644 --- a/app/views/faqs/_form.html.haml +++ b/app/views/faqs/_form.html.haml @@ -1,46 +1,50 @@ -.new_faq.controller - = simple_form_for @faq, url: (@faq.new_record? ? admin_faq_index_path : admin_faq_path(@faq)), html: { id: 'faq_form' } do |f| += simple_form_for @faq, url: (@faq.new_record? ? admin_faq_index_path : admin_faq_path(@faq)), html: { id: 'faq_form' } do |f| - %h1#lightBoxDialogTitle= t("faqs.#{@faq.persisted? ? 'edit' : 'new'}") - %span.form_requirement= t(:required_fields) + %h1#lightBoxDialogTitle= t("faqs.#{@faq.persisted? ? 'edit' : 'new'}") + %span.form_requirement= t(:required_fields) - .form-inputs.block_content.faq_box#basic_info + .form-inputs.block_content.faq_box#basic_info - -# Campos do FAQ (order e active) - SimpleForm busca labels automaticamente - = f.input :order, required: true, input_html: { value: @faq.order || 0 } - = f.input :active, as: :radio_buttons, required: true + -# Campos do FAQ (order e active) - SimpleForm busca labels automaticamente + = f.input :order, required: true, input_html: { value: @faq.order || 0 } + = f.input :active, as: :radio_buttons, required: true - %hr - %br + %hr + %br - %h2= t('faqs.form.translations') + %h2= t('faqs.form.translations') - -# NESTED FORM: Campos das traduções usando simple_fields_for - #translations-container - = f.simple_fields_for :faq_translations do |translation_form| - .translation-fields{style: "border: solid 1px #ccc; padding: 20px; margin-bottom: 20px; background: #f9f9f9; position: relative;"} + -# NESTED FORM: Campos das traduções usando simple_fields_for + #translations-container + = f.simple_fields_for :faq_translations do |translation_form| + .translation-fields{style: "border: solid 1px #ccc; padding: 20px; margin-bottom: 20px; background: #f9f9f9; position: relative;"} - -# Botão para remover tradução (só aparece se tiver mais de 2) - - if f.object.faq_translations.size > 2 - %button.remove-translation{type: 'button', style: 'position: absolute; top: 10px; right: 10px; background: #d9534f; color: white; border: none; padding: 5px 10px; cursor: pointer; border-radius: 3px;'} - × Remover + -# Botão para remover tradução (só aparece se tiver mais de 2) + - if f.object.faq_translations.size > 2 + %button.remove-translation{type: 'button', style: 'position: absolute; top: 10px; right: 10px; background: #d9534f; color: white; border: none; padding: 5px 10px; cursor: pointer; border-radius: 3px;'} + × Remover - -# SELECT de idioma (pode escolher qualquer um da lista) - = translation_form.input :locale, - collection: [['🇧🇷 Português (Brasil)', 'pt_BR'], ['🇺🇸 English (US)', 'en_US'], ['🇪🇸 Español', 'es_ES'], ['🇫🇷 Français', 'fr_FR'], ['🇩🇪 Deutsch', 'de_DE'], ['🇮🇹 Italiano', 'it_IT']], - required: true + -# SELECT de idioma (pode escolher qualquer um da lista) + = translation_form.input :locale, + collection: [['🇧🇷 Português (Brasil)', 'pt_BR'], ['🇺🇸 English (US)', 'en_US'], ['🇪🇸 Español', 'es_ES'], ['🇫🇷 Français', 'fr_FR'], ['🇩🇪 Deutsch', 'de_DE'], ['🇮🇹 Italiano', 'it_IT']], + required: true - -# Campos: Pergunta e Resposta - SimpleForm busca labels automaticamente - = translation_form.input :question, required: true - = translation_form.input :answer, as: :text, required: true, input_html: { rows: 5 } + -# Campos: Pergunta e Resposta - SimpleForm busca labels automaticamente + = translation_form.input :question, required: true + = translation_form.input :answer, as: :ckeditor, required: true - -# Campo hidden para marcar tradução para destruição - = translation_form.input :_destroy, as: :hidden, input_html: { class: 'destroy-flag' } + -# Campo hidden para marcar tradução para destruição + = translation_form.input :_destroy, as: :hidden, input_html: { class: 'destroy-flag' } - -# Botão para adicionar nova tradução - %button#add-translation.btn.btn_default{type: 'button', style: 'margin-bottom: 20px;'} - + Adicionar outra língua + -# Botão para adicionar nova tradução + %button#add-translation.btn.btn_default{type: 'button', style: 'margin-bottom: 20px;'} + + Adicionar outra língua - .form-actions - = f.button :submit, t('faqs.form.save'), class: 'btn btn_main' - = link_to t('faqs.form.cancel'), admin_faq_index_path, class: 'btn btn_caution' + .form-actions.right_buttons + = button_tag t('faqs.form.cancel'), type: 'button', onclick: "jQuery.fancybox.close()", class: 'btn btn_default btn_lightbox', alt: t('faqs.form.cancel') + = button_tag t('faqs.form.save'), type: 'button', onclick: "faq_save()", class: 'btn btn_main btn_lightbox', alt: t('faqs.form.save') + += javascript_include_tag 'ckeditor/init' + +:javascript + CKEDITOR_BASEPATH = "#{request.env['RAILS_RELATIVE_URL_ROOT']}/assets/ckeditor/"; diff --git a/app/views/faqs/index.html.haml b/app/views/faqs/index.html.haml index 5d9d6bbf3..a80464d00 100644 --- a/app/views/faqs/index.html.haml +++ b/app/views/faqs/index.html.haml @@ -4,7 +4,7 @@ .block_content_toolbar .block_toolbar_left.btn-group - = link_to content_tag(:i, nil, class: 'icon-plus'), new_admin_faq_path, class: 'btn btn_main', :'data-tooltip' => t('.new', default: 'Novo FAQ') + = link_to content_tag(:i, nil, class: 'icon-plus'), new_admin_faq_path, class: 'btn btn_main link_new_faq', :'data-tooltip' => t('.new', default: 'Novo FAQ') .block_toolbar_right .btn-group @@ -150,3 +150,5 @@ }); } }); + += javascript_include_tag 'faqs' diff --git a/app/views/faqs/show.html.haml b/app/views/faqs/show.html.haml index 73021679e..4086a7ade 100644 --- a/app/views/faqs/show.html.haml +++ b/app/views/faqs/show.html.haml @@ -1,37 +1,35 @@ -%p#notice= notice - -%h1 FAQ ##{@faq.id} - -%p - %b Ordem: - = @faq.order - -%p - %b Ativo: - = @faq.active ? "Sim" : "Não" - -%hr - -%h2 🇧🇷 Português -%p - %b Pergunta: - = @faq.translation_pt&.question || "(sem tradução)" -%p - %b Resposta: - = simple_format(@faq.translation_pt&.answer || "(sem tradução)") - -%hr - -%h2 🇺🇸 English -%p - %b Question: - = @faq.translation_en&.question || "(no translation)" -%p - %b Answer: - = simple_format(@faq.translation_en&.answer || "(no translation)") - -%hr - -= link_to 'Editar', edit_admin_faq_path(@faq) -\| -= link_to 'Voltar', admin_faq_index_path +%h1#lightBoxDialogTitle= t('faqs.show.title', default: 'Visualizar FAQ') + +.form-inputs.block_content.faq_box + .faq-info{style: 'margin-bottom: 20px;'} + %p + %strong #{t('activerecord.attributes.faq.order')}: + = @faq.order + %p + %strong #{t('activerecord.attributes.faq.active')}: + = @faq.active ? t(:yes, default: 'Sim') : t(:no, default: 'Não') + + %hr + + %h2{style: 'margin-top: 20px;'} 🇧🇷 Português (Brasil) + .translation-content{style: 'margin-bottom: 20px; padding: 15px; background: #f9f9f9; border-left: 3px solid #4CAF50;'} + %p + %strong #{t('activerecord.attributes.faq_translation.question')}: + = @faq.translation_pt&.question || t('faqs.index.no_translation', default: '(sem tradução)') + %p + %strong #{t('activerecord.attributes.faq_translation.answer')}: + .answer-content= raw(@faq.translation_pt&.answer || t('faqs.index.no_translation', default: '(sem tradução)')) + + %hr + + %h2{style: 'margin-top: 20px;'} 🇺🇸 English (US) + .translation-content{style: 'margin-bottom: 20px; padding: 15px; background: #f9f9f9; border-left: 3px solid #2196F3;'} + %p + %strong #{t('activerecord.attributes.faq_translation.question')}: + = @faq.translation_en&.question || t('faqs.index.no_translation', default: '(no translation)') + %p + %strong #{t('activerecord.attributes.faq_translation.answer')}: + .answer-content= raw(@faq.translation_en&.answer || t('faqs.index.no_translation', default: '(no translation)')) + +.form-actions.right_buttons + = button_tag t('faqs.form.cancel', default: 'Fechar'), type: 'button', onclick: "jQuery.fancybox.close()", class: 'btn btn_default btn_lightbox' diff --git a/config/locales/en_US.yml b/config/locales/en_US.yml index 3daba2e8a..bcef4dedd 100644 --- a/config/locales/en_US.yml +++ b/config/locales/en_US.yml @@ -5287,6 +5287,8 @@ en_US: faqs: new: New FAQ edit: Edit FAQ + show: + title: View FAQ index: title: Frequently Asked Questions (FAQ) new: New FAQ @@ -5318,8 +5320,11 @@ en_US: show: View form: translations: FAQ Translations - save: Save FAQ + save: Save cancel: Cancel + success: + created: FAQ created successfully + updated: FAQ updated successfully error: order: Error updating FAQ order @@ -5348,3 +5353,9 @@ en_US: locale: Language question: Question answer: Answer + errors: + models: + faq: + attributes: + order: + taken: is already in use by another active FAQ diff --git a/config/locales/pt_BR.yml b/config/locales/pt_BR.yml index cf739b643..bd19de79e 100644 --- a/config/locales/pt_BR.yml +++ b/config/locales/pt_BR.yml @@ -5617,6 +5617,9 @@ pt_BR: faqs: new: Novo FAQ edit: Editar FAQ + show: + order: Ordem + title: Visualizar FAQ index: title: Perguntas Frequentes (FAQ) new: Novo FAQ @@ -5648,8 +5651,11 @@ pt_BR: show: Visualizar form: translations: Traduções do FAQ - save: Salvar FAQ + save: Salvar cancel: Cancelar + success: + created: FAQ criado com sucesso + updated: FAQ atualizado com sucesso error: order: Erro ao atualizar a ordem da FAQ @@ -5678,3 +5684,9 @@ pt_BR: locale: Idioma question: Pergunta answer: Resposta + errors: + models: + faq: + attributes: + order: + taken: já está em uso por outro FAQ ativo From e93b9ce9961f1cc129bea48e4cc94787c61b841c Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Fri, 9 Jan 2026 13:35:06 -0300 Subject: [PATCH 29/31] =?UTF-8?q?Adicionar=20valida=C3=A7=C3=A3o=20de=20or?= =?UTF-8?q?dem=20=C3=BAnica=20para=20FAQs=20ativos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Criar validação unique_order_for_active_faqs no modelo - Impedir que FAQs ativos tenham a mesma ordem - Permitir que FAQs inativos tenham qualquer ordem - Adicionar mensagem de erro ao tentar usar ordem duplicada --- app/models/faq.rb | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/app/models/faq.rb b/app/models/faq.rb index ec3bfa8b6..eca0f6d04 100644 --- a/app/models/faq.rb +++ b/app/models/faq.rb @@ -4,6 +4,7 @@ class Faq < ApplicationRecord accepts_nested_attributes_for :faq_translations, allow_destroy: true validates :order, presence: true, numericality: {only_integer: true} validates :active, inclusion: {in: [true, false]} + validate :unique_order_for_active_faqs before_save :reorder_faqs_if_order_changed @@ -37,6 +38,26 @@ def translation_en private + # ========================================================================= + # VALIDAÇÃO: Ordem única para FAQs ativos + # ========================================================================= + # Garante que FAQs ativos não possam ter a mesma ordem + # FAQs inativos podem ter qualquer ordem (não aparecem na listagem pública) + # ========================================================================= + def unique_order_for_active_faqs + # Só valida se o FAQ for ativo + return unless active + + # Busca por outro FAQ ativo com a mesma ordem (excluindo o próprio) + duplicate = Faq.where(active: true, order: order) + .where.not(id: id) + .exists? + + if duplicate + errors.add(:order, 'já está em uso por outro FAQ ativo') + end + end + # ========================================================================= # REORDENAÇÃO AUTOMÁTICA # ========================================================================= From d5702e0dbaa03c1a377a62d077aa2b992a7219c1 Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Fri, 23 Jan 2026 08:24:00 -0300 Subject: [PATCH 30/31] =?UTF-8?q?fix(faq):=20corre=C3=A7=C3=B5es=20gerais?= =?UTF-8?q?=20e=20melhorias=20de=20acessibilidade?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Internacionalização (pt_BR e en_US) - Radio button ativo alinhado - Radio com "sim" default - Redução do input number - Aumentar o tamanho do input pergunta - Remover links duplicados - Ajuste do fancybox - Faq Editar dentro do fancybox - Alto contraste - Visual do status muda mas o comportamento não - Erro na ordenação do faq - Botão adicionar uma nova língua não funciona - Descrição aria-label (acessibilidade para o accordion) - Deletar não funcional --- app/assets/javascripts/faqs.js.erb | 219 ++++++++++++--------------- app/controllers/faqs_controller.rb | 39 +++-- app/views/faqs/_faq_row.html.haml | 13 +- app/views/faqs/_form.html.haml | 16 +- app/views/faqs/index.html.haml | 61 ++++---- app/views/faqs/toggle_active.js.haml | 35 +++-- config/initializers/assets.rb | 2 +- config/locales/en_US.yml | 24 +++ config/locales/pt_BR.yml | 31 ++++ 9 files changed, 250 insertions(+), 190 deletions(-) diff --git a/app/assets/javascripts/faqs.js.erb b/app/assets/javascripts/faqs.js.erb index 92cc46baa..54956806b 100644 --- a/app/assets/javascripts/faqs.js.erb +++ b/app/assets/javascripts/faqs.js.erb @@ -2,163 +2,136 @@ // FAQ Save - Função para salvar FAQ via AJAX (usada no fancybox) function faq_save(){ + // Sincronizar todos os CKEditors antes de enviar o formulário + if (typeof CKEDITOR !== 'undefined') { + for (var instanceName in CKEDITOR.instances) { + CKEDITOR.instances[instanceName].updateElement(); + } + } + $('form#faq_form').serialize_and_submit({ replace_list: $('.list_faqs') }); } // FAQ Form - Adicionar/Remover traduções dinamicamente +// Usa delegação de eventos para funcionar dentro do fancybox (elementos carregados via AJAX) -$(function() { - - // Verifica se estamos na página de FAQ - if ($('#translations-container').length === 0) { - return; // Não está na página de FAQ, não faz nada +// Função para inicializar CKEditor em um campo específico +function initFaqCKEditor(textareaId) { + if (typeof CKEDITOR !== 'undefined') { + if (CKEDITOR.instances[textareaId]) { + CKEDITOR.instances[textareaId].destroy(); + } + CKEDITOR.replace(textareaId); } +} - console.log('FAQ Form JavaScript carregado'); +// Função para atualizar botões de remover +function updateFaqRemoveButtons() { + var $visibleFields = $('#translations-container .translation-fields:visible'); + var visibleCount = $visibleFields.length; - // Contador para índices únicos dos novos campos - var translationIndex = $('#translations-container .translation-fields').length; - console.log('Número de traduções existentes:', translationIndex); - - // Função para inicializar CKEditor em um campo específico - function initCKEditor(textareaId) { - if (typeof CKEDITOR !== 'undefined') { - // Se já existe instância, destrói antes de recriar - if (CKEDITOR.instances[textareaId]) { - CKEDITOR.instances[textareaId].destroy(); - } - CKEDITOR.replace(textareaId); - } + if (visibleCount <= 2) { + $visibleFields.find('.remove-translation').hide(); + } else { + $visibleFields.find('.remove-translation').show(); } +} - // Função para adicionar nova tradução - $('#add-translation').click(function(e) { - e.preventDefault(); - console.log('Botão "Adicionar outra língua" clicado!'); +// Evento: Adicionar nova tradução (delegação de eventos para funcionar no fancybox) +// Usa namespace .faqForm e .off() para evitar handlers duplicados +$(document).off('click.faqForm', '#add-translation').on('click.faqForm', '#add-translation', function(e) { + e.preventDefault(); + e.stopImmediatePropagation(); - // Pega o primeiro campo de tradução como template - var $template = $('#translations-container .translation-fields').first().clone(); - console.log('Template clonado'); + var translationIndex = $('#translations-container .translation-fields').length; + var $template = $('#translations-container .translation-fields').first().clone(); - // Remove qualquer div do CKEditor que possa ter sido clonada - $template.find('.cke').remove(); + // Remove CKEditor clonado + $template.find('.cke').remove(); - // Limpa os valores dos campos - $template.find('input[type="text"], textarea, select').val(''); - $template.find('input[type="hidden"]').not('.destroy-flag').val(''); - $template.find('.destroy-flag').val('false'); + // Limpa valores + $template.find('input[type="text"], textarea, select').val(''); + $template.find('input[type="hidden"]').not('.destroy-flag').val(''); + $template.find('.destroy-flag').val('false'); - var newAnswerId; + var newAnswerId; - // Atualiza os IDs e nomes dos campos para usar novo índice - $template.find('input, select, textarea').each(function() { - var $field = $(this); + // Atualiza IDs e nomes + $template.find('input, select, textarea').each(function() { + var $field = $(this); - // Atualiza o name - if ($field.attr('name')) { - var newName = $field.attr('name').replace(/\[\d+\]/, '[' + translationIndex + ']'); - $field.attr('name', newName); - } + if ($field.attr('name')) { + $field.attr('name', $field.attr('name').replace(/\[\d+\]/, '[' + translationIndex + ']')); + } - // Atualiza o id - if ($field.attr('id')) { - var newId = $field.attr('id').replace(/_\d+_/, '_' + translationIndex + '_'); - $field.attr('id', newId); + if ($field.attr('id')) { + var newId = $field.attr('id').replace(/_\d+_/, '_' + translationIndex + '_'); + $field.attr('id', newId); - // Guarda o ID do campo answer para inicializar CKEditor depois - if ($field.attr('name') && $field.attr('name').indexOf('[answer]') > -1) { - newAnswerId = newId; - } - } - }); - - // Atualiza os labels para apontar para os novos IDs - $template.find('label').each(function() { - var $label = $(this); - if ($label.attr('for')) { - var newFor = $label.attr('for').replace(/_\d+_/, '_' + translationIndex + '_'); - $label.attr('for', newFor); + if ($field.attr('name') && $field.attr('name').indexOf('[answer]') > -1) { + newAnswerId = newId; } - }); - - // Adiciona o botão de remover (sempre visível nos novos campos) - if ($template.find('.remove-translation').length === 0) { - $template.prepend( - '' - ); } + }); - // Adiciona o novo campo ao container - $('#translations-container').append($template); - - // Inicializa o CKEditor no novo campo de resposta - if (newAnswerId) { - setTimeout(function() { - initCKEditor(newAnswerId); - }, 100); + // Atualiza labels + $template.find('label').each(function() { + if ($(this).attr('for')) { + $(this).attr('for', $(this).attr('for').replace(/_\d+_/, '_' + translationIndex + '_')); } - - // Incrementa o índice - translationIndex++; - - // Atualiza os botões de remover - updateRemoveButtons(); }); - // Função para remover tradução - $(document).on('click', '.remove-translation', function(e) { - e.preventDefault(); + // Adiciona botão de remover se não existir + if ($template.find('.remove-translation').length === 0) { + $template.prepend(''); + } - var $translationField = $(this).closest('.translation-fields'); - var totalFields = $('#translations-container .translation-fields').length; + $('#translations-container').append($template); - // Não permite remover se só tiver 2 campos (mínimo: pt_BR e en_US) - if (totalFields <= 2) { - alert('Você deve manter pelo menos 2 traduções (Português e Inglês).'); - return; - } + // Inicializa CKEditor + if (newAnswerId) { + setTimeout(function() { + initFaqCKEditor(newAnswerId); + }, 100); + } - // Encontra o textarea do campo answer e destrói a instância do CKEditor - var $answerField = $translationField.find('textarea[name*="[answer]"]'); - if ($answerField.length > 0 && $answerField.attr('id')) { - var editorId = $answerField.attr('id'); - if (typeof CKEDITOR !== 'undefined' && CKEDITOR.instances[editorId]) { - CKEDITOR.instances[editorId].destroy(); - } - } + updateFaqRemoveButtons(); +}); - // Se o registro já existe no banco (tem ID), marca para destruição - var $idField = $translationField.find('input[name*="[id]"]'); - if ($idField.length > 0 && $idField.val() !== '') { - $translationField.find('.destroy-flag').val('true'); - $translationField.hide(); - } else { - // Se é um campo novo (não está no banco), apenas remove do DOM - $translationField.remove(); - } +// Evento: Remover tradução (delegação de eventos) +$(document).off('click.faqForm', '.remove-translation').on('click.faqForm', '.remove-translation', function(e) { + e.preventDefault(); + e.stopImmediatePropagation(); - // Atualiza os botões de remover - updateRemoveButtons(); - }); + var $translationField = $(this).closest('.translation-fields'); + var visibleFields = $('#translations-container .translation-fields:visible').length; + + if (visibleFields <= 2) { + alert('<%= I18n.t("faqs.form.min_translations_alert") %>'); + return; + } - // Função para mostrar/ocultar botões de remover - function updateRemoveButtons() { - var $visibleFields = $('#translations-container .translation-fields:visible'); - var visibleCount = $visibleFields.length; - - if (visibleCount <= 2) { - // Oculta todos os botões de remover se só tiver 2 ou menos campos visíveis - $visibleFields.find('.remove-translation').hide(); - } else { - // Mostra os botões de remover se tiver mais de 2 campos - $visibleFields.find('.remove-translation').show(); + // Destrói CKEditor + var $answerField = $translationField.find('textarea[name*="[answer]"]'); + if ($answerField.length && $answerField.attr('id')) { + var editorId = $answerField.attr('id'); + if (typeof CKEDITOR !== 'undefined' && CKEDITOR.instances[editorId]) { + CKEDITOR.instances[editorId].destroy(); } } - // Inicializa: oculta botões de remover se tiver apenas 2 campos - updateRemoveButtons(); + // Se já existe no banco, marca para destruição; senão, remove do DOM + var $idField = $translationField.find('input[name*="[id]"]'); + if ($idField.length && $idField.val() !== '') { + $translationField.find('.destroy-flag').val('true'); + $translationField.hide(); + } else { + $translationField.remove(); + } + + updateFaqRemoveButtons(); }); // FAQ Fancybox - Configurar links para abrir formulários em popup @@ -229,7 +202,7 @@ jQuery(function ($) { handleError: function(error) { console.error("Falha ao atualizar a ordem:", error); - alert('Erro ao atualizar a ordem da FAQ'); + alert('<%= I18n.t("faqs.error.order") %>'); } }); }); diff --git a/app/controllers/faqs_controller.rb b/app/controllers/faqs_controller.rb index 138cb594a..cd4cc8f2c 100644 --- a/app/controllers/faqs_controller.rb +++ b/app/controllers/faqs_controller.rb @@ -22,7 +22,7 @@ def show # GET /admin/faqs/new - Formulário para novo FAQ def new - @faq = Faq.new + @faq = Faq.new(active: true) @faq.faq_translations.build(locale: 'pt_BR') @faq.faq_translations.build(locale: 'en_US') end @@ -55,19 +55,31 @@ def update end end - # DELETE /admin/faqs/1 - Deleta FAQ + # DELETE /admin/faqs/1 - Deleta FAQ (suporta múltiplos IDs via AJAX) def destroy @faq.destroy - redirect_to admin_faq_index_path, notice: 'FAQ removido com sucesso.' + + respond_to do |format| + format.html { redirect_to admin_faq_index_path, notice: t('faqs.success.deleted') } + format.json { render json: { success: true, notice: t('faqs.success.deleted') } } + end end # PATCH /admin/faqs/1/toggle_active - Alterna status ativo/inativo via AJAX def toggle_active - @faq.update(active: !@faq.active) + @faq.active = !@faq.active + @toggle_success = @faq.save + @toggle_errors = @faq.errors.full_messages.join(', ') unless @toggle_success respond_to do |format| format.js - format.json { render json: { success: true, active: @faq.active } } + format.json do + if @toggle_success + render json: { success: true, active: @faq.active } + else + render json: { success: false, errors: @faq.errors.full_messages }, status: :unprocessable_entity + end + end end end @@ -76,11 +88,18 @@ def order faq1 = @faq faq2 = Faq.find(params[:change_id]) - Faq.transaction do - faq1.order, faq2.order = faq2.order, faq1.order - faq1.save! - faq2.save! - end + order1 = faq1.order + order2 = faq2.order + + # Usa SQL direto para fazer o swap em uma única operação atômica + # Evita problemas de validação e timing + Faq.connection.execute( + "UPDATE faqs SET \"order\" = CASE + WHEN id = #{faq1.id} THEN #{order2} + WHEN id = #{faq2.id} THEN #{order1} + END + WHERE id IN (#{faq1.id}, #{faq2.id})" + ) render json: { success: true } rescue => e diff --git a/app/views/faqs/_faq_row.html.haml b/app/views/faqs/_faq_row.html.haml index 00e8198cd..1746fdfa6 100644 --- a/app/views/faqs/_faq_row.html.haml +++ b/app/views/faqs/_faq_row.html.haml @@ -1,19 +1,20 @@ %td{style: 'text-align: center;'} - = check_box_tag "ckb_faq[]", faq.id, false, class: 'ckb_faq', :'data-faq-id' => faq.id + = check_box_tag "ckb_faq[]", faq.id, false, class: 'ckb_faq', :'data-faq-id' => faq.id, :'aria-label' => t('.select_faq_aria', question: faq.question || 'FAQ') %td.center .arrow_up= button_tag content_tag(:i, nil, class: 'icon-arrow-up-triangle'), class: 'up btn_arrow', :"aria-label" => t('faqs.index.up_aria', question: faq.question || 'FAQ'), :"data-tooltip" => t('faqs.index.up', default: 'Mover para cima') .arrow_down= button_tag content_tag(:i, nil, class: 'icon-arrow-down-triangle'), class: 'down btn_arrow', :"aria-label" => t('faqs.index.down_aria', question: faq.question || 'FAQ'), :"data-tooltip" => t('faqs.index.down', default: 'Mover para baixo') %td - = link_to '#void', onclick: 'toggle_answer(this, event)', onkeydown: 'click_on_keypress(event, this);', class: 'faq-question', style: 'display: flex; align-items: center; text-decoration:none' do - = content_tag(:i, nil, class: 'icon-arrow-down-triangle', style: 'margin-right: 8px; transition: transform 0.3s;', :'aria-hidden' => 'true') + = link_to '#void', onclick: 'toggle_answer(this, event)', onkeydown: 'click_on_keypress(event, this);', class: 'faq-question', role: 'button', :'aria-expanded' => 'false', :'aria-label' => t('.toggle_answer_aria', question: faq.question || 'FAQ'), :'data-tooltip' => t('.expand_answer') do + = content_tag(:i, nil, class: 'icon-arrow-down-triangle', :'aria-hidden' => 'true') %span= faq.question || t('.no_translation') - .faq-answer.invisible{style: 'margin-top: 10px; padding: 10px; background: #f5f5f5; border-left: 3px solid #ccc;'} + .faq-answer.invisible{:'aria-hidden' => 'true'} %strong= t('.answer', default: 'Resposta:') %p= raw(faq.answer || t('.no_translation')) %td.center{style: 'width: 70px;'} - img_status = faq.active ? 'released.png' : 'rejected.png' - tooltip_text = faq.active ? t('.deactivate', default: 'Desativar') : t('.activate', default: 'Ativar') - = link_to image_tag(img_status), toggle_active_admin_faq_path(faq), method: :patch, remote: true, class: 'change_faq_status', :'data-faq-id' => faq.id, :'data-tooltip' => tooltip_text + - aria_label_text = faq.active ? t('.deactivate_aria', question: faq.question || 'FAQ') : t('.activate_aria', question: faq.question || 'FAQ') + = link_to image_tag(img_status, alt: ''), toggle_active_admin_faq_path(faq), method: :patch, remote: true, class: 'change_faq_status', :'data-faq-id' => faq.id, :'data-tooltip' => tooltip_text, :'aria-label' => aria_label_text %td{style: 'text-align: center;'} - = link_to admin_faq_path(faq), class: 'btn link_show_faq', :'data-tooltip' => t('.show', default: 'Visualizar') do + = link_to admin_faq_path(faq), class: 'btn link_show_faq', :'data-tooltip' => t('.show', default: 'Visualizar'), :'aria-label' => t('.show_aria', question: faq.question || 'FAQ') do = content_tag(:i, nil, class: 'icon-eye', :'aria-hidden' => 'true') diff --git a/app/views/faqs/_form.html.haml b/app/views/faqs/_form.html.haml index 327ebce63..b237bb6f7 100644 --- a/app/views/faqs/_form.html.haml +++ b/app/views/faqs/_form.html.haml @@ -17,12 +17,13 @@ -# NESTED FORM: Campos das traduções usando simple_fields_for #translations-container = f.simple_fields_for :faq_translations do |translation_form| - .translation-fields{style: "border: solid 1px #ccc; padding: 20px; margin-bottom: 20px; background: #f9f9f9; position: relative;"} + .translation-fields -# Botão para remover tradução (só aparece se tiver mais de 2) - if f.object.faq_translations.size > 2 - %button.remove-translation{type: 'button', style: 'position: absolute; top: 10px; right: 10px; background: #d9534f; color: white; border: none; padding: 5px 10px; cursor: pointer; border-radius: 3px;'} - × Remover + %button.remove-translation{type: 'button', :'aria-label' => t('faqs.form.remove_aria', default: 'Remover esta tradução')} + %i.icon-remove{"aria-hidden" => "true"} + = t('faqs.form.remove') -# SELECT de idioma (pode escolher qualquer um da lista) = translation_form.input :locale, @@ -37,12 +38,13 @@ = translation_form.input :_destroy, as: :hidden, input_html: { class: 'destroy-flag' } -# Botão para adicionar nova tradução - %button#add-translation.btn.btn_default{type: 'button', style: 'margin-bottom: 20px;'} - + Adicionar outra língua + %button#add-translation.btn.btn_default{type: 'button', :'aria-label' => t('faqs.form.add_language_aria', default: 'Adicionar nova tradução de idioma')} + %i.icon-plus{"aria-hidden" => "true"} + = t('faqs.form.add_language') .form-actions.right_buttons - = button_tag t('faqs.form.cancel'), type: 'button', onclick: "jQuery.fancybox.close()", class: 'btn btn_default btn_lightbox', alt: t('faqs.form.cancel') - = button_tag t('faqs.form.save'), type: 'button', onclick: "faq_save()", class: 'btn btn_main btn_lightbox', alt: t('faqs.form.save') + = button_tag t('faqs.form.cancel'), type: 'button', onclick: "jQuery.fancybox.close()", class: 'btn btn_default btn_lightbox', :'aria-label' => t('faqs.form.cancel_aria', default: 'Cancelar e fechar formulário') + = button_tag t('faqs.form.save'), type: 'button', onclick: "faq_save()", class: 'btn btn_main btn_lightbox', :'aria-label' => t('faqs.form.save_aria', default: 'Salvar FAQ') = javascript_include_tag 'ckeditor/init' diff --git a/app/views/faqs/index.html.haml b/app/views/faqs/index.html.haml index a80464d00..c20e24e63 100644 --- a/app/views/faqs/index.html.haml +++ b/app/views/faqs/index.html.haml @@ -4,18 +4,18 @@ .block_content_toolbar .block_toolbar_left.btn-group - = link_to content_tag(:i, nil, class: 'icon-plus'), new_admin_faq_path, class: 'btn btn_main link_new_faq', :'data-tooltip' => t('.new', default: 'Novo FAQ') + = link_to content_tag(:i, nil, class: 'icon-plus', :'aria-hidden' => 'true'), new_admin_faq_path, class: 'btn btn_main link_new_faq', :'data-tooltip' => t('.new', default: 'Novo FAQ'), :'aria-label' => t('.new_aria', default: 'Criar nova FAQ') .block_toolbar_right .btn-group - = link_to content_tag(:i, nil, class: 'icon-edit'), '#void', class: 'btn btn_edit', :'data-link-edit' => edit_admin_faq_path(id: ':id'), disabled: true, :'data-tooltip' => t('.edit', default: 'Editar') - = link_to content_tag(:i, nil, class: 'icon-trash'), '#void', class: 'btn btn_del delete_faq', :'data-link-delete' => admin_faq_path(id: ':id'), disabled: true, :'data-tooltip' => t('.delete', default: 'Deletar') + = link_to content_tag(:i, nil, class: 'icon-edit', :'aria-hidden' => 'true'), '#void', class: 'btn btn_edit', :'data-link-edit' => edit_admin_faq_path(id: ':id'), disabled: true, :'data-tooltip' => t('.edit', default: 'Editar'), :'aria-label' => t('.edit_aria', default: 'Editar FAQ selecionada') + = link_to content_tag(:i, nil, class: 'icon-trash', :'aria-hidden' => 'true'), '#void', class: 'btn btn_del delete_faq', :'data-link-delete' => admin_faq_path(id: ':id'), disabled: true, :'data-tooltip' => t('.delete', default: 'Deletar'), :'aria-label' => t('.delete_aria', default: 'Deletar FAQ(s) selecionada(s)') .block_content.responsive-table %table.tb_list.tb_faqs{data: { reorder_url: change_order_admin_faq_path(':id', ':change_id') }} %thead{style: (@faqs.blank? ? 'display: none' : '')} %tr.lines - %th.no_sort{style: 'text-align: center; width: 25px;'}= check_box_tag :all_faqs, false, false, :'data-children-names' => 'ckb_faq', class: 'all_faqs' + %th.no_sort{style: 'text-align: center; width: 25px;'}= check_box_tag :all_faqs, false, false, :'data-children-names' => 'ckb_faq', class: 'all_faqs', :'aria-label' => t('.select_all_aria', default: 'Selecionar todas as FAQs') %th.no_sort{style: 'width: 60px;'}= t('.order', default: 'Ordem') %th{align: 'left'}= t('.question', default: 'Pergunta') %th.no_sort{style: 'width: 80px; text-align: center;'}= t('.active', default: 'Ativo') @@ -33,16 +33,22 @@ // Toggle accordion para mostrar/esconder resposta window.toggle_answer = function(element, event) { event.preventDefault(); - var $answer = $(element).next('.faq-answer'); - var $icon = $(element).find('i'); + var $element = $(element); + var $answer = $element.next('.faq-answer'); + var $icon = $element.find('i'); + var isExpanded = $answer.is(':visible'); $answer.toggle(); - // Alternar chevron (baixo -> cima) - if ($answer.is(':visible')) { + // Alternar chevron e aria-expanded + if (!isExpanded) { $icon.removeClass('icon-arrow-down-triangle').addClass('icon-arrow-up-triangle'); + $element.attr('aria-expanded', 'true'); + $answer.attr('aria-hidden', 'false'); } else { $icon.removeClass('icon-arrow-up-triangle').addClass('icon-arrow-down-triangle'); + $element.attr('aria-expanded', 'false'); + $answer.attr('aria-hidden', 'true'); } }; @@ -93,39 +99,31 @@ // Confirmar deleção $('.delete_faq').on('click', function(e) { + e.preventDefault(); + if ($(this).attr('disabled')) { - e.preventDefault(); return false; } var checkedBoxes = $('.ckb_faq:checked'); if (checkedBoxes.length === 0) { - e.preventDefault(); return false; } - if (!confirm('Tem certeza que deseja deletar esta(s) FAQ(s)?')) { - e.preventDefault(); + if (!confirm("#{t('faqs.index.confirm_delete', default: 'Tem certeza que deseja deletar esta(s) FAQ(s)?')}")) { return false; } - if (checkedBoxes.length === 1) { - // Deletar um único item via link - return true; - } else { - // Deletar múltiplos itens - e.preventDefault(); - var faqIds = []; - checkedBoxes.each(function() { - faqIds.push($(this).val()); - }); + // Deletar via AJAX + var faqIds = []; + checkedBoxes.each(function() { + faqIds.push($(this).val()); + }); - // Deletar múltiplos FAQs - deleteFaqs(faqIds); - } + deleteFaqs(faqIds, checkedBoxes); }); - function deleteFaqs(faqIds) { + function deleteFaqs(faqIds, checkedBoxes) { var deleteUrl = $('.delete_faq').data('link-delete'); var promises = []; @@ -135,6 +133,7 @@ $.ajax({ url: url, type: 'DELETE', + dataType: 'json', headers: { 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') } @@ -143,10 +142,14 @@ }); $.when.apply($, promises).done(function() { - flash_message('FAQ(s) deletada(s) com sucesso!', 'notice'); - location.reload(); + flash_message("#{t('faqs.success.deleted', default: 'FAQ(s) deletada(s) com sucesso!')}", 'notice'); + checkedBoxes.closest('tr').fadeOut(function() { + $(this).remove(); + update_tables_with_no_data('.tb_faqs', 'tbody>tr'); + }); + toggleActionButtons(); }).fail(function() { - flash_message('Erro ao deletar FAQ(s)', 'alert'); + flash_message("#{t('faqs.error.delete', default: 'Erro ao deletar FAQ(s)')}", 'alert'); }); } }); diff --git a/app/views/faqs/toggle_active.js.haml b/app/views/faqs/toggle_active.js.haml index 3a2f92795..b44752792 100644 --- a/app/views/faqs/toggle_active.js.haml +++ b/app/views/faqs/toggle_active.js.haml @@ -1,20 +1,27 @@ -- # Salvar estado do checkbox antes de atualizar -var $row = $("tr.faq-row[data-faq-id='#{@faq.id}']"); -var wasChecked = $row.find('.ckb_faq').is(':checked'); +- if @toggle_success + - # Salvar estado do checkbox antes de atualizar + var $row = $("tr.faq-row[data-faq-id='#{@faq.id}']"); + var wasChecked = $row.find('.ckb_faq').is(':checked'); -- # Esconder tooltip antes de atualizar -$row.find('.change_faq_status').qtip('hide'); + - # Esconder tooltip antes de atualizar + $row.find('.change_faq_status').qtip('hide'); -- # Atualizar conteúdo da linha -$row.html('#{j render partial: 'faq_row', locals: { faq: @faq }}'); + - # Atualizar conteúdo da linha + $row.html('#{j render partial: 'faq_row', locals: { faq: @faq }}'); -- # Restaurar estado do checkbox -if (wasChecked) { $row.find('.ckb_faq').prop('checked', true); } + - # Restaurar estado do checkbox + if (wasChecked) { $row.find('.ckb_faq').prop('checked', true); } -- # Reativar event listeners para o novo checkbox -$row.find('.ckb_faq').on('change', function() { toggleActionButtons(); }); + - # Reativar event listeners para o novo checkbox + $row.find('.ckb_faq').on('change', function() { toggleActionButtons(); }); -- # Atualizar botões de ação -if (typeof toggleActionButtons === 'function') { toggleActionButtons(); } + - # Re-inicializar fancybox no link de visualizar + $row.find('.link_show_faq').call_fancybox({ width: '70%' }); -flash_message('Status atualizado com sucesso!', 'notice'); + - # Atualizar botões de ação + if (typeof toggleActionButtons === 'function') { toggleActionButtons(); } + + flash_message('#{t('faqs.success.status_updated', default: 'Status atualizado com sucesso!')}', 'notice'); +- else + - # Reverter o estado visual e mostrar erro + flash_message('#{t('faqs.error.toggle_status', default: 'Erro ao atualizar status')}: #{@toggle_errors}', 'alert'); diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index b8ef91e55..8ddf4faeb 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -1,4 +1,4 @@ # TODO: o sistema usa include_tags de js e css em varios lugares. # E isso precisa ser corrigido. Enquanto isso segue um "workaround": -Rails.application.config.assets.precompile += %w[academic_allocation_user.js administrations.js allocations.js application.js assignment_webconferences.js assignments.js audios.js autocomplete.js bibliographies.js bibliography_authors.js breadcrumb.js calendar.js chat_rooms.js ckeditor/init.js comments.js contextual_help/discussion.js contextual_help/discussion_posts.js contextual_help/home.js contextual_help/lessons.js contextual_help/subject.js contextual_help/support_material.js courses.js digital_classes.js discussions.js edition.js edition_discussions.js enrollments.js exams.js fullcalendar.js group_assignments.js groups.js groups_tags.js ie-warning.js ip.js jquery-3.3.1.min.js jquery-ui-1.8.6.js jquery-ui-timepicker-addon.js jquery-ui.js jquery.cookie.js jquery.dropdown.js jquery.fancybox3.min.js jquery.js jquery.mask.js jquery.qtip.min.js jquery.tokeninput.js jquery.ui.datepicker-en-US.js jquery.ui.datepicker-pt-BR.js jspanel.js jspdf.min.js lesson_files.js lesson_notes.js lessons.js login.js lte-ie7 lte-ie7.js messages.js multiple_file_upload.js notifications.js online_correction_files.js pagination.js pdfjs/pdf.js portlet_slider.js profiles.js questions.js registrations.js respond.min.js schedule_event_files.js schedule_events.js scores.js shortcut.js social_networks.js tableHeadFixer.js tablesorter.js tooltip.js user_blacklist.js webconferences.js zoom/jquery.zoom.js] +Rails.application.config.assets.precompile += %w[academic_allocation_user.js administrations.js allocations.js application.js assignment_webconferences.js assignments.js audios.js autocomplete.js bibliographies.js bibliography_authors.js breadcrumb.js calendar.js chat_rooms.js ckeditor/init.js comments.js contextual_help/discussion.js contextual_help/discussion_posts.js contextual_help/home.js contextual_help/lessons.js contextual_help/subject.js contextual_help/support_material.js courses.js digital_classes.js discussions.js edition.js edition_discussions.js enrollments.js exams.js faqs.js fullcalendar.js group_assignments.js groups.js groups_tags.js ie-warning.js ip.js jquery-3.3.1.min.js jquery-ui-1.8.6.js jquery-ui-timepicker-addon.js jquery-ui.js jquery.cookie.js jquery.dropdown.js jquery.fancybox3.min.js jquery.js jquery.mask.js jquery.qtip.min.js jquery.tokeninput.js jquery.ui.datepicker-en-US.js jquery.ui.datepicker-pt-BR.js jspanel.js jspdf.min.js lesson_files.js lesson_notes.js lessons.js login.js lte-ie7 lte-ie7.js messages.js multiple_file_upload.js notifications.js online_correction_files.js pagination.js pdfjs/pdf.js portlet_slider.js profiles.js questions.js registrations.js respond.min.js schedule_event_files.js schedule_events.js scores.js shortcut.js social_networks.js tableHeadFixer.js tablesorter.js tooltip.js user_blacklist.js webconferences.js zoom/jquery.zoom.js] Rails.application.config.assets.precompile += %w[themes/theme_blue.css themes/theme_red.css themes/theme_high_contrast.css autocomplete.css fancyBox.css fonts/fonts-ie.css fonts/fonts.css fonts/icons.css fullcalendar.css.css jquery-ui-timepicker-addon.css jquery.dropdown.css.css jquery.fancybox3.min.css jquery.qtip.min.css login.css misc/div_layout.css misc/facebook.css misc/ie7.css online_correction_files.css pdf.css ui.dynatree.custom.css viewer.css] diff --git a/config/locales/en_US.yml b/config/locales/en_US.yml index 6ff0f7289..3a0ec2a88 100644 --- a/config/locales/en_US.yml +++ b/config/locales/en_US.yml @@ -5295,8 +5295,11 @@ en_US: index: title: Frequently Asked Questions (FAQ) new: New FAQ + new_aria: Create new FAQ edit: Edit + edit_aria: Edit selected FAQ delete: Delete + delete_aria: Delete selected FAQ(s) show: View preview: Preview order: Order @@ -5308,6 +5311,7 @@ en_US: deactivate: Deactivate no_data: No FAQs registered confirm_delete: Are you sure you want to delete this FAQ? + select_all_aria: Select all FAQs up: Move up down: Move down up_aria: Move FAQ "%{question}" up @@ -5319,12 +5323,26 @@ en_US: answer: Answer no_translation: (no translation) deactivate: Deactivate + deactivate_aria: Deactivate FAQ "%{question}" activate: Activate + activate_aria: Activate FAQ "%{question}" show: View + show_aria: View FAQ "%{question}" + select_faq_aria: Select FAQ "%{question}" + expand_answer: Expand answer + collapse_answer: Collapse answer + toggle_answer_aria: Click to expand or collapse the answer to the question "%{question}" form: translations: FAQ Translations save: Save + save_aria: Save FAQ cancel: Cancel + cancel_aria: Cancel and close form + add_language: Add another language + add_language_aria: Add new language translation + remove: Remove + remove_aria: Remove this translation + min_translations_alert: You must keep at least 2 translations (Portuguese and English). success: created: FAQ created successfully updated: FAQ updated successfully @@ -5362,3 +5380,9 @@ en_US: attributes: order: taken: is already in use by another active FAQ + faq_translation: + attributes: + question: + blank: Not blank + answer: + blank: Not blank \ No newline at end of file diff --git a/config/locales/pt_BR.yml b/config/locales/pt_BR.yml index 7e03c4ec8..6e34c15a9 100644 --- a/config/locales/pt_BR.yml +++ b/config/locales/pt_BR.yml @@ -5622,11 +5622,15 @@ pt_BR: show: order: Ordem title: Visualizar FAQ + close: Fechar index: title: Perguntas Frequentes (FAQ) new: Novo FAQ + new_aria: Criar nova FAQ edit: Editar + edit_aria: Editar FAQ selecionada delete: Deletar + delete_aria: Deletar FAQ(s) selecionada(s) show: Visualizar preview: Prévia order: Ordem @@ -5638,6 +5642,7 @@ pt_BR: deactivate: Desativar no_data: Nenhuma FAQ cadastrada confirm_delete: Tem certeza que deseja deletar esta FAQ? + select_all_aria: Selecionar todas as FAQs up: Mover para cima down: Mover para baixo up_aria: Mover FAQ "%{question}" para cima @@ -5649,17 +5654,35 @@ pt_BR: answer: Resposta no_translation: (sem tradução) deactivate: Desativar + deactivate_aria: Desativar FAQ "%{question}" activate: Ativar + activate_aria: Ativar FAQ "%{question}" show: Visualizar + show_aria: Visualizar FAQ "%{question}" + select_faq_aria: Selecionar FAQ "%{question}" + expand_answer: Expandir resposta + collapse_answer: Ocultar resposta + toggle_answer_aria: Clique para expandir ou ocultar a resposta da pergunta "%{question}" form: translations: Traduções do FAQ save: Salvar + save_aria: Salvar FAQ cancel: Cancelar + cancel_aria: Cancelar e fechar formulário + add_language: Adicionar outra língua + add_language_aria: Adicionar nova tradução de idioma + remove: Remover + remove_aria: Remover esta tradução + min_translations_alert: Você deve manter pelo menos 2 traduções (Português e Inglês). success: created: FAQ criado com sucesso updated: FAQ atualizado com sucesso + deleted: FAQ(s) deletado(s) com sucesso + status_updated: Status atualizado com sucesso! error: order: Erro ao atualizar a ordem da FAQ + toggle_status: Erro ao atualizar status + delete: Erro ao deletar FAQ(s) simple_form: labels: @@ -5692,3 +5715,11 @@ pt_BR: attributes: order: taken: já está em uso por outro FAQ ativo + active: + inclusion: deve ser selecionado + faq_translation: + attributes: + question: + blank: não pode ficar em branco + answer: + blank: não pode ficar em branco From 0167035b520771341df3d3524222b433b0b2496d Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Sun, 8 Feb 2026 15:47:16 -0300 Subject: [PATCH 31/31] =?UTF-8?q?fix(faq):=20corre=C3=A7=C3=B5es=20de=20pa?= =?UTF-8?q?dr=C3=A3o,=20acessibilidade=20e=20alto=20contraste?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Padroniza código seguindo convenções do Solar (controllers, models, views, JS, CSS) - Adiciona suporte a alto contraste usando variáveis de tema ($background_light, $color_main) - Remove estilos inline do show e aplica via SCSS - Configura toolbar customizada do CKEditor adequada para FAQ - Corrige bug de window.location.reload sem execução no index - Remove link Voltar inconsistente do edit - Adiciona _faq.scss com estilos do formulário e respostas --- app/assets/javascripts/faqs.js.erb | 43 +++---------- app/assets/stylesheets/partials/_all.scss | 1 + app/assets/stylesheets/partials/_faq.scss | 47 ++++++++++++++ app/controllers/faqs_controller.rb | 37 +++-------- app/models/faq.rb | 75 +---------------------- app/models/faq_translation.rb | 10 +-- app/views/faqs/_form.html.haml | 21 ++++--- app/views/faqs/edit.html.haml | 5 -- app/views/faqs/index.html.haml | 14 +---- app/views/faqs/new.html.haml | 2 - app/views/faqs/show.html.haml | 10 +-- config/locales/en_US.yml | 45 +++++++------- config/locales/pt_BR.yml | 47 +++++++------- 13 files changed, 132 insertions(+), 225 deletions(-) create mode 100644 app/assets/stylesheets/partials/_faq.scss diff --git a/app/assets/javascripts/faqs.js.erb b/app/assets/javascripts/faqs.js.erb index 54956806b..ee355e87b 100644 --- a/app/assets/javascripts/faqs.js.erb +++ b/app/assets/javascripts/faqs.js.erb @@ -1,8 +1,6 @@ <%# @encoding: UTF-8 %> -// FAQ Save - Função para salvar FAQ via AJAX (usada no fancybox) function faq_save(){ - // Sincronizar todos os CKEditors antes de enviar o formulário if (typeof CKEDITOR !== 'undefined') { for (var instanceName in CKEDITOR.instances) { CKEDITOR.instances[instanceName].updateElement(); @@ -14,10 +12,6 @@ function faq_save(){ }); } -// FAQ Form - Adicionar/Remover traduções dinamicamente -// Usa delegação de eventos para funcionar dentro do fancybox (elementos carregados via AJAX) - -// Função para inicializar CKEditor em um campo específico function initFaqCKEditor(textareaId) { if (typeof CKEDITOR !== 'undefined') { if (CKEDITOR.instances[textareaId]) { @@ -27,20 +21,17 @@ function initFaqCKEditor(textareaId) { } } -// Função para atualizar botões de remover function updateFaqRemoveButtons() { - var $visibleFields = $('#translations-container .translation-fields:visible'); - var visibleCount = $visibleFields.length; - - if (visibleCount <= 2) { - $visibleFields.find('.remove-translation').hide(); - } else { - $visibleFields.find('.remove-translation').show(); - } + $('#translations-container .translation-fields:visible').each(function() { + var locale = $(this).find('select[name*="[locale]"]').val(); + if (locale === 'pt_BR' || locale === 'en_US') { + $(this).find('.remove-translation').hide(); + } else { + $(this).find('.remove-translation').show(); + } + }); } -// Evento: Adicionar nova tradução (delegação de eventos para funcionar no fancybox) -// Usa namespace .faqForm e .off() para evitar handlers duplicados $(document).off('click.faqForm', '#add-translation').on('click.faqForm', '#add-translation', function(e) { e.preventDefault(); e.stopImmediatePropagation(); @@ -48,17 +39,14 @@ $(document).off('click.faqForm', '#add-translation').on('click.faqForm', '#add-t var translationIndex = $('#translations-container .translation-fields').length; var $template = $('#translations-container .translation-fields').first().clone(); - // Remove CKEditor clonado $template.find('.cke').remove(); - // Limpa valores $template.find('input[type="text"], textarea, select').val(''); $template.find('input[type="hidden"]').not('.destroy-flag').val(''); $template.find('.destroy-flag').val('false'); var newAnswerId; - // Atualiza IDs e nomes $template.find('input, select, textarea').each(function() { var $field = $(this); @@ -76,21 +64,18 @@ $(document).off('click.faqForm', '#add-translation').on('click.faqForm', '#add-t } }); - // Atualiza labels $template.find('label').each(function() { if ($(this).attr('for')) { $(this).attr('for', $(this).attr('for').replace(/_\d+_/, '_' + translationIndex + '_')); } }); - // Adiciona botão de remover se não existir if ($template.find('.remove-translation').length === 0) { - $template.prepend(''); + $template.prepend(''); } $('#translations-container').append($template); - // Inicializa CKEditor if (newAnswerId) { setTimeout(function() { initFaqCKEditor(newAnswerId); @@ -100,7 +85,6 @@ $(document).off('click.faqForm', '#add-translation').on('click.faqForm', '#add-t updateFaqRemoveButtons(); }); -// Evento: Remover tradução (delegação de eventos) $(document).off('click.faqForm', '.remove-translation').on('click.faqForm', '.remove-translation', function(e) { e.preventDefault(); e.stopImmediatePropagation(); @@ -113,7 +97,6 @@ $(document).off('click.faqForm', '.remove-translation').on('click.faqForm', '.re return; } - // Destrói CKEditor var $answerField = $translationField.find('textarea[name*="[answer]"]'); if ($answerField.length && $answerField.attr('id')) { var editorId = $answerField.attr('id'); @@ -134,21 +117,17 @@ $(document).off('click.faqForm', '.remove-translation').on('click.faqForm', '.re updateFaqRemoveButtons(); }); -// FAQ Fancybox - Configurar links para abrir formulários em popup jQuery(function ($) { - // Configurar link "Novo FAQ" para abrir em fancybox $(".link_new_faq").call_fancybox({ width: '70%' }); - // Configurar link "Visualizar FAQ" para abrir em fancybox $(".link_show_faq").call_fancybox({ width: '70%' }); - // Configurar link "Editar FAQ" para abrir em fancybox $(".btn_edit[data-link-edit]").on('click', function(e) { - e.preventDefault(); // Previne navegação padrão do link + e.preventDefault(); var checkedBoxes = $('.ckb_faq:checked'); if (checkedBoxes.length === 1 && !$(this).attr('disabled')) { @@ -160,9 +139,7 @@ jQuery(function ($) { }); }); -// FAQ Ordering - Ordenação com setas de acessibilidade jQuery(function ($) { - // Só executa se estiver na página de listagem de FAQs if ($(".tb_faqs").length === 0) { return; } diff --git a/app/assets/stylesheets/partials/_all.scss b/app/assets/stylesheets/partials/_all.scss index e91fad5f4..e9e418c08 100644 --- a/app/assets/stylesheets/partials/_all.scss +++ b/app/assets/stylesheets/partials/_all.scss @@ -56,6 +56,7 @@ @import 'tutorials'; @import 'panels'; +@import 'faq'; @import 'load'; diff --git a/app/assets/stylesheets/partials/_faq.scss b/app/assets/stylesheets/partials/_faq.scss new file mode 100644 index 000000000..3c713454d --- /dev/null +++ b/app/assets/stylesheets/partials/_faq.scss @@ -0,0 +1,47 @@ +.faq-answer { + padding: 15px; + margin-top: 5px; + background-color: $background_light; + border-left: 3px solid $color_main; +} + +.translation-content { + margin-bottom: 20px; + padding: 15px; + background-color: $background_light; + border-left: 3px solid $color_main; +} + +#faq_form { + .faq_order input { + width: 80px !important; + } + + .translation-fields input[type="text"] { + width: 100% !important; + } + + .faq_active { + display: flex; + align-items: center; + gap: 15px; + + .control-label { + margin-bottom: 0; + } + + span.radio { + display: inline-flex !important; + align-items: center; + margin: 0; + + label { + display: inline-flex !important; + align-items: center; + gap: 5px; + margin: 0; + white-space: nowrap; + } + } + } +} diff --git a/app/controllers/faqs_controller.rb b/app/controllers/faqs_controller.rb index cd4cc8f2c..1e96f83dc 100644 --- a/app/controllers/faqs_controller.rb +++ b/app/controllers/faqs_controller.rb @@ -1,33 +1,25 @@ class FaqsController < ApplicationController - #layout 'login', only: [:apresentation] layout false, except: [:index, :apresentation] - # Apenas administradores podem acessar before_action :require_admin - - # Carrega o FAQ antes destas ações before_action :set_faq, only: [:show, :edit, :update, :destroy, :toggle_active, :order] - skip_before_action :require_admin, only:[:apresentation] - # GET /admin/faqs - Lista todos os FAQs def index @faqs = Faq.all + render layout: false if params[:layout].present? && params[:layout] == 'false' end - # GET /admin/faqs/1 - Mostra um FAQ def show end - # GET /admin/faqs/new - Formulário para novo FAQ def new @faq = Faq.new(active: true) @faq.faq_translations.build(locale: 'pt_BR') @faq.faq_translations.build(locale: 'en_US') end - # POST /admin/faqs - Cria novo FAQ def create @faq = Faq.new(faq_params) @@ -39,13 +31,11 @@ def create end end - # GET /admin/faqs/1/edit - Formulário para editar FAQ def edit @faq.translation_pt @faq.translation_en end - # PATCH/PUT /admin/faqs/1 - Atualiza FAQ existente def update begin @faq.update!(faq_params) @@ -55,7 +45,6 @@ def update end end - # DELETE /admin/faqs/1 - Deleta FAQ (suporta múltiplos IDs via AJAX) def destroy @faq.destroy @@ -65,7 +54,6 @@ def destroy end end - # PATCH /admin/faqs/1/toggle_active - Alterna status ativo/inativo via AJAX def toggle_active @faq.active = !@faq.active @toggle_success = @faq.save @@ -83,7 +71,6 @@ def toggle_active end end - # PUT /admin/faqs/1/order/:change_id - Troca a ordem de dois FAQs def order faq1 = @faq faq2 = Faq.find(params[:change_id]) @@ -91,8 +78,6 @@ def order order1 = faq1.order order2 = faq2.order - # Usa SQL direto para fazer o swap em uma única operação atômica - # Evita problemas de validação e timing Faq.connection.execute( "UPDATE faqs SET \"order\" = CASE WHEN id = #{faq1.id} THEN #{order2} @@ -112,30 +97,26 @@ def apresentation end private - # Verifica se usuário é admin (redireciona se não for) def require_admin unless current_user.admin? redirect_to home_path, alert: t(:no_permission) end end - # Carrega FAQ pelo ID (chamado pelo before_action) def set_faq @faq = Faq.find(params[:id]) end - # Lista de parâmetros permitidos (strong parameters) - # Aceita apenas estes campos do formulário (segurança contra mass assignment) def faq_params params.require(:faq).permit( - :order, # Ordem de exibição - :active, # Ativo/Inativo - faq_translations_attributes: [ # Nested attributes das traduções - :id, # ID da tradução (para editar existente) - :locale, # Idioma (pt_BR ou en_US) - :question, # Pergunta - :answer, # Resposta - :_destroy # Flag para deletar tradução (se necessário) + :order, + :active, + faq_translations_attributes: [ + :id, + :locale, + :question, + :answer, + :_destroy ] ) end diff --git a/app/models/faq.rb b/app/models/faq.rb index eca0f6d04..62378d5d8 100644 --- a/app/models/faq.rb +++ b/app/models/faq.rb @@ -1,6 +1,5 @@ class Faq < ApplicationRecord has_many :faq_translations, dependent: :destroy - #Permite salvar FAQ + traduções em unm único formulário accepts_nested_attributes_for :faq_translations, allow_destroy: true validates :order, presence: true, numericality: {only_integer: true} validates :active, inclusion: {in: [true, false]} @@ -12,22 +11,18 @@ class Faq < ApplicationRecord scope :faq_translations, -> { includes(:faq_translations).where(active: true) } - # Retorna pergunta no idioma atual def question translation_for(I18n.locale)&.question end - # Retorna resposta no idioma atual def answer translation_for(I18n.locale)&.answer end - # Busca tradução para um idioma específico def translation_for(locale) faq_translations.find_by(locale: locale.to_s) end - # Helpers para formulário admin (garante que PT e EN existem) def translation_pt faq_translations.find_or_initialize_by(locale: 'pt_BR') end @@ -38,17 +33,9 @@ def translation_en private - # ========================================================================= - # VALIDAÇÃO: Ordem única para FAQs ativos - # ========================================================================= - # Garante que FAQs ativos não possam ter a mesma ordem - # FAQs inativos podem ter qualquer ordem (não aparecem na listagem pública) - # ========================================================================= def unique_order_for_active_faqs - # Só valida se o FAQ for ativo return unless active - # Busca por outro FAQ ativo com a mesma ordem (excluindo o próprio) duplicate = Faq.where(active: true, order: order) .where.not(id: id) .exists? @@ -58,79 +45,21 @@ def unique_order_for_active_faqs end end - # ========================================================================= - # REORDENAÇÃO AUTOMÁTICA - # ========================================================================= - # Este método é executado automaticamente ANTES de salvar um FAQ (before_save) - # - # OBJETIVO: Quando você muda a ordem de um FAQ, os outros FAQs são - # automaticamente reorganizados para "abrir espaço" na nova posição. - # - # EXEMPLO: - # FAQs atuais: ordem 1, 2, 3, 4, 5 - # Você edita FAQ com ordem 5 e muda para ordem 2 - # Resultado automático: - # - FAQ que era ordem 2 → vira ordem 3 - # - FAQ que era ordem 3 → vira ordem 4 - # - FAQ que era ordem 4 → vira ordem 5 - # - FAQ editado (era 5) → fica ordem 2 - # ========================================================================= def reorder_faqs_if_order_changed - # order_changed? → retorna true se o campo "order" foi modificado - # persisted? → retorna true se o registro já existe no banco (não é novo) - # Ou seja: só reordena em UPDATES, não em CREATES (novos FAQs) return unless order_changed? && persisted? - # order_was → valor ANTIGO da ordem (antes da mudança) - # order → valor NOVO da ordem (depois da mudança) old_order = order_was new_order = order - # Se os valores são iguais, não precisa reordenar nada return if old_order == new_order - # ----------------------------------------------------------------------- - # IMPORTANTE: Remove temporariamente o default_scope - # ----------------------------------------------------------------------- - # O model Faq tem um default_scope { order(:order) } que sempre ordena - # os resultados. Ao usar unscoped, removemos essa ordenação temporariamente - # para fazer as queries de UPDATE sem interferência. - # ----------------------------------------------------------------------- Faq.unscoped do - - # CASO 1: Moveu PARA CIMA (ordem diminuiu) - # Exemplo: mudou de ordem 5 para ordem 2 if new_order < old_order - # Precisamos "empurrar para baixo" (incrementar +1) todos os FAQs - # que estão entre a nova ordem e a ordem antiga - # - # No exemplo (5 → 2): - # - FAQs com ordem >= 2 E ordem < 5 precisam incrementar - # - Ou seja: ordem 2, 3, 4 viram 3, 4, 5 - # - # WHERE: - # id != ? → exclui o próprio FAQ que está sendo editado - # order >= ? → maior ou igual à nova ordem (2) - # order < ? → menor que a ordem antiga (5) Faq.where('id != ? AND "order" >= ? AND "order" < ?', id, new_order, old_order) - .update_all('"order" = "order" + 1') # Incrementa +1 (aspas duplas para escapar palavra reservada) - - # CASO 2: Moveu PARA BAIXO (ordem aumentou) - # Exemplo: mudou de ordem 2 para ordem 5 + .update_all('"order" = "order" + 1') else - # Precisamos "puxar para cima" (decrementar -1) todos os FAQs - # que estão entre a ordem antiga e a nova ordem - # - # No exemplo (2 → 5): - # - FAQs com ordem > 2 E ordem <= 5 precisam decrementar - # - Ou seja: ordem 3, 4, 5 viram 2, 3, 4 - # - # WHERE: - # id != ? → exclui o próprio FAQ que está sendo editado - # order > ? → maior que a ordem antiga (2) - # order <= ? → menor ou igual à nova ordem (5) Faq.where('id != ? AND "order" > ? AND "order" <= ?', id, old_order, new_order) - .update_all('"order" = "order" - 1') # Decrementa -1 (aspas duplas para escapar palavra reservada) + .update_all('"order" = "order" - 1') end end end diff --git a/app/models/faq_translation.rb b/app/models/faq_translation.rb index dfd0a7cb9..df1324be0 100644 --- a/app/models/faq_translation.rb +++ b/app/models/faq_translation.rb @@ -1,21 +1,15 @@ class FaqTranslation < ApplicationRecord belongs_to :faq - # Validações - # Lista de idiomas suportados (pode adicionar mais conforme necessário) AVAILABLE_LOCALES = %w[pt_BR en_US es_ES fr_FR de_DE it_IT].freeze validates :locale, presence: true, inclusion: { in: AVAILABLE_LOCALES } validates :question, presence: true validates :answer, presence: true - - # Garante que não pode ter duas traduções do mesmo idioma para um FAQ validates :locale, uniqueness: { scope: :faq_id, message: "já existe para este FAQ" } - - # Garante que não pode ter perguntas duplicadas no mesmo idioma (case-insensitive) validates :question, uniqueness: { - scope: :locale, #verifica dentro do mesmo idioma - case_sensitive: false, #ignora maiusculas e minusculas + scope: :locale, + case_sensitive: false, message: "já existe neste idioma. Não é permitido perguntas duplicadas." } end diff --git a/app/views/faqs/_form.html.haml b/app/views/faqs/_form.html.haml index b237bb6f7..e9ff399ba 100644 --- a/app/views/faqs/_form.html.haml +++ b/app/views/faqs/_form.html.haml @@ -5,7 +5,6 @@ .form-inputs.block_content.faq_box#basic_info - -# Campos do FAQ (order e active) - SimpleForm busca labels automaticamente = f.input :order, required: true, input_html: { value: @faq.order || 0 } = f.input :active, as: :radio_buttons, required: true @@ -14,30 +13,24 @@ %h2= t('faqs.form.translations') - -# NESTED FORM: Campos das traduções usando simple_fields_for #translations-container = f.simple_fields_for :faq_translations do |translation_form| .translation-fields - -# Botão para remover tradução (só aparece se tiver mais de 2) - - if f.object.faq_translations.size > 2 + - unless ['pt_BR', 'en_US'].include?(translation_form.object.locale) %button.remove-translation{type: 'button', :'aria-label' => t('faqs.form.remove_aria', default: 'Remover esta tradução')} %i.icon-remove{"aria-hidden" => "true"} = t('faqs.form.remove') - -# SELECT de idioma (pode escolher qualquer um da lista) = translation_form.input :locale, collection: [['🇧🇷 Português (Brasil)', 'pt_BR'], ['🇺🇸 English (US)', 'en_US'], ['🇪🇸 Español', 'es_ES'], ['🇫🇷 Français', 'fr_FR'], ['🇩🇪 Deutsch', 'de_DE'], ['🇮🇹 Italiano', 'it_IT']], required: true - -# Campos: Pergunta e Resposta - SimpleForm busca labels automaticamente = translation_form.input :question, required: true = translation_form.input :answer, as: :ckeditor, required: true - -# Campo hidden para marcar tradução para destruição = translation_form.input :_destroy, as: :hidden, input_html: { class: 'destroy-flag' } - -# Botão para adicionar nova tradução %button#add-translation.btn.btn_default{type: 'button', :'aria-label' => t('faqs.form.add_language_aria', default: 'Adicionar nova tradução de idioma')} %i.icon-plus{"aria-hidden" => "true"} = t('faqs.form.add_language') @@ -50,3 +43,15 @@ :javascript CKEDITOR_BASEPATH = "#{request.env['RAILS_RELATIVE_URL_ROOT']}/assets/ckeditor/"; + CKEDITOR.editorConfig = function (config) { + config.language = "#{I18n.locale}"; + config.toolbar = + [ + { name: 'clipboard', items : [ 'Cut', 'Copy', 'Paste', 'PasteText', 'PasteFromWord', '-', 'Undo', 'Redo' ] }, + { name: 'styles', items : [ 'Styles', 'Format', 'Font', 'TextColor' ] }, + { name: 'basicstyles', items : [ 'Bold', 'Italic', 'Underline', 'Strike', '-', 'RemoveFormat' ] }, + { name: 'paragraph', items : [ 'NumberedList', 'BulletedList', '-', 'Outdent', 'Indent' ] }, + { name: 'links', items : [ 'Link', 'Unlink', 'Image' ] }, + ]; + config.autoParagraph = false; + }; diff --git a/app/views/faqs/edit.html.haml b/app/views/faqs/edit.html.haml index 8f0df2ee1..bcc583279 100644 --- a/app/views/faqs/edit.html.haml +++ b/app/views/faqs/edit.html.haml @@ -1,6 +1 @@ = render 'form' - -%br -= link_to 'Ver', admin_faq_path(@faq) -\| -= link_to 'Voltar', admin_faq_index_path diff --git a/app/views/faqs/index.html.haml b/app/views/faqs/index.html.haml index c20e24e63..90c37a53c 100644 --- a/app/views/faqs/index.html.haml +++ b/app/views/faqs/index.html.haml @@ -1,4 +1,4 @@ -.block_wrapper.list_faqs +.block_wrapper.list_faqs{:"data-link-list" => admin_faq_index_path(layout: false)} .block_title %h2= t('.title', default: 'Perguntas Frequentes (FAQ)') @@ -30,7 +30,6 @@ :javascript $(function(){ - // Toggle accordion para mostrar/esconder resposta window.toggle_answer = function(element, event) { event.preventDefault(); var $element = $(element); @@ -40,7 +39,6 @@ $answer.toggle(); - // Alternar chevron e aria-expanded if (!isExpanded) { $icon.removeClass('icon-arrow-down-triangle').addClass('icon-arrow-up-triangle'); $element.attr('aria-expanded', 'true'); @@ -52,23 +50,18 @@ } }; - // Função para suportar Enter em links window.click_on_keypress = function(event, element) { if (event.which == 13) { $(element).click(); } }; - // O toggle de ativo/inativo agora usa remote: true do Rails - - // Selecionar todos os checkboxes $('.all_faqs').on('change', function() { var isChecked = $(this).is(':checked'); $('.ckb_faq').prop('checked', isChecked); toggleActionButtons(); }); - // Habilitar/desabilitar botões de ação baseado na seleção $('.ckb_faq').on('change', function() { toggleActionButtons(); }); @@ -79,11 +72,9 @@ var $deleteBtn = $('.delete_faq[data-link-delete]'); if (checkedBoxes.length === 0) { - // Nenhum selecionado - desabilitar botões $editBtn.attr('disabled', true).attr('href', '#void'); $deleteBtn.attr('disabled', true).attr('href', '#void'); } else if (checkedBoxes.length === 1) { - // Exatamente um selecionado - habilitar editar e deletar var faqId = checkedBoxes.first().val(); var editUrl = $editBtn.data('link-edit').replace(':id', faqId); var deleteUrl = $deleteBtn.data('link-delete').replace(':id', faqId); @@ -91,13 +82,11 @@ $editBtn.attr('disabled', false).attr('href', editUrl); $deleteBtn.attr('disabled', false).attr('href', deleteUrl); } else { - // Múltiplos selecionados - desabilitar editar, habilitar deletar $editBtn.attr('disabled', true).attr('href', '#void'); $deleteBtn.attr('disabled', false); } }; - // Confirmar deleção $('.delete_faq').on('click', function(e) { e.preventDefault(); @@ -114,7 +103,6 @@ return false; } - // Deletar via AJAX var faqIds = []; checkedBoxes.each(function() { faqIds.push($(this).val()); diff --git a/app/views/faqs/new.html.haml b/app/views/faqs/new.html.haml index cbd73b68e..3c55de36f 100644 --- a/app/views/faqs/new.html.haml +++ b/app/views/faqs/new.html.haml @@ -1,4 +1,2 @@ = render 'form' -%br -= link_to 'Voltar', admin_faq_index_path diff --git a/app/views/faqs/show.html.haml b/app/views/faqs/show.html.haml index 4086a7ade..4c9be3443 100644 --- a/app/views/faqs/show.html.haml +++ b/app/views/faqs/show.html.haml @@ -11,8 +11,8 @@ %hr - %h2{style: 'margin-top: 20px;'} 🇧🇷 Português (Brasil) - .translation-content{style: 'margin-bottom: 20px; padding: 15px; background: #f9f9f9; border-left: 3px solid #4CAF50;'} + %h2{style: 'margin-top: 20px;'}= "🇧🇷 #{t('faqs.show.portuguese', default: 'Português (Brasil)')}" + .translation-content %p %strong #{t('activerecord.attributes.faq_translation.question')}: = @faq.translation_pt&.question || t('faqs.index.no_translation', default: '(sem tradução)') @@ -22,8 +22,8 @@ %hr - %h2{style: 'margin-top: 20px;'} 🇺🇸 English (US) - .translation-content{style: 'margin-bottom: 20px; padding: 15px; background: #f9f9f9; border-left: 3px solid #2196F3;'} + %h2{style: 'margin-top: 20px;'}= "🇺🇸 #{t('faqs.show.english', default: 'English (US)')}" + .translation-content %p %strong #{t('activerecord.attributes.faq_translation.question')}: = @faq.translation_en&.question || t('faqs.index.no_translation', default: '(no translation)') @@ -32,4 +32,4 @@ .answer-content= raw(@faq.translation_en&.answer || t('faqs.index.no_translation', default: '(no translation)')) .form-actions.right_buttons - = button_tag t('faqs.form.cancel', default: 'Fechar'), type: 'button', onclick: "jQuery.fancybox.close()", class: 'btn btn_default btn_lightbox' + = button_tag t('faqs.show.close', default: 'Fechar'), type: 'button', onclick: "jQuery.fancybox.close()", class: 'btn btn_default btn_lightbox' diff --git a/config/locales/en_US.yml b/config/locales/en_US.yml index e67db31b5..ddf57d20e 100644 --- a/config/locales/en_US.yml +++ b/config/locales/en_US.yml @@ -214,6 +214,8 @@ en_US: email: Email supportmaterialfile: Support Material schedule_event_file: Arquivo + faq: FAQ + faq_translation: FAQ Translation attributes: user: password: "Password" @@ -516,6 +518,13 @@ en_US: attachment_content_type: "" attachment_file_size: "" attachment_file_name: "" + faq: + order: Order + active: Active + faq_translation: + locale: Language + question: Question + answer: Answer errors: uniqueness: "Record already exists in the system" template: @@ -590,6 +599,18 @@ en_US: attributes: attachment_content_type: invalid_type: invalid + faq: + attributes: + order: + taken: is already in use by another active FAQ + active: + inclusion: must be selected + faq_translation: + attributes: + question: + blank: can't be blank + answer: + blank: can't be blank all: "All" of: "of" @@ -5379,27 +5400,3 @@ en_US: question: Type the question... answer: Type the answer... - activerecord: - models: - faq: FAQ - faq_translation: FAQ Translation - attributes: - faq: - order: Order - active: Active - faq_translation: - locale: Language - question: Question - answer: Answer - errors: - models: - faq: - attributes: - order: - taken: is already in use by another active FAQ - faq_translation: - attributes: - question: - blank: Not blank - answer: - blank: Not blank \ No newline at end of file diff --git a/config/locales/pt_BR.yml b/config/locales/pt_BR.yml index 77a070469..de92fea44 100644 --- a/config/locales/pt_BR.yml +++ b/config/locales/pt_BR.yml @@ -249,6 +249,8 @@ pt_BR: email: Email supportmaterialfile: Material de Apoio schedule_event_file: Arquivo + faq: FAQ + faq_translation: Tradução do FAQ attributes: user: alternate_email: "E-mail alternativo" @@ -581,6 +583,13 @@ pt_BR: attachment_content_type: "" attachment_file_size: "" attachment_file_name: "" + faq: + order: Ordem + active: Ativo + faq_translation: + locale: Idioma + question: Pergunta + answer: Resposta errors: uniqueness: "Registro já existe no sistema" template: @@ -655,6 +664,18 @@ pt_BR: attributes: attachment_content_type: invalid_type: inválido + faq: + attributes: + order: + taken: já está em uso por outro FAQ ativo + active: + inclusion: deve ser selecionado + faq_translation: + attributes: + question: + blank: não pode ficar em branco + answer: + blank: não pode ficar em branco ########### GERAL ############### @@ -5712,29 +5733,3 @@ pt_BR: question: Digite a pergunta... answer: Digite a resposta... - activerecord: - models: - faq: FAQ - faq_translation: Tradução do FAQ - attributes: - faq: - order: Ordem - active: Ativo - faq_translation: - locale: Idioma - question: Pergunta - answer: Resposta - errors: - models: - faq: - attributes: - order: - taken: já está em uso por outro FAQ ativo - active: - inclusion: deve ser selecionado - faq_translation: - attributes: - question: - blank: não pode ficar em branco - answer: - blank: não pode ficar em branco