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 e59f2262d..a4fba8548 100755 --- a/app/assets/javascripts/application.js.erb +++ b/app/assets/javascripts/application.js.erb @@ -48,6 +48,8 @@ // = require initialize-jspanel +// = require faqs + /** * General */ diff --git a/app/assets/javascripts/faqs.js.erb b/app/assets/javascripts/faqs.js.erb new file mode 100644 index 000000000..ee355e87b --- /dev/null +++ b/app/assets/javascripts/faqs.js.erb @@ -0,0 +1,185 @@ +<%# @encoding: UTF-8 %> + +function faq_save(){ + if (typeof CKEDITOR !== 'undefined') { + for (var instanceName in CKEDITOR.instances) { + CKEDITOR.instances[instanceName].updateElement(); + } + } + + $('form#faq_form').serialize_and_submit({ + replace_list: $('.list_faqs') + }); +} + +function initFaqCKEditor(textareaId) { + if (typeof CKEDITOR !== 'undefined') { + if (CKEDITOR.instances[textareaId]) { + CKEDITOR.instances[textareaId].destroy(); + } + CKEDITOR.replace(textareaId); + } +} + +function updateFaqRemoveButtons() { + $('#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(); + } + }); +} + +$(document).off('click.faqForm', '#add-translation').on('click.faqForm', '#add-translation', function(e) { + e.preventDefault(); + e.stopImmediatePropagation(); + + var translationIndex = $('#translations-container .translation-fields').length; + var $template = $('#translations-container .translation-fields').first().clone(); + + $template.find('.cke').remove(); + + $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; + + $template.find('input, select, textarea').each(function() { + var $field = $(this); + + if ($field.attr('name')) { + $field.attr('name', $field.attr('name').replace(/\[\d+\]/, '[' + translationIndex + ']')); + } + + if ($field.attr('id')) { + var newId = $field.attr('id').replace(/_\d+_/, '_' + translationIndex + '_'); + $field.attr('id', newId); + + if ($field.attr('name') && $field.attr('name').indexOf('[answer]') > -1) { + newAnswerId = newId; + } + } + }); + + $template.find('label').each(function() { + if ($(this).attr('for')) { + $(this).attr('for', $(this).attr('for').replace(/_\d+_/, '_' + translationIndex + '_')); + } + }); + + if ($template.find('.remove-translation').length === 0) { + $template.prepend(''); + } + + $('#translations-container').append($template); + + if (newAnswerId) { + setTimeout(function() { + initFaqCKEditor(newAnswerId); + }, 100); + } + + updateFaqRemoveButtons(); +}); + +$(document).off('click.faqForm', '.remove-translation').on('click.faqForm', '.remove-translation', function(e) { + e.preventDefault(); + e.stopImmediatePropagation(); + + 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; + } + + 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(); + } + } + + // 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(); +}); + +jQuery(function ($) { + $(".link_new_faq").call_fancybox({ + width: '70%' + }); + + $(".link_show_faq").call_fancybox({ + width: '70%' + }); + + $(".btn_edit[data-link-edit]").on('click', function(e) { + e.preventDefault(); + + 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; + }); +}); + +jQuery(function ($) { + 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('<%= I18n.t("faqs.error.order") %>'); + } + }); +}); 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/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 new file mode 100644 index 000000000..1e96f83dc --- /dev/null +++ b/app/controllers/faqs_controller.rb @@ -0,0 +1,123 @@ +class FaqsController < ApplicationController + + layout false, except: [:index, :apresentation] + + before_action :require_admin + before_action :set_faq, only: [:show, :edit, :update, :destroy, :toggle_active, :order] + skip_before_action :require_admin, only:[:apresentation] + + def index + @faqs = Faq.all + render layout: false if params[:layout].present? && params[:layout] == 'false' + end + + def show + end + + def new + @faq = Faq.new(active: true) + @faq.faq_translations.build(locale: 'pt_BR') + @faq.faq_translations.build(locale: 'en_US') + end + + def create + @faq = Faq.new(faq_params) + + begin + @faq.save! + render json: {success: true, notice: t('faqs.success.created')} + rescue + render :new + end + end + + def edit + @faq.translation_pt + @faq.translation_en + end + + def update + begin + @faq.update!(faq_params) + render json: {success: true, notice: t('faqs.success.updated')} + rescue + render :edit + end + end + + def destroy + @faq.destroy + + 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 + + def toggle_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 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 + + def order + faq1 = @faq + faq2 = Faq.find(params[:change_id]) + + order1 = faq1.order + order2 = faq2.order + + 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 + render json: { success: false, error: e.message }, status: :unprocessable_entity + end + + def apresentation + @faq_order = Faq.faq_translations + render :faq + end + + private + def require_admin + unless current_user.admin? + redirect_to home_path, alert: t(:no_permission) + end + end + + def set_faq + @faq = Faq.find(params[:id]) + end + + def faq_params + params.require(:faq).permit( + :order, + :active, + faq_translations_attributes: [ + :id, + :locale, + :question, + :answer, + :_destroy + ] + ) + end +end 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/models/faq.rb b/app/models/faq.rb new file mode 100644 index 000000000..62378d5d8 --- /dev/null +++ b/app/models/faq.rb @@ -0,0 +1,67 @@ +class Faq < ApplicationRecord + has_many :faq_translations, dependent: :destroy + 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 + + default_scope {order(:order)} + + scope :faq_translations, -> { includes(:faq_translations).where(active: true) } + + def question + translation_for(I18n.locale)&.question + end + + def answer + translation_for(I18n.locale)&.answer + end + + def translation_for(locale) + faq_translations.find_by(locale: locale.to_s) + end + + 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 + + def unique_order_for_active_faqs + return unless active + + 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 + + def reorder_faqs_if_order_changed + return unless order_changed? && persisted? + + old_order = order_was + new_order = order + + return if old_order == new_order + + Faq.unscoped do + if new_order < old_order + Faq.where('id != ? AND "order" >= ? AND "order" < ?', id, new_order, old_order) + .update_all('"order" = "order" + 1') + else + Faq.where('id != ? AND "order" > ? AND "order" <= ?', id, old_order, new_order) + .update_all('"order" = "order" - 1') + end + end + end + +end diff --git a/app/models/faq_translation.rb b/app/models/faq_translation.rb new file mode 100644 index 000000000..df1324be0 --- /dev/null +++ b/app/models/faq_translation.rb @@ -0,0 +1,15 @@ +class FaqTranslation < ApplicationRecord + belongs_to :faq + + 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 + validates :locale, uniqueness: { scope: :faq_id, message: "já existe para este FAQ" } + validates :question, uniqueness: { + scope: :locale, + case_sensitive: false, + message: "já existe neste idioma. Não é permitido perguntas duplicadas." + } +end diff --git a/app/views/faqs/_faq_row.html.haml b/app/views/faqs/_faq_row.html.haml new file mode 100644 index 000000000..1746fdfa6 --- /dev/null +++ b/app/views/faqs/_faq_row.html.haml @@ -0,0 +1,20 @@ +%td{style: 'text-align: center;'} + = 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', 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{:'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') + - 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'), :'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 new file mode 100644 index 000000000..e9ff399ba --- /dev/null +++ b/app/views/faqs/_form.html.haml @@ -0,0 +1,57 @@ += 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 + + = 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') + + #translations-container + = f.simple_fields_for :faq_translations do |translation_form| + .translation-fields + + - 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') + + = 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 + + = translation_form.input :question, required: true + = translation_form.input :answer, as: :ckeditor, required: true + + = translation_form.input :_destroy, as: :hidden, input_html: { class: 'destroy-flag' } + + %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', :'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' + +: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 new file mode 100644 index 000000000..bcc583279 --- /dev/null +++ b/app/views/faqs/edit.html.haml @@ -0,0 +1 @@ += render 'form' 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..90c37a53c --- /dev/null +++ b/app/views/faqs/index.html.haml @@ -0,0 +1,145 @@ +.block_wrapper.list_faqs{:"data-link-list" => admin_faq_index_path(layout: false)} + .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', :'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', :'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', :'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') + %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, "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(){ + window.toggle_answer = function(element, event) { + event.preventDefault(); + var $element = $(element); + var $answer = $element.next('.faq-answer'); + var $icon = $element.find('i'); + var isExpanded = $answer.is(':visible'); + + $answer.toggle(); + + 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'); + } + }; + + window.click_on_keypress = function(event, element) { + if (event.which == 13) { + $(element).click(); + } + }; + + $('.all_faqs').on('change', function() { + var isChecked = $(this).is(':checked'); + $('.ckb_faq').prop('checked', isChecked); + toggleActionButtons(); + }); + + $('.ckb_faq').on('change', function() { + toggleActionButtons(); + }); + + window.toggleActionButtons = function() { + var checkedBoxes = $('.ckb_faq:checked'); + var $editBtn = $('.btn_edit[data-link-edit]'); + var $deleteBtn = $('.delete_faq[data-link-delete]'); + + if (checkedBoxes.length === 0) { + $editBtn.attr('disabled', true).attr('href', '#void'); + $deleteBtn.attr('disabled', true).attr('href', '#void'); + } else if (checkedBoxes.length === 1) { + 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 { + $editBtn.attr('disabled', true).attr('href', '#void'); + $deleteBtn.attr('disabled', false); + } + }; + + $('.delete_faq').on('click', function(e) { + e.preventDefault(); + + if ($(this).attr('disabled')) { + return false; + } + + var checkedBoxes = $('.ckb_faq:checked'); + if (checkedBoxes.length === 0) { + return false; + } + + if (!confirm("#{t('faqs.index.confirm_delete', default: 'Tem certeza que deseja deletar esta(s) FAQ(s)?')}")) { + return false; + } + + var faqIds = []; + checkedBoxes.each(function() { + faqIds.push($(this).val()); + }); + + deleteFaqs(faqIds, checkedBoxes); + }); + + function deleteFaqs(faqIds, checkedBoxes) { + 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', + dataType: 'json', + headers: { + 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') + } + }) + ); + }); + + $.when.apply($, promises).done(function() { + 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("#{t('faqs.error.delete', default: 'Erro ao deletar FAQ(s)')}", 'alert'); + }); + } + }); + += javascript_include_tag 'faqs' diff --git a/app/views/faqs/new.html.haml b/app/views/faqs/new.html.haml new file mode 100644 index 000000000..3c55de36f --- /dev/null +++ b/app/views/faqs/new.html.haml @@ -0,0 +1,2 @@ += render 'form' + diff --git a/app/views/faqs/show.html.haml b/app/views/faqs/show.html.haml new file mode 100644 index 000000000..4c9be3443 --- /dev/null +++ b/app/views/faqs/show.html.haml @@ -0,0 +1,35 @@ +%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;'}= "🇧🇷 #{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)') + %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;'}= "🇺🇸 #{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)') + %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.show.close', default: 'Fechar'), type: 'button', onclick: "jQuery.fancybox.close()", class: 'btn btn_default btn_lightbox' diff --git a/app/views/faqs/toggle_active.js.haml b/app/views/faqs/toggle_active.js.haml new file mode 100644 index 000000000..b44752792 --- /dev/null +++ b/app/views/faqs/toggle_active.js.haml @@ -0,0 +1,27 @@ +- 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'); + + - # 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(); }); + + - # Re-inicializar fancybox no link de visualizar + $row.find('.link_show_faq').call_fancybox({ width: '70%' }); + + - # 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/app/views/pages/faq.html.haml b/app/views/pages/faq.html.haml index a8c006e99..679fa16ed 100644 --- a/app/views/pages/faq.html.haml +++ b/app/views/pages/faq.html.haml @@ -11,231 +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.still_pending') - .invisible - %p.title_child_first= raw t('faq.answers.still_pending') - %h4.title_child_first= raw t('faq.answers.still_pending_automatic') - %p.title_child_second= raw t('faq.answers.still_pending_automatic_details') - %p.title_child_second= raw t('faq.answers.still_pending_automatic_details2') - %h4.title_child_first= raw t('faq.answers.still_pending_manually') - %p.title_child_second=raw t('faq.answers.still_pending_manually_details') - - .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) 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 3e2cd1afd..72563298a 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" @@ -1714,6 +1735,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" @@ -5303,3 +5325,80 @@ en_US: 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. must_have_comment: It is not possible to create a comment without a previous comment. + + # FAQ Translations + faqs: + new: New FAQ + edit: Edit FAQ + show: + title: View FAQ + 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 + 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? + select_all_aria: Select all FAQs + 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) + 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 + error: + order: Error updating FAQ order + + 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... + diff --git a/config/locales/pt_BR.yml b/config/locales/pt_BR.yml index 93e40dd3f..c8e5aa485 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 ############### @@ -1982,7 +2003,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" @@ -5287,6 +5309,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?" still_pending: "Discente alcançou frequências e notas satisfatórias para aprovação, mas o Resultado apresenta situação 'Pendente', o que fazer?" @@ -5333,6 +5356,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:" + 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." 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:" @@ -5623,3 +5652,86 @@ pt_BR: 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. must_have_comment: Não é possível criar um comentário sem que exista um comentário anterior. + + # Traduções FAQ + faqs: + new: Novo FAQ + edit: Editar FAQ + 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 + 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? + select_all_aria: Selecionar todas as FAQs + 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) + 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: + 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... + diff --git a/config/routes.rb b/config/routes.rb index 76f647f9f..eb90ae4c8 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,15 @@ end end - scope "/admin" do + get "/faqs/apresentation", to: "faqs#apresentation", as: 'faqs_questions' + scope "/admin" do + resources :faqs, as: 'admin_faq' do + 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 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 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 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 b4eab6b36..6f8908e2b 100644 --- a/test/fixtures/menus.yml +++ b/test/fixtures/menus.yml @@ -258,6 +258,14 @@ admin_reports: order: 14 resource_id: 226 +#FAQ +admin_faq: + id: 615 + parent_id: 60 + name: menu_admin_faq + order: 15 + resource_id: 247 + admin_notifications: id: 804 parent_id: 60 diff --git a/test/fixtures/menus_contexts.yml b/test/fixtures/menus_contexts.yml index c012b376c..79bf1c405 100644 --- a/test/fixtures/menus_contexts.yml +++ b/test/fixtures/menus_contexts.yml @@ -218,3 +218,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/fixtures/permissions_resources.yml b/test/fixtures/permissions_resources.yml index aeb2549c9..5d8707125 100644 --- a/test/fixtures/permissions_resources.yml +++ b/test/fixtures/permissions_resources.yml @@ -2681,4 +2681,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 diff --git a/test/fixtures/resources.yml b/test/fixtures/resources.yml index d3772212e..d427ed30c 100644 --- a/test/fixtures/resources.yml +++ b/test/fixtures/resources.yml @@ -1250,4 +1250,11 @@ res246: id: 246 controller: "schedule_events" action: 'index' - description: 'Lista de eventos' \ No newline at end of file + description: 'Lista de eventos' + +#FAQ - Perguntas e respostas +res247: + id: 247 + controller: 'faqs' + action: 'index' + description: 'Lista de perguntas e respostas' 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