From f6b9517e504b6eff7691a7f12d7f21d6bf38bbf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 7 May 2019 21:32:41 +0200 Subject: [PATCH] initial commit --- Calculator/CalculatorInterface.php | 22 ++ Calculator/CalculatorManager.php | 67 +++++ Calculator/CalculatorResult.php | 23 ++ ChillAMLIBudgetBundle.php | 16 ++ Config/ConfigRepository.php | 72 ++++++ Controller/AbstractElementController.php | 207 ++++++++++++++++ Controller/ChargeController.php | 102 ++++++++ Controller/ElementController.php | 96 ++++++++ Controller/ResourceController.php | 103 ++++++++ .../ChillAMLIBudgetExtension.php | 61 +++++ .../Compiler/CalculatorCompilerPass.php | 30 +++ DependencyInjection/Configuration.php | 78 ++++++ Entity/AbstractElement.php | 225 +++++++++++++++++ Entity/Charge.php | 86 +++++++ Entity/Resource.php | 55 +++++ Form/ChargeType.php | 124 ++++++++++ Form/ResourceType.php | 108 +++++++++ Menu/UserMenuBuilder.php | 60 +++++ README.md | 5 + Repository/ChargeRepository.php | 35 +++ Repository/ResourceRepository.php | 35 +++ Resources/config/routing.yml | 3 + Resources/config/services/calculator.yml | 2 + Resources/config/services/config.yml | 5 + Resources/config/services/controller.yml | 5 + Resources/config/services/form.yml | 14 ++ Resources/config/services/menu.yml | 7 + Resources/config/services/security.yml | 7 + Resources/config/services/templating.yml | 7 + .../migrations/Version20180522080432.php | 41 ++++ .../migrations/Version20181219145631.php | 26 ++ Resources/translations/messages.fr.yml | 60 +++++ Resources/translations/validators.fr.yml | 2 + .../views/Charge/confirm_delete.html.twig | 19 ++ Resources/views/Charge/edit.html.twig | 31 +++ Resources/views/Charge/new.html.twig | 31 +++ Resources/views/Charge/view.html.twig | 52 ++++ Resources/views/Element/index.html.twig | 228 ++++++++++++++++++ .../views/Resource/confirm_delete.html.twig | 19 ++ Resources/views/Resource/edit.html.twig | 30 +++ Resources/views/Resource/new.html.twig | 30 +++ Resources/views/Resource/view.html.twig | 52 ++++ Security/Authorization/BudgetElementVoter.php | 79 ++++++ Templating/Twig.php | 64 +++++ Tests/Controller/ElementControllerTest.php | 23 ++ composer.json | 32 +++ 46 files changed, 2479 insertions(+) create mode 100644 Calculator/CalculatorInterface.php create mode 100644 Calculator/CalculatorManager.php create mode 100644 Calculator/CalculatorResult.php create mode 100644 ChillAMLIBudgetBundle.php create mode 100644 Config/ConfigRepository.php create mode 100644 Controller/AbstractElementController.php create mode 100644 Controller/ChargeController.php create mode 100644 Controller/ElementController.php create mode 100644 Controller/ResourceController.php create mode 100644 DependencyInjection/ChillAMLIBudgetExtension.php create mode 100644 DependencyInjection/Compiler/CalculatorCompilerPass.php create mode 100644 DependencyInjection/Configuration.php create mode 100644 Entity/AbstractElement.php create mode 100644 Entity/Charge.php create mode 100644 Entity/Resource.php create mode 100644 Form/ChargeType.php create mode 100644 Form/ResourceType.php create mode 100644 Menu/UserMenuBuilder.php create mode 100644 README.md create mode 100644 Repository/ChargeRepository.php create mode 100644 Repository/ResourceRepository.php create mode 100644 Resources/config/routing.yml create mode 100644 Resources/config/services/calculator.yml create mode 100644 Resources/config/services/config.yml create mode 100644 Resources/config/services/controller.yml create mode 100644 Resources/config/services/form.yml create mode 100644 Resources/config/services/menu.yml create mode 100644 Resources/config/services/security.yml create mode 100644 Resources/config/services/templating.yml create mode 100644 Resources/migrations/Version20180522080432.php create mode 100644 Resources/migrations/Version20181219145631.php create mode 100644 Resources/translations/messages.fr.yml create mode 100644 Resources/translations/validators.fr.yml create mode 100644 Resources/views/Charge/confirm_delete.html.twig create mode 100644 Resources/views/Charge/edit.html.twig create mode 100644 Resources/views/Charge/new.html.twig create mode 100644 Resources/views/Charge/view.html.twig create mode 100644 Resources/views/Element/index.html.twig create mode 100644 Resources/views/Resource/confirm_delete.html.twig create mode 100644 Resources/views/Resource/edit.html.twig create mode 100644 Resources/views/Resource/new.html.twig create mode 100644 Resources/views/Resource/view.html.twig create mode 100644 Security/Authorization/BudgetElementVoter.php create mode 100644 Templating/Twig.php create mode 100644 Tests/Controller/ElementControllerTest.php create mode 100644 composer.json diff --git a/Calculator/CalculatorInterface.php b/Calculator/CalculatorInterface.php new file mode 100644 index 000000000..91ad6b7bf --- /dev/null +++ b/Calculator/CalculatorInterface.php @@ -0,0 +1,22 @@ + + */ +interface CalculatorInterface +{ + /** + * + * @param AbstractElement[] $elements + */ + public function calculate(array $elements) : ?CalculatorResult; + + public function getAlias(); +} diff --git a/Calculator/CalculatorManager.php b/Calculator/CalculatorManager.php new file mode 100644 index 000000000..ae90a448e --- /dev/null +++ b/Calculator/CalculatorManager.php @@ -0,0 +1,67 @@ + + */ +class CalculatorManager +{ + /** + * + * @var CalculatorInterface[] + */ + protected $calculators = []; + + protected $defaultCalculator = []; + + public function addCalculator(CalculatorInterface $calculator, bool $default) + { + $this->calculators[$calculator::getAlias()] = $calculator; + + if ($default) { + $this->defaultCalculator[] = $calculator::getAlias(); + } + } + + /** + * + * @param string $alias + * @return CalculatorInterface + */ + public function getCalculator($alias) + { + if (FALSE === \array_key_exists($alias, $this->calculators)) { + throw new \OutOfBoundsException("The calculator with alias '$alias' does " + . "not exists. Possible values are ". \implode(", ", \array_keys($this->calculators))); + } + + return $this->calculators[$alias]; + } + + /** + * + * @param AbstractElement[] $elements + * @return CalculatorResult[] + */ + public function calculateDefault(array $elements) + { + $results = []; + + foreach ($this->defaultCalculator as $alias) { + $calculator = $this->calculators[$alias]; + $result = $calculator->calculate($elements); + + if ($result !== null) { + $results[$calculator::getAlias()] = $result; + } + } + + return $results; + } +} diff --git a/Calculator/CalculatorResult.php b/Calculator/CalculatorResult.php new file mode 100644 index 000000000..1066dab25 --- /dev/null +++ b/Calculator/CalculatorResult.php @@ -0,0 +1,23 @@ + + */ +class CalculatorResult +{ + const TYPE_RATE = 'rate'; + const TYPE_CURRENCY = 'currency'; + const TYPE_PERCENTAGE = 'percentage'; + + public $type; + + public $result; + + public $label; +} diff --git a/ChillAMLIBudgetBundle.php b/ChillAMLIBudgetBundle.php new file mode 100644 index 000000000..0d68e1cbf --- /dev/null +++ b/ChillAMLIBudgetBundle.php @@ -0,0 +1,16 @@ +addCompilerPass(new CalculatorCompilerPass()); + } +} diff --git a/Config/ConfigRepository.php b/Config/ConfigRepository.php new file mode 100644 index 000000000..2cc579d6b --- /dev/null +++ b/Config/ConfigRepository.php @@ -0,0 +1,72 @@ + + */ +class ConfigRepository +{ + /** + * + * @var array + */ + protected $resources; + + /** + * + * @var array + */ + protected $charges; + + public function __construct($resources, $charges) + { + $this->resources = $resources; + $this->charges = $charges; + } + + /** + * + * @return array where keys are the resource'key and label the ressource label + */ + public function getResourcesLabels() + { + $resources = array(); + + foreach ($this->resources as $definition) { + $resources[$definition['key']] = $this->normalizeLabel($definition['labels']); + } + + return $resources; + } + + /** + * + * @return array where keys are the resource'key and label the ressource label + */ + public function getChargesLabels() + { + $charges = array(); + + foreach ($this->charges as $definition) { + $charges[$definition['key']] = $this->normalizeLabel($definition['labels']); + } + + return $charges; + } + + private function normalizeLabel($labels) + { + $normalizedLabels = array(); + + foreach ($labels as $labelDefinition) { + $normalizedLabels[$labelDefinition['lang']] = $labelDefinition['label']; + } + + return $normalizedLabels; + } + +} diff --git a/Controller/AbstractElementController.php b/Controller/AbstractElementController.php new file mode 100644 index 000000000..9ef3758b0 --- /dev/null +++ b/Controller/AbstractElementController.php @@ -0,0 +1,207 @@ +em = $em; + $this->translator = $translator; + $this->chillMainLogger = $chillMainLogger; + } + + /** + * @return AbstractElement the newly created element + */ + abstract protected function createNewElement(); + + abstract protected function getType(); + + /** + * + */ + protected function _new(Person $person, Request $request, $template, $flashMessageOnSuccess) + { + /* @var $element \Chill\AMLI\BudgetBundle\Entity\AbstractElement */ + $element = $this->createNewElement() + ->setPerson($person) + ; + + $this->denyAccessUnlessGranted(BudgetElementVoter::CREATE, $element); + + $form = $this->createForm($this->getType(), $element); + $form->add('submit', SubmitType::class); + + $form->handleRequest($request); + + if ($form->isSubmitted() and $form->isValid()) { + $em = $this->getDoctrine()->getManager(); + $em->persist($element); + $em->flush(); + + $this->addFlash('success', $this->translator->trans($flashMessageOnSuccess)); + + return $this->redirectToRoute('chill_budget_elements_index', [ + 'id' => $person->getId() + ]); + } elseif ($form->isSubmitted()) { + $this->addFlash('error', $this->translator->trans('This form contains errors')); + } + + return $this->render($template, array( + 'form' => $form->createView(), + 'person' => $person, + 'element' => $element + )); + } + + /** + * + * @param AbstractElement $element + * @param Request $request + * @param string $template + * @param string $flashOnSuccess + * @return \Symfony\Component\HttpFoundation\Response + */ + protected function _edit(AbstractElement $element, Request $request, $template, $flashOnSuccess) + { + $this->denyAccessUnlessGranted(BudgetElementVoter::UPDATE, $element); + + $form = $this->createForm($this->getType(), $element); + $form->add('submit', SubmitType::class); + + $form->handleRequest($request); + + if ($form->isSubmitted() and $form->isValid()) { + $em = $this->getDoctrine()->getManager(); + $em->flush(); + + $this->addFlash('success', $this->translator->trans($flashOnSuccess)); + + return $this->redirectToRoute('chill_budget_elements_index', [ + 'id' => $element->getPerson()->getId() + ]); + } + + return $this->render($template, array( + 'element' => $element, + 'form' => $form->createView(), + 'person' => $element->getPerson() + )); + } + + /** + * + * Route( + * "{_locale}/family-members/family-members/{id}/delete", + * name="chill_family_members_family_members_delete" + * ) + * + * @param AbstractElement $element + * @param Request $request + * @return \Symfony\Component\BrowserKit\Response + */ + protected function _delete(AbstractElement $element, Request $request, $template, $flashMessage) + { + $this->denyAccessUnlessGranted(BudgetElementVoter::DELETE, $element, 'You are not ' + . 'allowed to delete this family membership'); + + $form = $this->createDeleteForm(); + + if ($request->getMethod() === Request::METHOD_DELETE) { + $form->handleRequest($request); + + if ($form->isValid()) { + $this->chillMainLogger->notice("A budget element has been removed", array( + 'family_element' => get_class($element), + 'by_user' => $this->getUser()->getUsername(), + 'family_member_id' => $element->getId(), + 'amount' => $element->getAmount(), + 'type' => $element->getType() + )); + + $em = $this->getDoctrine()->getManager(); + $em->remove($element); + $em->flush(); + + $this->addFlash('success', $this->translator + ->trans($flashMessage)); + + return $this->redirectToRoute('chill_budget_elements_index', array( + 'id' => $element->getPerson()->getId() + )); + } + } + + + return $this->render($template, array( + 'element' => $element, + 'delete_form' => $form->createView() + )); + } + + /** + * Route( + * "{_locale}/family-members/family-members/{id}/view", + * name="chill_family_members_family_members_view" + * ) + */ + protected function _view(AbstractElement $element, $template) + { + $this->denyAccessUnlessGranted(BudgetElementVoter::SHOW, $element); + + return $this->render($template, array( + 'element' => $element + )); + } + + /** + * Creates a form to delete a help request entity by id. + * + * @param mixed $id The entity id + * + * @return \Symfony\Component\Form\Form The form + */ + private function createDeleteForm() + { + return $this->createFormBuilder() + ->setMethod(Request::METHOD_DELETE) + ->add('submit', SubmitType::class, array('label' => 'Delete')) + ->getForm() + ; + } + +} diff --git a/Controller/ChargeController.php b/Controller/ChargeController.php new file mode 100644 index 000000000..370d1abb2 --- /dev/null +++ b/Controller/ChargeController.php @@ -0,0 +1,102 @@ + + */ +class ChargeController extends AbstractElementController +{ + protected function getType() + { + return ChargeType::class; + } + + protected function createNewElement() + { + return new Charge(); + } + + /** + * + * @Route( + * "{_locale}/budget/charge/{id}/view", + * name="chill_budget_charge_view" + * ) + * @param Charge $charge + * @return \Symfony\Component\HttpFoundation\Response + */ + public function viewAction(Charge $charge) + { + return $this->_view($charge, '@ChillAMLIBudget/Charge/view.html.twig'); + } + + /** + * @Route( + * "{_locale}/budget/charge/by-person/{id}/new", + * name="chill_budget_charge_new" + * ) + * + * @param Request $request + * @param Person $person + * @return \Symfony\Component\HttpFoundation\Response + */ + public function newAction(Request $request, Person $person) + { + return $this->_new( + $person, + $request, + '@ChillAMLIBudget/Charge/new.html.twig', + 'Charge created'); + } + + /** + * @Route( + * "{_locale}/budget/charge/{id}/edit", + * name="chill_budget_charge_edit" + * ) + * + * @param Request $request + * @param Charge $charge + * @return \Symfony\Component\HttpFoundation\Response + */ + public function editAction(Request $request, Charge $charge) + { + return $this->_edit( + $charge, + $request, + '@ChillAMLIBudget/Charge/edit.html.twig', + 'Charge updated'); + } + + /** + * + * @Route( + * "{_locale}/budget/charge/{id}/delete", + * name="chill_budget_charge_delete" + * ) + * + * @param Request $request + * @param Charge $charge + * @return \Symfony\Component\HttpFoundation\Response + */ + public function deleteAction(Request $request, Charge $charge) + { + return $this->_delete( + $charge, + $request, + '@ChillAMLIBudget/Charge/confirm_delete.html.twig', + 'Charge deleted'); + } +} diff --git a/Controller/ElementController.php b/Controller/ElementController.php new file mode 100644 index 000000000..d5f6b4bea --- /dev/null +++ b/Controller/ElementController.php @@ -0,0 +1,96 @@ +em = $em; + $this->translator = $translator; + $this->chillMainLogger = $chillMainLogger; + $this->calculator = $calculator; + } + + /** + * @Route( + * "{_locale}/budget/elements/by-person/{id}", + * name="chill_budget_elements_index" + * ) + */ + public function indexAction(Person $person) + { + $this->denyAccessUnlessGranted(BudgetElementVoter::SHOW, $person); + + $charges = $this->em + ->getRepository(Charge::class) + ->findByPerson($person); + $ressources = $this->em + ->getRepository(Resource::class) + ->findByPerson($person); + + $now = new \DateTime('now'); + + $actualCharges = $this->em + ->getRepository(Charge::class) + ->findByPersonAndDate($person, $now); + $actualResources = $this->em + ->getRepository(Resource::class) + ->findByPersonAndDate($person, $now); + + $elements = \array_merge($actualCharges, $actualResources); + + if (count($elements) > 0) { + $results = $this->calculator->calculateDefault($elements); + } + + return $this->render('ChillAMLIBudgetBundle:Element:index.html.twig', array( + 'person' => $person, + 'charges' => $charges, + 'resources' => $ressources, + 'results' => $results ?? [] + )); + } + +} diff --git a/Controller/ResourceController.php b/Controller/ResourceController.php new file mode 100644 index 000000000..8b252ea48 --- /dev/null +++ b/Controller/ResourceController.php @@ -0,0 +1,103 @@ + + */ +class ResourceController extends AbstractElementController +{ + + protected function getType() + { + return ResourceType::class; + } + + protected function createNewElement() + { + return new Resource(); + } + + /** + * + * @Route( + * "{_locale}/budget/resource/{id}/view", + * name="chill_budget_resource_view" + * ) + * @param Request $request + * @param Resource $resource + * @return \Symfony\Component\HttpFoundation\Response + */ + public function viewAction(Resource $resource) + { + return $this->_view($resource, '@ChillAMLIBudget/Resource/view.html.twig'); + } + + /** + * @Route( + * "{_locale}/budget/resource/by-person/{id}/new", + * name="chill_budget_resource_new" + * ) + * + * @param Request $request + * @param Person $person + * @return \Symfony\Component\HttpFoundation\Response + */ + public function newAction(Request $request, Person $person) + { + return $this->_new( + $person, + $request, + '@ChillAMLIBudget/Resource/new.html.twig', + 'Resource created'); + } + + /** + * @Route( + * "{_locale}/budget/resource/{id}/edit", + * name="chill_budget_resource_edit" + * ) + * + * @param Request $request + * @param Resource $resource + * @return \Symfony\Component\HttpFoundation\Response + */ + public function editAction(Request $request, Resource $resource) + { + return $this->_edit( + $resource, + $request, + '@ChillAMLIBudget/Resource/edit.html.twig', + 'Resource updated'); + } + + /** + * + * @Route( + * "{_locale}/budget/resource/{id}/delete", + * name="chill_budget_resource_delete" + * ) + * + * @param Request $request + * @param Resource $resource + * @return \Symfony\Component\HttpFoundation\Response + */ + public function deleteAction(Request $request, Resource $resource) + { + return $this->_delete($resource, + $request, + '@ChillAMLIBudget/Resource/confirm_delete.html.twig', + 'Resource deleted'); + } +} diff --git a/DependencyInjection/ChillAMLIBudgetExtension.php b/DependencyInjection/ChillAMLIBudgetExtension.php new file mode 100644 index 000000000..0ece33a4a --- /dev/null +++ b/DependencyInjection/ChillAMLIBudgetExtension.php @@ -0,0 +1,61 @@ +processConfiguration($configuration, $configs); + + $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + $loader->load('services/config.yml'); + $loader->load('services/form.yml'); + $loader->load('services/security.yml'); + $loader->load('services/controller.yml'); + $loader->load('services/templating.yml'); + $loader->load('services/menu.yml'); + $loader->load('services/calculator.yml'); + + $this->storeConfig('resources', $config, $container); + $this->storeConfig('charges', $config, $container); + } + + public function prepend(ContainerBuilder $container) + { + $this->prependAuthorization($container); + } + + protected function storeConfig($position, array $config, ContainerBuilder $container) + { + $container + ->setParameter(sprintf('chill_budget.%s', $position), $config[$position]) + ; + } + + protected function prependAuthorization(ContainerBuilder $container) + { + $container->prependExtensionConfig('security', array( + 'role_hierarchy' => array( + BudgetElementVoter::UPDATE => [ BudgetElementVoter::SHOW ], + BudgetElementVoter::CREATE => [ BudgetElementVoter::SHOW ] + ) + )); + } +} diff --git a/DependencyInjection/Compiler/CalculatorCompilerPass.php b/DependencyInjection/Compiler/CalculatorCompilerPass.php new file mode 100644 index 000000000..30e5ae8da --- /dev/null +++ b/DependencyInjection/Compiler/CalculatorCompilerPass.php @@ -0,0 +1,30 @@ + + */ +class CalculatorCompilerPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container) + { + $manager = $container->getDefinition('Chill\AMLI\BudgetBundle\Calculator\CalculatorManager'); + + foreach ($container->findTaggedServiceIds('chill_budget.calculator') as $id => $tags) { + foreach($tags as $tag) { + $reference = new Reference($id); + + $manager->addMethodCall('addCalculator', [ $reference, $tag['default'] ?? false ]); + } + } + } +} diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php new file mode 100644 index 000000000..7774e08fd --- /dev/null +++ b/DependencyInjection/Configuration.php @@ -0,0 +1,78 @@ +root('chill_amli_budget'); + + $rootNode + ->children() + + // ressources + ->arrayNode('resources')->isRequired()->requiresAtLeastOneElement() + ->arrayPrototype() + ->children() + ->scalarNode('key')->isRequired()->cannotBeEmpty() + ->info('the key stored in database') + ->example('salary') + ->end() + ->arrayNode('labels')->isRequired()->requiresAtLeastOneElement() + ->arrayPrototype() + ->children() + ->scalarNode('lang')->isRequired()->cannotBeEmpty() + ->example('fr') + ->end() + ->scalarNode('label')->isRequired()->cannotBeEmpty() + ->example('Salaire') + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + + ->arrayNode('charges')->isRequired()->requiresAtLeastOneElement() + ->arrayPrototype() + ->children() + ->scalarNode('key')->isRequired()->cannotBeEmpty() + ->info('the key stored in database') + ->example('salary') + ->end() + ->arrayNode('labels')->isRequired()->requiresAtLeastOneElement() + ->arrayPrototype() + ->children() + ->scalarNode('lang')->isRequired()->cannotBeEmpty() + ->example('fr') + ->end() + ->scalarNode('label')->isRequired()->cannotBeEmpty() + ->example('Salaire') + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + + ->end() + ; + + return $treeBuilder; + } +} diff --git a/Entity/AbstractElement.php b/Entity/AbstractElement.php new file mode 100644 index 000000000..985af9676 --- /dev/null +++ b/Entity/AbstractElement.php @@ -0,0 +1,225 @@ +person; + } + + public function setPerson(Person $person) + { + $this->person = $person; + + return $this; + } + + /** + * Set type. + * + * @param string $type + * + * @return AbstractElement + */ + public function setType($type) + { + $this->type = $type; + + return $this; + } + + /** + * Get type. + * + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * Set amount. + * + * @param string $amount + * + * @return AbstractElement + */ + public function setAmount($amount) + { + $this->amount = $amount; + + return $this; + } + + /** + * Get amount. + * + * @return double + */ + public function getAmount() + { + return (double) $this->amount; + } + + /** + * Set comment. + * + * @param string|null $comment + * + * @return AbstractElement + */ + public function setComment($comment = null) + { + $this->comment = $comment; + + return $this; + } + + /** + * Get comment. + * + * @return string|null + */ + public function getComment() + { + return $this->comment; + } + + /** + * Set startDate. + * + * @param \DateTimeInterface $startDate + * + * @return AbstractElement + */ + public function setStartDate(\DateTimeInterface $startDate) + { + if ($startDate instanceof \DateTime) { + $this->startDate = \DateTimeImmutable::createFromMutable($startDate); + } elseif (NULL === $startDate) { + $this->startDate = null; + } else { + $this->startDate = $startDate; + } + + return $this; + } + + /** + * Get startDate. + * + * @return \DateTimeImmutable + */ + public function getStartDate() + { + return $this->startDate; + } + + /** + * Set endDate. + * + * @param \DateTimeInterface|null $endDate + * + * @return AbstractElement + */ + public function setEndDate(\DateTimeInterface $endDate = null) + { + if ($endDate instanceof \DateTime) { + $this->endDate = \DateTimeImmutable::createFromMutable($endDate); + } elseif (NULL === $endDate) { + $this->endDate = null; + } else { + $this->endDate = $endDate; + } + + return $this; + } + + /** + * Get endDate. + * + * @return \DateTimeImmutable|null + */ + public function getEndDate() + { + return $this->endDate; + } + + public function isEmpty() + { + return $this->amount == 0; + } +} diff --git a/Entity/Charge.php b/Entity/Charge.php new file mode 100644 index 000000000..a330c382d --- /dev/null +++ b/Entity/Charge.php @@ -0,0 +1,86 @@ +setStartDate(new \DateTimeImmutable('today')); + } + + /** + * Get id. + * + * @return int + */ + public function getId() + { + return $this->id; + } + + public function getHelp() + { + return $this->help; + } + + public function setHelp($help) + { + $this->help = $help; + + return $this; + } + + public function getCenter(): \Chill\MainBundle\Entity\Center + { + return $this->getPerson()->getCenter(); + } + + public function isCharge(): bool + { + return true; + } + + public function isResource(): bool + { + return false; + } +} diff --git a/Entity/Resource.php b/Entity/Resource.php new file mode 100644 index 000000000..390268592 --- /dev/null +++ b/Entity/Resource.php @@ -0,0 +1,55 @@ +setStartDate(new \DateTimeImmutable('today')); + } + + + /** + * Get id. + * + * @return int + */ + public function getId() + { + return $this->id; + } + + public function getCenter(): \Chill\MainBundle\Entity\Center + { + return $this->getPerson()->getCenter(); + } + + public function isCharge(): bool + { + return false; + } + + public function isResource(): bool + { + return true; + } +} diff --git a/Form/ChargeType.php b/Form/ChargeType.php new file mode 100644 index 000000000..da219abb9 --- /dev/null +++ b/Form/ChargeType.php @@ -0,0 +1,124 @@ +configRepository = $configRepository; + $this->translatableStringHelper = $translatableStringHelper; + } + + + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + ->add('type', ChoiceType::class, [ + 'choices' => $this->getTypes(), + 'placeholder' => 'Choose a charge type' + ]) + ->add('amount', MoneyType::class) + ->add('comment', TextAreaType::class, [ + 'required' => false + ]) + ; + + if ($options['show_start_date']) { + $builder->add('startDate', ChillDateType::class, [ + 'label' => 'Start of validity period' + ]); + } + + if ($options['show_end_date']) { + $builder->add('endDate', ChillDateType::class, [ + 'required' => false, + 'label' => 'End of validity period' + ]); + } + + if ($options['show_help']) { + $builder->add('help', ChoiceType::class, [ + 'choices' => [ + 'charge.help.running' => Charge::HELP_ASKED, + 'charge.help.no' => Charge::HELP_NO, + 'charge.help.yes' => Charge::HELP_YES, + 'charge.help.not-concerned' => Charge::HELP_NOT_RELEVANT + ], + 'placeholder' => 'Choose a status', + 'required' => false, + 'label' => 'Help to pay charges' + ]); + } + } + + private function getTypes() + { + $charges = $this->configRepository + ->getChargesLabels(); + + // rewrite labels to filter in language + foreach ($charges as $key => $labels) { + $charges[$key] = $this->translatableStringHelper->localize($labels); + } + + \asort($charges); + + return \array_flip($charges); + } + + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults(array( + 'data_class' => 'Chill\AMLI\BudgetBundle\Entity\Charge', + 'show_start_date' => true, + 'show_end_date' => true, + 'show_help' => true + )); + + $resolver + ->setAllowedTypes('show_start_date', 'boolean') + ->setAllowedTypes('show_end_date', 'boolean') + ->setAllowedTypes('show_help', 'boolean') + ; + } + + /** + * {@inheritdoc} + */ + public function getBlockPrefix() + { + return 'chill_amli_budgetbundle_charge'; + } + + +} diff --git a/Form/ResourceType.php b/Form/ResourceType.php new file mode 100644 index 000000000..abf7191ab --- /dev/null +++ b/Form/ResourceType.php @@ -0,0 +1,108 @@ +configRepository = $configRepository; + $this->translatableStringHelper = $translatableStringHelper; + } + + + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + ->add('type', ChoiceType::class, [ + 'choices' => $this->getTypes(), + 'placeholder' => 'Choose a resource type', + 'label' => 'Resource element type' + ]) + ->add('amount', MoneyType::class) + ->add('comment', TextAreaType::class, [ + 'required' => false + ]) + ; + + if ($options['show_start_date']) { + $builder->add('startDate', ChillDateType::class, [ + 'label' => 'Start of validity period' + ]); + } + + if ($options['show_end_date']) { + $builder->add('endDate', ChillDateType::class, [ + 'required' => false, + 'label' => 'End of validity period' + ]); + } + } + + private function getTypes() + { + $resources = $this->configRepository + ->getResourcesLabels(); + + // rewrite labels to filter in language + foreach ($resources as $key => $labels) { + $resources[$key] = $this->translatableStringHelper->localize($labels); + } + + asort($resources); + + return \array_flip($resources); + } + + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults(array( + 'data_class' => Resource::class, + 'show_start_date' => true, + 'show_end_date' => true + )); + + $resolver + ->setAllowedTypes('show_start_date', 'boolean') + ->setAllowedTypes('show_end_date', 'boolean') + ; + } + + /** + * {@inheritdoc} + */ + public function getBlockPrefix() + { + return 'chill_amli_budgetbundle_resource'; + } + + +} diff --git a/Menu/UserMenuBuilder.php b/Menu/UserMenuBuilder.php new file mode 100644 index 000000000..f2b395267 --- /dev/null +++ b/Menu/UserMenuBuilder.php @@ -0,0 +1,60 @@ + + */ +class UserMenuBuilder implements LocalMenuBuilderInterface +{ + /** + * + * @var AuthorizationCheckerInterface + */ + protected $authorizationChecker; + + /** + * + * @var TranslatorInterface + */ + protected $translator; + + public function __construct( + AuthorizationCheckerInterface $authorizationChecker, + TranslatorInterface $translator + ) { + $this->authorizationChecker = $authorizationChecker; + $this->translator = $translator; + } + + + public function buildMenu($menuId, MenuItem $menu, array $parameters) + { + /* @var $person \Chill\PersonBundle\Entity\Person */ + $person = $parameters['person']; + + if ($this->authorizationChecker->isGranted(BudgetElementVoter::SHOW, $person)) { + $menu->addChild( + $this->translator->trans('Budget'), [ + 'route' => 'chill_budget_elements_index', + 'routeParameters' => [ 'id' => $person->getId() ], + ]) + ->setExtra('order', 460) + ; + } + } + + public static function getMenuIds(): array + { + return [ 'person' ]; + } +} diff --git a/README.md b/README.md new file mode 100644 index 000000000..345faaad7 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +Budget Bundle +============= + +Add the possibility to register budget element (charges and resources) for people. + diff --git a/Repository/ChargeRepository.php b/Repository/ChargeRepository.php new file mode 100644 index 000000000..73a310d24 --- /dev/null +++ b/Repository/ChargeRepository.php @@ -0,0 +1,35 @@ +createQueryBuilder('c'); + + $qb->where('c.person = :person') + ->andWhere('c.startDate < :date') + ->andWhere('c.startDate < :date OR c.startDate IS NULL') + ; + + if ($sort !== null) { + $qb->orderBy($sort); + } + + $qb->setParameters([ + 'person' => $person, + 'date' => $date + ]); + + return $qb->getQuery()->getResult(); + } +} diff --git a/Repository/ResourceRepository.php b/Repository/ResourceRepository.php new file mode 100644 index 000000000..50197ce65 --- /dev/null +++ b/Repository/ResourceRepository.php @@ -0,0 +1,35 @@ +createQueryBuilder('c'); + + $qb->where('c.person = :person') + ->andWhere('c.startDate < :date') + ->andWhere('c.startDate < :date OR c.startDate IS NULL') + ; + + if ($sort !== null) { + $qb->orderBy($sort); + } + + $qb->setParameters([ + 'person' => $person, + 'date' => $date + ]); + + return $qb->getQuery()->getResult(); + } +} diff --git a/Resources/config/routing.yml b/Resources/config/routing.yml new file mode 100644 index 000000000..a771dd748 --- /dev/null +++ b/Resources/config/routing.yml @@ -0,0 +1,3 @@ +chill_amli_budget_controllers: + resource: "@ChillAMLIBudgetBundle/Controller" + type: annotation diff --git a/Resources/config/services/calculator.yml b/Resources/config/services/calculator.yml new file mode 100644 index 000000000..7022f400e --- /dev/null +++ b/Resources/config/services/calculator.yml @@ -0,0 +1,2 @@ +services: + Chill\AMLI\BudgetBundle\Calculator\CalculatorManager: ~ diff --git a/Resources/config/services/config.yml b/Resources/config/services/config.yml new file mode 100644 index 000000000..a03bd1db2 --- /dev/null +++ b/Resources/config/services/config.yml @@ -0,0 +1,5 @@ +services: + Chill\AMLI\BudgetBundle\Config\ConfigRepository: + arguments: + $resources: '%chill_budget.resources%' + $charges: '%chill_budget.charges%' diff --git a/Resources/config/services/controller.yml b/Resources/config/services/controller.yml new file mode 100644 index 000000000..f69837a58 --- /dev/null +++ b/Resources/config/services/controller.yml @@ -0,0 +1,5 @@ +services: + Chill\AMLI\BudgetBundle\Controller\: + autowire: true + resource: '../../../Controller' + tags: ['controller.service_arguments'] diff --git a/Resources/config/services/form.yml b/Resources/config/services/form.yml new file mode 100644 index 000000000..0a4f451fd --- /dev/null +++ b/Resources/config/services/form.yml @@ -0,0 +1,14 @@ +services: + Chill\AMLI\BudgetBundle\Form\ResourceType: + arguments: + $configRepository: '@Chill\AMLI\BudgetBundle\Config\ConfigRepository' + $translatableStringHelper: '@Chill\MainBundle\Templating\TranslatableStringHelper' + tags: + - { name: 'form.type' } + + Chill\AMLI\BudgetBundle\Form\ChargeType: + arguments: + $configRepository: '@Chill\AMLI\BudgetBundle\Config\ConfigRepository' + $translatableStringHelper: '@Chill\MainBundle\Templating\TranslatableStringHelper' + tags: + - { name: 'form.type' } diff --git a/Resources/config/services/menu.yml b/Resources/config/services/menu.yml new file mode 100644 index 000000000..0b72feb1a --- /dev/null +++ b/Resources/config/services/menu.yml @@ -0,0 +1,7 @@ +services: + Chill\AMLI\BudgetBundle\Menu\UserMenuBuilder: + arguments: + $authorizationChecker: '@Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface' + $translator: '@Symfony\Component\Translation\TranslatorInterface' + tags: + - { name: 'chill.menu_builder' } diff --git a/Resources/config/services/security.yml b/Resources/config/services/security.yml new file mode 100644 index 000000000..128f65f1f --- /dev/null +++ b/Resources/config/services/security.yml @@ -0,0 +1,7 @@ +services: + Chill\AMLI\BudgetBundle\Security\Authorization\BudgetElementVoter: + arguments: + $authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper' + tags: + - { name: chill.role } + - { name: security.voter } diff --git a/Resources/config/services/templating.yml b/Resources/config/services/templating.yml new file mode 100644 index 000000000..49ee3f8c5 --- /dev/null +++ b/Resources/config/services/templating.yml @@ -0,0 +1,7 @@ +services: + Chill\AMLI\BudgetBundle\Templating\Twig: + arguments: + $configRepository: '@Chill\AMLI\BudgetBundle\Config\ConfigRepository' + $translatableStringHelper: '@Chill\MainBundle\Templating\TranslatableStringHelper' + tags: + - { name: 'twig.extension' } \ No newline at end of file diff --git a/Resources/migrations/Version20180522080432.php b/Resources/migrations/Version20180522080432.php new file mode 100644 index 000000000..b326cb452 --- /dev/null +++ b/Resources/migrations/Version20180522080432.php @@ -0,0 +1,41 @@ +abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); + + $this->addSql('CREATE SCHEMA chill_budget'); + $this->addSql('DROP SEQUENCE report_id_seq CASCADE'); + $this->addSql('CREATE SEQUENCE chill_budget.resource_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE chill_budget.charge_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE chill_budget.resource (id INT NOT NULL, person_id INT DEFAULT NULL, type VARCHAR(255) NOT NULL, amount NUMERIC(10, 2) NOT NULL, comment TEXT DEFAULT NULL, startDate TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, endDate TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_5E0A5E97217BBB47 ON chill_budget.resource (person_id)'); + $this->addSql('COMMENT ON COLUMN chill_budget.resource.startDate IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_budget.resource.endDate IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('CREATE TABLE chill_budget.charge (id INT NOT NULL, person_id INT DEFAULT NULL, type VARCHAR(255) NOT NULL, amount NUMERIC(10, 2) NOT NULL, comment TEXT DEFAULT NULL, startDate TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, endDate TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, help VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_5C99D2C3217BBB47 ON chill_budget.charge (person_id)'); + $this->addSql('COMMENT ON COLUMN chill_budget.charge.startDate IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_budget.charge.endDate IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE chill_budget.resource ADD CONSTRAINT FK_5E0A5E97217BBB47 FOREIGN KEY (person_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_budget.charge ADD CONSTRAINT FK_5C99D2C3217BBB47 FOREIGN KEY (person_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + + } + + public function down(Schema $schema) + { + $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); + + $this->addSql('CREATE SCHEMA chill_budget CASCADE'); + + } +} diff --git a/Resources/migrations/Version20181219145631.php b/Resources/migrations/Version20181219145631.php new file mode 100644 index 000000000..b9c01cd7c --- /dev/null +++ b/Resources/migrations/Version20181219145631.php @@ -0,0 +1,26 @@ +abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); + + $this->addSql('ALTER TABLE chill_budget.charge ALTER help DROP NOT NULL'); + } + + public function down(Schema $schema) : void + { + $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); + + $this->addSql('ALTER TABLE chill_budget.charge ALTER help SET NOT NULL'); + } +} diff --git a/Resources/translations/messages.fr.yml b/Resources/translations/messages.fr.yml new file mode 100644 index 000000000..30188907a --- /dev/null +++ b/Resources/translations/messages.fr.yml @@ -0,0 +1,60 @@ +Budget: Budget +Budget for %name%: Budget de %name% +Resource element type: Nature de la ressource +Actual budget: Budget actuel +Actual resources: Ressources actuelles +Actual charges: Charges actuelles +Past budget: Éléments du budget passé +Past resources: Ressources passées +Past charges: Chargées passées +Future budget: Futurs éléments du budget +Future resources: Ressources futures +Future charges: Charges futures +Budget element type: Nature +Validity period: Période de validité +Start of validity period: Début de la période de validité +End of validity period: Fin de la période de validité +Total: Total +Create new resource: Créer une nouvelle ressource +Create new charge: Créer une nouvelle charge + +There isn't any element recorded: Aucun élément enregistré +No resources registered: Aucune ressource enregistrée +No charges registered: Aucune charge enregistrée +No past resources registered: Aucune ressource passée +No past charges registered: Aucune charge passée +No future resources registered: Aucune ressource future enregistrée +No future charges registered: Aucune ressource future enregistrée + +New Resource for %name%: Nouvelle ressource pour %name% +New Charge for %name%: Nouvelle charge pour %name% +Edit Resource for %name%: Modifier une ressource de %name% +Edit Charge for %name%: Modifier une charge de %name% +Remove resource: Supprimer la ressource +Remove charge: Supprimer la charge +Are you sure you want to remove the ressource "%type%" associated to "%name%" ?: Êtes-vous sûr·e de vouloir supprimer la ressource de nature "%type%" associée à %name% ? +Are you sure you want to remove the charge "%type%" associated to "%name%" ?: Êtes-vous sûr·e de vouloir supprimer la charge de nature "%type%" associée à %name% ? +Resource deleted: Ressource supprimée +Charge deleted: Charge supprimée +Charge created: Charge créée +Resource created: Ressource créée +Resource updated: Resource mise à jour +Charge updated: charge mise à jour + +Choose a resource type: Choisissez un type de ressource +Choose a charge type: Choisissez un type de charge +Amount: Montant +Comment: Commentaire + +Help to pay charges: Aide au payement des dépenses d'énergie +Choose a status: Choisissez un état +charge.help.running: En cours +charge.help.no: Non demandé +charge.help.yes: Oui +charge.help.not-concerned: Non concerné + +Budget calculator: Calculs et indices sur le budget +Budget calculator result: Résultats + +'Valid since %startDate% until %endDate%': Valide depuis le %startDate% jusqu'au %endDate% +'Valid since %startDate%': Valide depuis le %startDate% \ No newline at end of file diff --git a/Resources/translations/validators.fr.yml b/Resources/translations/validators.fr.yml new file mode 100644 index 000000000..7281b6380 --- /dev/null +++ b/Resources/translations/validators.fr.yml @@ -0,0 +1,2 @@ +The amount cannot be empty: Le montant ne peut pas être vide ou égal à zéro +The budget element's end date must be after the start date: La date de fin doit être après la date de début \ No newline at end of file diff --git a/Resources/views/Charge/confirm_delete.html.twig b/Resources/views/Charge/confirm_delete.html.twig new file mode 100644 index 000000000..8d6ecac7b --- /dev/null +++ b/Resources/views/Charge/confirm_delete.html.twig @@ -0,0 +1,19 @@ +{% extends "ChillPersonBundle::layout.html.twig" %} + +{% set activeRouteKey = '' %} +{% set person = element.person %} + +{% block title 'Remove resource'|trans %} + +{% block personcontent %} + +{{ include('ChillMainBundle:Util:confirmation_template.html.twig', + { + 'title' : 'Remove charge'|trans, + 'confirm_question' : 'Are you sure you want to remove the charge "%type%" associated to "%name%" ?'|trans({ '%name%' : person.firstname ~ ' ' ~ person.lastname, '%type%': element.type|budget_element_type_display('charge') } ), + 'cancel_route' : 'chill_budget_elements_index', + 'cancel_parameters' : { 'id' : element.person.id }, + 'form' : delete_form + } ) }} + +{% endblock %} diff --git a/Resources/views/Charge/edit.html.twig b/Resources/views/Charge/edit.html.twig new file mode 100644 index 000000000..dba002163 --- /dev/null +++ b/Resources/views/Charge/edit.html.twig @@ -0,0 +1,31 @@ +{% extends "@ChillPerson/layout.html.twig" %} + +{% set activeRouteKey = '' %} +{% set title = 'Edit Charge for %name%'|trans({ '%name%' : person.firstName ~ " " ~ person.lastName } ) %} +{% block title title %} + +{% block personcontent %} +

{{ title }}

+ +{{ form_start(form) }} + +{{ form_row(form.type) }} +{{ form_row(form.amount) }} +{{ form_row(form.help) }} +{{ form_row(form.comment) }} +{{ form_row(form.startDate) }} +{{ form_row(form.endDate) }} + + + +{{ form_end(form) }} +{% endblock %} diff --git a/Resources/views/Charge/new.html.twig b/Resources/views/Charge/new.html.twig new file mode 100644 index 000000000..5aa0f0728 --- /dev/null +++ b/Resources/views/Charge/new.html.twig @@ -0,0 +1,31 @@ +{% extends "@ChillPerson/layout.html.twig" %} + +{% set activeRouteKey = '' %} +{% set title = 'New Charge for %name%'|trans({ '%name%' : person.firstName ~ " " ~ person.lastName } ) %} +{% block title title %} + +{% block personcontent %} +

{{ title }}

+ +{{ form_start(form) }} + +{{ form_row(form.type) }} +{{ form_row(form.amount) }} +{{ form_row(form.help) }} +{{ form_row(form.comment) }} +{{ form_row(form.startDate) }} +{{ form_row(form.endDate) }} + + + +{{ form_end(form) }} +{% endblock %} diff --git a/Resources/views/Charge/view.html.twig b/Resources/views/Charge/view.html.twig new file mode 100644 index 000000000..e339c06e3 --- /dev/null +++ b/Resources/views/Charge/view.html.twig @@ -0,0 +1,52 @@ +{% extends "@ChillPerson/layout.html.twig" %} + +{% set activeRouteKey = '' %} +{% set person = element.person %} +{% set title = 'Charge for %name%'|trans({ '%name%' : person.firstName ~ " " ~ person.lastName } ) %} + +{% block title title %} + +{% block personcontent %} +

{{ title }}

+ +
+
{{ 'Type'|trans }}
+
{{ element.type|budget_element_type_display('charge') }}
+ +
{{ 'Amount'|trans }}
+
{{ element.amount|localizedcurrency('EUR') }}
+ +
{{ 'Validity period'|trans }}
+
+ {% if element.endDate is not null %} + {{ 'Valid since %startDate% until %endDate%'|trans( { '%startDate%': element.startDate|localizeddate('long', 'none'), '%endDate%': familyMember.endDate|localizeddate('long', 'none') } ) }} + {% else %} + {{ 'Valid since %startDate%'|trans( { '%startDate%': element.startDate|localizeddate('long', 'none') } ) }} + {% endif %} +
+ +
{{ 'Comment'|trans }}
+
+ {%- if element.comment is not empty -%} +
+ {{ element.comment }} +
+ {%- else -%} + {{ 'Not given'|trans }} + {%- endif -%} +
+
+ + +{% endblock %} \ No newline at end of file diff --git a/Resources/views/Element/index.html.twig b/Resources/views/Element/index.html.twig new file mode 100644 index 000000000..b531cd9d7 --- /dev/null +++ b/Resources/views/Element/index.html.twig @@ -0,0 +1,228 @@ +{% extends "@ChillPerson/layout.html.twig" %} + +{% set activeRouteKey = '' %} +{% set title = 'Budget for %name%'|trans({ '%name%' : person.firstName ~ " " ~ person.lastName } ) %} +{% block title title %} + + +{% set actualResources = [] %} +{% set futureResources = [] %} +{% set pastResources = [] %} + +{% for r in resources %} + {% if r.startDate|date('U') <= 'now'|date('U') %} + {% if r.endDate is null or r.endDate|date('U') >= 'now'|date('U') %} + {% set actualResources = actualResources|merge([ r ]) %} + {% else %} + {% set pastResources = pastResources|merge([ r ]) %} + {% endif %} + {% else %} + {% set futureResources = futureResources|merge([ r ]) %} + {% endif %} +{% endfor %} + +{% set actualCharges = [] %} +{% set futureCharges = [] %} +{% set pastCharges = [] %} + +{% for c in charges %} + {% if c.startDate|date('U') <= 'now'|date('U') %} + {% if c.endDate is null or c.endDate|date('U') >= 'now'|date('U') %} + {% set actualCharges = actualCharges|merge([ c ]) %} + {% else %} + {% set pastCharges = pastCharges|merge([ c ]) %} + {% endif %} + {% else %} + {% set futureCharges = futureCharges|merge([ c ]) %} + {% endif %} +{% endfor %} + + +{% macro table_elements(elements, family) %} + + + + + + + + + + + {% set total = 0 %} + {% for f in elements %} + {% set total = total + f.amount %} + + + + + + + {% endfor %} + + + + + + + +
{{ 'Budget element type'|trans }}{{ 'Amount'|trans }}{{ 'Validity period'|trans }} 
+ {{ f.type|budget_element_type_display(family) }} + {{ f.amount|localizedcurrency('EUR') }} + {% if f.endDate is not null %} + {{ 'Valid since %startDate% until %endDate%'|trans( { '%startDate%': f.startDate|localizeddate('long', 'none'), '%endDate%': f.endDate|localizeddate('long', 'none') } ) }} + {% else %} + {{ 'Valid since %startDate%'|trans( { '%startDate%': f.startDate|localizeddate('long', 'none') } ) }} + {% endif %} + +
    + {% if is_granted(constant('Chill\\AMLI\\BudgetBundle\\Security\\Authorization\\BudgetElementVoter::SHOW'), f) %} +
  • + +
  • + {% endif %} + {% if is_granted(constant('Chill\\AMLI\\BudgetBundle\\Security\\Authorization\\BudgetElementVoter::UPDATE'), f) %} +
  • + +
  • + {% endif %} + {% if is_granted(constant('Chill\\AMLI\\BudgetBundle\\Security\\Authorization\\BudgetElementVoter::DELETE'), f) %} +
  • + +
  • + {% endif %} +
+
+ {{ 'Total'|trans }} + + {{ total|localizedcurrency('EUR') }} +   
+{% endmacro %} + +{% macro table_results(results) %} + + + + + + + + + {% for result in results %} + + + + + {% endfor %} + +
 {{ 'Budget calculator result'|trans }}
{{ result.label }} + {% if result.type == constant('CHILL\\AMLI\\BudgetBundle\\Calculator\\CalculatorResult::TYPE_CURRENCY') %} + {{ result.result|localizedcurrency('EUR') }} + {% elseif result.type == constant('CHILL\\AMLI\\BudgetBundle\\Calculator\\CalculatorResult::TYPE_PERCENTAGE') %} + {{ result.result|round(2, 'ceil') ~ '%' }} + {% else %} + {{ result.result|round(2, 'common') }} + {% endif %} +
+{% endmacro %} + +{% import _self as m %} + +{% block personcontent %} +

{{ title }}

+ +

{{ 'Actual budget'|trans }}

+ +{% if resources|length == 0 and charges|length == 0 %} +

{{ "There isn't any element recorded"|trans }}

+{% else %} +

{{ 'Actual resources'|trans }}

+ + {% if actualResources|length > 0 %} + {{ m.table_elements(actualResources, 'resource') }} + {% else %} + {{ 'No resources registered'|trans }} + {% endif %} + +

{{ 'Actual charges'|trans }}

+ + {% if actualCharges|length > 0 %} + {{ m.table_elements(actualCharges, 'charge') }} + {% else %} + {{ 'No charges registered'|trans }} + {% endif %} + + {% if results|length > 0 %} +

{{ 'Budget calculator'|trans }}

+ + {{ m.table_results(results) }} + {% endif %} + + {% if is_granted(constant('Chill\\AMLI\\BudgetBundle\\Security\\Authorization\\BudgetElementVoter::CREATE'), person) %} + + {% endif %} + +{% endif %} + +{% if pastCharges|length > 0 or pastResources|length > 0 %} +

{{ 'Past budget'|trans }}

+ +

{{ 'Past resources'|trans }}

+ + {% if pastResources|length > 0 %} + {{ m.table_elements(pastResources, 'resource') }} + {% else %} + {{ 'No past resources registered'|trans }} + {% endif %} + +

{{ 'Past charges'|trans }}

+ + {% if pastCharges|length > 0 %} + {{ m.table_elements(pastCharges, 'charge') }} + {% else %} + {{ 'No past charges registered'|trans }} + {% endif %} +{% endif %} + +{% if futureCharges|length > 0 or futureResources|length > 0 %} +

{{ 'Future budget'|trans }}

+ +

{{ 'Future resources'|trans }}

+ + {% if futureResources|length > 0 %} + {{ m.table_elements(futureResources, 'resource') }} + {% else %} + {{ 'No future resources registered'|trans }} + {% endif %} + +

{{ 'Future charges'|trans }}

+ + {% if futureCharges|length > 0 %} + {{ m.table_elements(futureCharges, 'charge') }} + {% else %} + {{ 'No future charges registered'|trans }} + {% endif %} +{% endif %} + +{% if (resources|length + charges|length) == 0 or futureCharges|length > 0 or futureResources|length > 0 or pastCharges|length > 0 or pastResources|length > 0 %} + {% if is_granted(constant('Chill\\AMLI\\BudgetBundle\\Security\\Authorization\\BudgetElementVoter::CREATE'), person) %} + + {% endif %} +{% endif %} + +{% endblock %} + diff --git a/Resources/views/Resource/confirm_delete.html.twig b/Resources/views/Resource/confirm_delete.html.twig new file mode 100644 index 000000000..65abcdd69 --- /dev/null +++ b/Resources/views/Resource/confirm_delete.html.twig @@ -0,0 +1,19 @@ +{% extends "ChillPersonBundle::layout.html.twig" %} + +{% set activeRouteKey = '' %} +{% set person = element.person %} + +{% block title 'Remove resource'|trans %} + +{% block personcontent %} + +{{ include('ChillMainBundle:Util:confirmation_template.html.twig', + { + 'title' : 'Remove resource'|trans, + 'confirm_question' : 'Are you sure you want to remove the ressource "%type%" associated to "%name%" ?'|trans({ '%name%' : person.firstname ~ ' ' ~ person.lastname, '%type%': element.type|budget_element_type_display('resource') } ), + 'cancel_route' : 'chill_budget_elements_index', + 'cancel_parameters' : { 'id' : element.person.id }, + 'form' : delete_form + } ) }} + +{% endblock %} diff --git a/Resources/views/Resource/edit.html.twig b/Resources/views/Resource/edit.html.twig new file mode 100644 index 000000000..1b457c1ff --- /dev/null +++ b/Resources/views/Resource/edit.html.twig @@ -0,0 +1,30 @@ +{% extends "@ChillPerson/layout.html.twig" %} + +{% set activeRouteKey = '' %} +{% set title = 'Edit Resource for %name%'|trans({ '%name%' : person.firstName ~ " " ~ person.lastName } ) %} +{% block title title %} + +{% block personcontent %} +

{{ title }}

+ +{{ form_start(form) }} + +{{ form_row(form.type) }} +{{ form_row(form.amount) }} +{{ form_row(form.comment) }} +{{ form_row(form.startDate) }} +{{ form_row(form.endDate) }} + + + +{{ form_end(form) }} +{% endblock %} diff --git a/Resources/views/Resource/new.html.twig b/Resources/views/Resource/new.html.twig new file mode 100644 index 000000000..c6eaeb06e --- /dev/null +++ b/Resources/views/Resource/new.html.twig @@ -0,0 +1,30 @@ +{% extends "@ChillPerson/layout.html.twig" %} + +{% set activeRouteKey = '' %} +{% set title = 'New Resource for %name%'|trans({ '%name%' : person.firstName ~ " " ~ person.lastName } ) %} +{% block title title %} + +{% block personcontent %} +

{{ title }}

+ +{{ form_start(form) }} + +{{ form_row(form.type) }} +{{ form_row(form.amount) }} +{{ form_row(form.comment) }} +{{ form_row(form.startDate) }} +{{ form_row(form.endDate) }} + + + +{{ form_end(form) }} +{% endblock %} diff --git a/Resources/views/Resource/view.html.twig b/Resources/views/Resource/view.html.twig new file mode 100644 index 000000000..b7a082041 --- /dev/null +++ b/Resources/views/Resource/view.html.twig @@ -0,0 +1,52 @@ +{% extends "@ChillPerson/layout.html.twig" %} + +{% set activeRouteKey = '' %} +{% set person = element.person %} +{% set title = 'Resource for %name%'|trans({ '%name%' : person.firstName ~ " " ~ person.lastName } ) %} + +{% block title title %} + +{% block personcontent %} +

{{ title }}

+ +
+
{{ 'Type'|trans }}
+
{{ element.type|budget_element_type_display('resource') }}
+ +
{{ 'Amount'|trans }}
+
{{ element.amount|localizedcurrency('EUR') }}
+ +
{{ 'Validity period'|trans }}
+
+ {% if element.endDate is not null %} + {{ 'Valid since %startDate% until %endDate%'|trans( { '%startDate%': element.startDate|localizeddate('long', 'none'), '%endDate%': familyMember.endDate|localizeddate('long', 'none') } ) }} + {% else %} + {{ 'Valid since %startDate%'|trans( { '%startDate%': element.startDate|localizeddate('long', 'none') } ) }} + {% endif %} +
+ +
{{ 'Comment'|trans }}
+
+ {%- if element.comment is not empty -%} +
+ {{ element.comment }} +
+ {%- else -%} + {{ 'Not given'|trans }} + {%- endif -%} +
+
+ + +{% endblock %} \ No newline at end of file diff --git a/Security/Authorization/BudgetElementVoter.php b/Security/Authorization/BudgetElementVoter.php new file mode 100644 index 000000000..2f7530938 --- /dev/null +++ b/Security/Authorization/BudgetElementVoter.php @@ -0,0 +1,79 @@ + + */ +class BudgetElementVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface +{ + const CREATE = 'CHILL_BUDGET_ELEMENT_CREATE'; + const DELETE = 'CHILL_BUDGET_ELEMENT_DELETE'; + const UPDATE = 'CHILL_BUDGET_ELEMENT_UPDATE'; + const SHOW = 'CHILL_BUDGET_ELEMENT_SHOW'; + + const ROLES = [ + self::CREATE, + self::DELETE, + self::SHOW, + self::UPDATE + ]; + + /** + * + * @var AuthorizationHelper + */ + protected $authorizationHelper; + + public function __construct(AuthorizationHelper $authorizationHelper) + { + $this->authorizationHelper = $authorizationHelper; + } + + + protected function supports($attribute, $subject) + { + return (\in_array($attribute, self::ROLES) && $subject instanceof AbstractElement) + or + ($subject instanceof Person && \in_array($attribute, [ self::SHOW, self::CREATE ])); + } + + protected function voteOnAttribute($attribute, $subject, \Symfony\Component\Security\Core\Authentication\Token\TokenInterface $token) + { + $user = $token->getUser(); + + if (FALSE === $user instanceof User) { + return false; + } + + return $this->authorizationHelper + ->userHasAccess($user, $subject, new Role($attribute)); + } + + public function getRoles() + { + return self::ROLES; + } + + public function getRolesWithHierarchy(): array + { + return [ 'Budget elements' => self::ROLES ]; + } + + public function getRolesWithoutScope() + { + return self::ROLES; + } + +} diff --git a/Templating/Twig.php b/Templating/Twig.php new file mode 100644 index 000000000..a291fcec6 --- /dev/null +++ b/Templating/Twig.php @@ -0,0 +1,64 @@ + + */ +class Twig extends AbstractExtension +{ + /** + * + * @var ConfigRepository + */ + protected $configRepository; + + /** + * + * @var TranslatableStringHelper + */ + protected $translatableStringHelper; + + public function __construct( + ConfigRepository $configRepository, + TranslatableStringHelper $translatableStringHelper + ) { + $this->configRepository = $configRepository; + $this->translatableStringHelper = $translatableStringHelper; + } + + + public function getFilters() + { + return [ + new \Twig_Filter('budget_element_type_display', [ $this, 'displayLink' ], [ 'is_safe' => [ 'html' ]]) + ]; + } + + public function displayLink($link, $family) + { + switch($family) { + case 'resource': + return $this->translatableStringHelper->localize( + $this->configRepository->getResourcesLabels()[$link] + ); + case 'charge': + return $this->translatableStringHelper->localize( + $this->configRepository->getChargesLabels()[$link] + ); + default: + throw new \UnexpectedValueException("This family of element: $family is not " + . "supported. Supported families are 'resource', 'charge'"); + } + + } + +} diff --git a/Tests/Controller/ElementControllerTest.php b/Tests/Controller/ElementControllerTest.php new file mode 100644 index 000000000..b672d51ef --- /dev/null +++ b/Tests/Controller/ElementControllerTest.php @@ -0,0 +1,23 @@ +request('GET', '/list'); + } + + public function testIndex() + { + $client = static::createClient(); + + $crawler = $client->request('GET', '/index'); + } + +} diff --git a/composer.json b/composer.json new file mode 100644 index 000000000..02637be56 --- /dev/null +++ b/composer.json @@ -0,0 +1,32 @@ +{ + "name": "chill-project/budget", + "description": "This bundle extend chill for recording element of a budget for peoples", + "type": "symfony-bundle", + "license": "AGPL-3.0", + "keywords" : ["chill", "social work"], + "homepage" : "https://framagit.org/Chill-project/BudgetBundle", + "autoload": { + "psr-4": { "Chill\\AMLI\BudgetBundle\\": "" } + }, + "autoload-dev": { + "classmap": [ "Resources/test/Fixtures/App/app/AppKernel.php" ] + }, + "authors": [ + { + "name": "Champs-Libres", + "email": "info@champs-libres.coop" + } + ], + "require": { + "chill-project/person": "~1.5" + }, + "require-dev": { + "phpunit/phpunit": "~6" + }, + "extra": { + "app-migrations-dir": "Resources/test/Fixtures/App/app/DoctrineMigrations", + "symfony-app-dir": "Test/Fixtures/App/app/" + }, + "minimum-stability": "dev", + "prefer-stable": true +}