diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..388ce13 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,13 @@ +| Q | A +| ---------------- | ----- +| Bug report? | yes/no +| Feature request? | yes/no +| Novo SGA version | x.y.z +| PHP version | x.y.z +| Database version | MySQL 5.7/PostgreSQL 15 +| Platform/OS | Linux/Windows + + diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..10f5bd2 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,24 @@ +name: CI + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: shivammathur/setup-php@2.28.0 + with: + php-version: 8.2 + + - name: Install dependencies + run: composer install + + - name: PHP Code Standards + run: vendor/bin/phpcs + + - name: PHP Code Analysis + run: vendor/bin/phpstan + + - name: PHP Unit Tests + run: vendor/bin/phpunit diff --git a/.phpunit.result.cache b/.phpunit.result.cache new file mode 100644 index 0000000..2de2a6d --- /dev/null +++ b/.phpunit.result.cache @@ -0,0 +1 @@ +{"version":1,"defects":[],"times":[]} \ No newline at end of file diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..b545cee --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + src/ + tests/ + + diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..d268c2f --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + level: 6 + paths: + - src/ + - tests/ diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..8a916fc --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + tests + + + + + + src + + + + + + diff --git a/src/Controller/DefaultController.php b/src/Controller/DefaultController.php index 1da7279..25d4c20 100644 --- a/src/Controller/DefaultController.php +++ b/src/Controller/DefaultController.php @@ -55,7 +55,7 @@ public function index( UsuarioRepositoryInterface $repository, ): Response { $search = $request->get('q'); - /** @var Usuario */ + /** @var UsuarioInterface */ $usuario = $this->getUser(); $unidade = $usuario->getLotacao()->getUnidade(); @@ -188,6 +188,7 @@ private function form( ): Response { /** @var UsuarioInterface */ $currentUser = $this->getUser(); + $unidade = $currentUser->getLotacao()->getUnidade(); $unidades = $unidadeRepository->findByUsuario($currentUser); $isAdmin = $currentUser->isAdmin(); @@ -211,7 +212,7 @@ private function form( throw new Exception($error); } } - + $lotacoesRemovidas = []; if ($form->isSubmitted() && $form->isValid()) { try { @@ -226,7 +227,7 @@ private function form( $error = $translator->trans('error.remove_lotation_permission_denied', [ '%unidade%' => $lotacao->getUnidade(), ], NovosgaUsersBundle::getDomain()); - + throw new Exception($error); } $lotacoesRemovidas[] = $lotacao; @@ -246,15 +247,16 @@ private function form( if ($unidade && $perfil) { if (!$isAdmin && !in_array($unidade, $unidades)) { - $error = $translator->trans('error.add_lotation_permission_denied', [ - '%unidade%' => $lotacao->getUnidade(), - ], NovosgaUsersBundle::getDomain()); - + $error = $translator->trans( + 'error.add_lotation_permission_denied', + [ '%unidade%' => $unidade ], + NovosgaUsersBundle::getDomain(), + ); throw new Exception($error); } - + $lotacao = null; - + // tenta reaproveitar uma lotacao da mesma unidade foreach ($lotacoesRemovidas as $l) { if ($l->getUnidade()->getId() === $unidade->getId()) { @@ -264,9 +266,10 @@ private function form( } if (!$lotacao) { - $lotacao = $lotacaoService->build(); - $lotacao->setUnidade($unidade); - $lotacao->setUsuario($entity); + $lotacao = $lotacaoService + ->build() + ->setUnidade($unidade) + ->setUsuario($entity); } $lotacao->setPerfil($perfil); @@ -283,7 +286,11 @@ private function form( $unidadesMap = []; foreach ($entity->getLotacoes() as $lotacao) { if (isset($unidadesMap[$lotacao->getUnidade()->getId()])) { - throw new Exception($translator->trans('error.more_than_one_lotation', [], NovosgaUsersBundle::getDomain())); + throw new Exception($translator->trans( + 'error.more_than_one_lotation', + [], + NovosgaUsersBundle::getDomain(), + )); } $unidadesMap[$lotacao->getUnidade()->getId()] = true; } @@ -299,12 +306,12 @@ private function form( $entity ->setSenha($encoded) ->setAtivo(true) - ->setAdmin(false); + ->setAdmin(false); } - + $em->persist($entity); $em->flush(); - + if (!$isNew) { $lotacoes = $entity->getLotacoes()->toArray(); $lotacao = end($lotacoes); @@ -315,7 +322,11 @@ private function form( ); } - $this->addFlash('success', $translator->trans('label.add_success', [], NovosgaUsersBundle::getDomain())); + $this->addFlash('success', $translator->trans( + 'label.add_success', + [], + NovosgaUsersBundle::getDomain(), + )); return $this->redirectToRoute('novosga_users_edit', [ 'id' => $entity->getId() ]); } catch (Exception $e) { diff --git a/src/DependencyInjection/NovosgaUsersExtension.php b/src/DependencyInjection/NovosgaUsersExtension.php index c3829e6..0c2f319 100644 --- a/src/DependencyInjection/NovosgaUsersExtension.php +++ b/src/DependencyInjection/NovosgaUsersExtension.php @@ -28,7 +28,7 @@ class NovosgaUsersExtension extends Extension */ public function load(array $configs, ContainerBuilder $container) { - $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); $loader->load('services.yml'); } } diff --git a/src/Form/ChangePasswordType.php b/src/Form/ChangePasswordType.php index f21d570..ee22ead 100644 --- a/src/Form/ChangePasswordType.php +++ b/src/Form/ChangePasswordType.php @@ -24,10 +24,7 @@ class ChangePasswordType extends AbstractType { - /** - * @param FormBuilderInterface $builder - * @param array $options - */ + /** {@inheritDoc} */ public function buildForm(FormBuilderInterface $builder, array $options) { $builder @@ -44,8 +41,8 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'constraints' => [ new Length([ 'min' => 6 ]), new Callback(function ($object, ExecutionContextInterface $context, $payload) { - $form = $context->getRoot(); - $senha = $form->get('senha'); + $form = $context->getRoot(); + $senha = $form->get('senha'); $confirmacao = $form->get('confirmacaoSenha'); if ($senha->getData() !== $confirmacao->getData()) { @@ -61,16 +58,15 @@ public function buildForm(FormBuilderInterface $builder, array $options) ]); } - /** - * {@inheritdoc} - */ + /** {@inheritDoc} */ public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults([ 'translation_domain' => 'NovosgaUsersBundle', ]); } - + + /** {@inheritDoc} */ public function getBlockPrefix() { return ''; diff --git a/src/Form/LotacaoType.php b/src/Form/LotacaoType.php index 0c7daba..ef2b374 100644 --- a/src/Form/LotacaoType.php +++ b/src/Form/LotacaoType.php @@ -13,80 +13,64 @@ namespace Novosga\UsersBundle\Form; -use Doctrine\ORM\EntityRepository; -use App\Entity\Perfil; -use App\Entity\Lotacao; -use App\Entity\Unidade; -use App\Entity\Usuario; -use Symfony\Bridge\Doctrine\Form\Type\EntityType; +use Novosga\Entity\LotacaoInterface; +use Novosga\Entity\PerfilInterface; +use Novosga\Entity\UnidadeInterface; +use Novosga\Entity\UsuarioInterface; +use Novosga\Repository\PerfilRepositoryInterface; +use Novosga\Repository\UnidadeRepositoryInterface; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; class LotacaoType extends AbstractType { - /** - * @param FormBuilderInterface $builder - * @param array $options - */ + public function __construct( + private readonly UnidadeRepositoryInterface $unidadeRepository, + private readonly PerfilRepositoryInterface $perfilRepository, + ) { + } + + /** {@inheritDoc} */ public function buildForm(FormBuilderInterface $builder, array $options) { - $ignore = $options['ignore']; + $ignoreList = (array) $options['ignore']; $usuario = $options['usuario']; - + $unidadesUsuario = $this->unidadeRepository->findByUsuario($usuario); + $unidadesDisponiveis = array_values(array_filter( + $unidadesUsuario, + fn (UnidadeInterface $unidade) => !in_array($unidade->getId(), $ignoreList), + )); + $builder - ->add('unidade', EntityType::class, [ - 'class' => Unidade::class, + ->add('unidade', ChoiceType::class, [ 'placeholder' => '', - 'query_builder' => function (EntityRepository $er) use ($usuario, $ignore) { - $qb = $er - ->createQueryBuilder('e') - ->where('e.deletedAt IS NULL') - ->orderBy('e.nome', 'ASC'); - - if (!$usuario->isAdmin()) { - $qb - ->join(Lotacao::class, 'l', 'WITH', 'l.unidade = e') - ->andWhere('l.usuario = :usuario') - ->andWhere('e.deletedAt IS NULL') - ->setParameter('usuario', $usuario); - } - - if (count($ignore)) { - $qb - ->andWhere('e.id NOT IN (:ignore)') - ->setParameter('ignore', $ignore); - } - - return $qb; - }, + 'choice_value' => fn (?UnidadeInterface $value) => $value?->getId(), + 'choice_label' => fn (?UnidadeInterface $value) => $value?->getNome(), + 'choices' => $unidadesDisponiveis, 'label' => 'form.lotacao.unidade', ]) - ->add('perfil', EntityType::class, [ - 'class' => Perfil::class, + ->add('perfil', ChoiceType::class, [ 'placeholder' => '', - 'query_builder' => function (EntityRepository $er) { - return $er - ->createQueryBuilder('e') - ->orderBy('e.nome', 'ASC'); - }, + 'choice_value' => fn (?PerfilInterface $value) => $value?->getId(), + 'choice_label' => fn (?PerfilInterface $value) => $value?->getNome(), + 'choices' => $this->perfilRepository->findAll(), 'label' => 'form.lotacao.perfil', ]) ; } - - /** - * - * @param OptionsResolver $resolver - */ + + /** {@inheritDoc} */ public function configureOptions(OptionsResolver $resolver) { $resolver ->setDefaults([ - 'data_class' => Lotacao::class, + 'data_class' => LotacaoInterface::class, 'translation_domain' => 'NovosgaUsersBundle', ]) ->setRequired(['usuario', 'ignore']) - ->setAllowedTypes('usuario', [ Usuario::class ]); + ->setAllowedTypes('ignore', [ 'array' ]) + ->setAllowedTypes('usuario', [ UsuarioInterface::class ]); } } diff --git a/src/Form/UsuarioType.php b/src/Form/UsuarioType.php index 847ff48..d80b2b1 100644 --- a/src/Form/UsuarioType.php +++ b/src/Form/UsuarioType.php @@ -13,34 +13,30 @@ namespace Novosga\UsersBundle\Form; -use App\Entity\Usuario; +use Novosga\Entity\UsuarioInterface; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\Extension\Core\Type\PasswordType; +use Symfony\Component\Form\Extension\Core\Type\RepeatedType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\Email; use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotNull; use Symfony\Component\Validator\Constraints\Regex; -use Symfony\Component\Validator\Context\ExecutionContextInterface; class UsuarioType extends AbstractType { - /** - * @param FormBuilderInterface $builder - * @param array $options - */ + /** {@inheritDoc} */ public function buildForm(FormBuilderInterface $builder, array $options) { $entity = $options['data']; $isAdmin = $options['admin']; - + $builder ->add('login', TextType::class, [ 'attr' => [ @@ -86,7 +82,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'label' => 'form.user.admin', ]); } - + if ($entity->getId()) { $builder->add('ativo', CheckboxType::class, [ 'required' => false, @@ -97,46 +93,29 @@ public function buildForm(FormBuilderInterface $builder, array $options) ]); } else { $builder - ->add('senha', PasswordType::class, [ + ->add('senha', RepeatedType::class, [ 'mapped' => false, + 'type' => PasswordType::class, 'constraints' => [ new NotNull(), new Length([ 'min' => 6 ]), ], - 'label' => 'form.user.password', - ]) - ->add('confirmacaoSenha', PasswordType::class, [ - 'mapped' => false, - 'constraints' => [ - new Length([ 'min' => 6 ]), - new Callback(function ($object, ExecutionContextInterface $context, $payload) { - $form = $context->getRoot(); - $senha = $form->get('senha'); - $confirmacao = $form->get('confirmacaoSenha'); - - if ($senha->getData() !== $confirmacao->getData()) { - $context - ->buildViolation('error.password_confirm') - ->setTranslationDomain('NovosgaUsersBundle') - ->atPath('confirmacaoSenha') - ->addViolation(); - } - }), + 'first_options' => [ + 'label' => 'form.user.password', + ], + 'second_options' => [ + 'label' => 'form.user.password_confirm', ], - 'label' => 'form.user.password_confirm', ]); } } - - /** - * - * @param OptionsResolver $resolver - */ + + /** {@inheritDoc} */ public function configureOptions(OptionsResolver $resolver) { $resolver ->setDefaults([ - 'data_class' => Usuario::class, + 'data_class' => UsuarioInterface::class, 'translation_domain' => 'NovosgaUsersBundle', ]) ->setRequired('admin'); diff --git a/src/Resources/config/services.yml b/src/Resources/config/services.yml index 03423d6..dd7adde 100644 --- a/src/Resources/config/services.yml +++ b/src/Resources/config/services.yml @@ -12,4 +12,7 @@ services: Novosga\UsersBundle\Controller\: resource: '../../Controller' - tags: ['controller.service_arguments'] \ No newline at end of file + tags: ['controller.service_arguments'] + + Novosga\UsersBundle\Form\: + resource: '../../Form' diff --git a/src/Resources/public/js/script.js b/src/Resources/public/js/script.js index a7b51e2..a206bcf 100644 --- a/src/Resources/public/js/script.js +++ b/src/Resources/public/js/script.js @@ -4,112 +4,95 @@ */ (function () { 'use strict' - - var dialogPerfil = new Vue({ - el: '#dialog-perfil', + + new Vue({ + el: '#users-form', data: { - perfil: null + perfil: null, + lotacaoModal: null, + perfilModal: null, + senhaModal: null, + lotacoes: lotacoes, + lotacoesRemovidas: lotacoesRemovidas, + errors: {}, + }, + computed: { + idsLotacoesRemovidas() { + return this.lotacoesRemovidas.map((lotacao) => lotacao.id).join(','); + } }, methods: { - viewPerfil: function (id) { - var self = this; + viewPerfil(id) { App.ajax({ url: App.url('/novosga.users/perfis/') + id, - success: function (response) { - self.perfil = response.data; - new bootstrap.Modal('#dialog-perfil').show(); + success: (response) => { + this.perfil = response.data; + this.perfilModal.show(); } }); }, - } - }); - - var lotacoesTable = new Vue({ - el: '#lotacoes', - data: { - lotacoes: lotacoes, - lotacoesRemovidas: lotacoesRemovidas - }, - computed: { - idsLotacoesRemovidas: function () { - return this.lotacoesRemovidas.map(function (lotacao) { - return lotacao.id; - }).join(','); - } - }, - methods: { - add: function (lotacao) { + add(lotacao) { lotacao.novo = true; this.lotacoes.push(lotacao); }, - remove: function (lotacao) { + remove(lotacao) { this.lotacoes.splice(this.lotacoes.indexOf(lotacao), 1); if (lotacao.id) { this.lotacoesRemovidas.push(lotacao); } }, - viewPerfil: function (id) { - dialogPerfil.viewPerfil(id); + async alterarSenha(e) { + this.errors = {}; + const form = e.target + const resp = await fetch(form.action, { + method: form.method || 'post', + body: new FormData(form) + }); + const result = await resp.json(); + if (!result.data.error) { + form.reset(); + this.senhaModal.hide(); + } else { + this.errors = result.data.errors || {}; + } + }, + handleLotacaoSubmit() { + const perfil = document.getElementById('lotacao_perfil'); + const unidade = document.getElementById('lotacao_unidade'); + + if (perfil && unidade) { + this.add({ + unidade: { + id: unidade.value, + nome: unidade.innerText, + }, + perfil: { + id: perfil.value, + nome: perfil.innerText, + } + }); + } + + this.lotacaoModal.hide(); }, - } - }); - - new Vue({ - el: '#dialog-senha', - data: { - errors: {} + showSenhaModal() { + this.senhaModal.show(); + }, + async showLotacaoModal() { + const ids = this.lotacoes.map((lotacao) => lotacao.unidade.id); + const resp = await fetch(App.url('/novosga.users/novalotacao?ignore=') + ids.join(',')); + const text = await resp.text(); + this.$refs.lotacaoModal.querySelector('.modal-body').innerHTML = text; + + this.lotacaoModal.show(); + } }, - methods: { - alterarSenha: function (e) { - var $elem = $(e.target), self = this; - - self.errors = {}; - $.ajax({ - url: $elem.attr('action'), - type: $elem.attr('method'), - data: $elem.serialize(), - success: function (response) { - if (!response.data.error) { - $elem.trigger('reset'); - $('#dialog-senha').modal('hide'); - } else { - self.errors = response.data.errors ? response.data.errors : {}; - } - }, - }); + mounted() { + this.lotacaoModal = new bootstrap.Modal(this.$refs.lotacaoModal); + this.perfilModal = new bootstrap.Modal(this.$refs.perfilModal); + if (this.$refs.senhaModal) { + this.senhaModal = new bootstrap.Modal(this.$refs.senhaModal); } } }); - - $('#dialog-lotacao').on('show.bs.modal', function () { - var ids = lotacoesTable.lotacoes.map(function(lotacao) { - return lotacao.unidade.id; - }); - - $(this) - .find('.modal-body') - .load(App.url('/novosga.users/novalotacao?ignore=') + ids.join(',')); - }); - - $('#lotacao-form').on('submit', function(e) { - e.preventDefault(); - - var perfil = $('#lotacao_perfil :selected'), - unidade = $('#lotacao_unidade :selected'); - - if (perfil.val() && unidade.val()) { - lotacoesTable.add({ - unidade: { - id: unidade.val(), - nome: unidade.text(), - }, - perfil: { - id: perfil.val(), - nome: perfil.text(), - } - }); - } - - $('#dialog-lotacao').modal('hide'); - }); })(); diff --git a/src/Resources/views/default/form.html.twig b/src/Resources/views/default/form.html.twig index bc74e57..b370256 100644 --- a/src/Resources/views/default/form.html.twig +++ b/src/Resources/views/default/form.html.twig @@ -3,6 +3,7 @@ {% trans_default_domain 'NovosgaUsersBundle' %} {% block body %} +

{{ 'title'|trans }} @@ -10,9 +11,9 @@ {{ 'subtitle'|trans }}

- +
- + {{ form_start(form) }} {% include 'flashMessages.html.twig' %} @@ -55,8 +56,8 @@
{% endif %} {% if entity.id %} -
- @@ -66,9 +67,8 @@ {% endif %} {% if not entity.id %} - {{ form_row(form.senha) }} - - {{ form_row(form.confirmacaoSenha) }} + {{ form_row(form.senha.first) }} + {{ form_row(form.senha.second) }} {% endif %}
@@ -92,15 +92,15 @@ {% verbatim %} {{ lotacao.unidade.nome }} {% endverbatim %} - + {% verbatim %} {{ lotacao.perfil.nome }} {% endverbatim %} - + - + @@ -115,8 +115,8 @@ - - @@ -126,7 +126,6 @@ {{ form_row(form.lotacoesRemovidas, { attr: { 'v-model': 'idsLotacoesRemovidas' } }) }} - @@ -143,15 +142,13 @@ {{ 'button.back'|trans }} - {{ form_end(form) }} - {# lotação #} -