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 %}
-
-