mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Merge branch '21-save-exports' into '111_exports_suite'
Allow to save exports See merge request Chill-Projet/chill-bundles!465
This commit is contained in:
commit
9dba5965f6
@ -11,23 +11,32 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\MainBundle\Controller;
|
namespace Chill\MainBundle\Controller;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\SavedExport;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
use Chill\MainBundle\Export\ExportManager;
|
use Chill\MainBundle\Export\ExportManager;
|
||||||
|
use Chill\MainBundle\Form\SavedExportType;
|
||||||
use Chill\MainBundle\Form\Type\Export\ExportType;
|
use Chill\MainBundle\Form\Type\Export\ExportType;
|
||||||
use Chill\MainBundle\Form\Type\Export\FormatterType;
|
use Chill\MainBundle\Form\Type\Export\FormatterType;
|
||||||
use Chill\MainBundle\Form\Type\Export\PickCenterType;
|
use Chill\MainBundle\Form\Type\Export\PickCenterType;
|
||||||
use Chill\MainBundle\Redis\ChillRedis;
|
use Chill\MainBundle\Redis\ChillRedis;
|
||||||
|
use Chill\MainBundle\Security\Authorization\SavedExportVoter;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use LogicException;
|
use LogicException;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
use RedisException;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||||
use Symfony\Component\Form\FormFactoryInterface;
|
use Symfony\Component\Form\FormFactoryInterface;
|
||||||
use Symfony\Component\Form\FormInterface;
|
use Symfony\Component\Form\FormInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
use function count;
|
use function count;
|
||||||
use function serialize;
|
use function serialize;
|
||||||
use function unserialize;
|
use function unserialize;
|
||||||
@ -38,35 +47,37 @@ use function unserialize;
|
|||||||
*/
|
*/
|
||||||
class ExportController extends AbstractController
|
class ExportController extends AbstractController
|
||||||
{
|
{
|
||||||
|
private EntityManagerInterface $entityManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var ExportManager
|
* @var ExportManager
|
||||||
*/
|
*/
|
||||||
protected $exportManager;
|
private $exportManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var FormFactoryInterface
|
* @var FormFactoryInterface
|
||||||
*/
|
*/
|
||||||
protected $formFactory;
|
private $formFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var LoggerInterface
|
* @var LoggerInterface
|
||||||
*/
|
*/
|
||||||
protected $logger;
|
private $logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var ChillRedis
|
* @var ChillRedis
|
||||||
*/
|
*/
|
||||||
protected $redis;
|
private $redis;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var SessionInterface
|
* @var SessionInterface
|
||||||
*/
|
*/
|
||||||
protected $session;
|
private $session;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var TranslatorInterface
|
* @var TranslatorInterface
|
||||||
*/
|
*/
|
||||||
protected $translator;
|
private $translator;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
ChillRedis $chillRedis,
|
ChillRedis $chillRedis,
|
||||||
@ -74,8 +85,10 @@ class ExportController extends AbstractController
|
|||||||
FormFactoryInterface $formFactory,
|
FormFactoryInterface $formFactory,
|
||||||
LoggerInterface $logger,
|
LoggerInterface $logger,
|
||||||
SessionInterface $session,
|
SessionInterface $session,
|
||||||
TranslatorInterface $translator
|
TranslatorInterface $translator,
|
||||||
|
EntityManagerInterface $entityManager
|
||||||
) {
|
) {
|
||||||
|
$this->entityManager = $entityManager;
|
||||||
$this->redis = $chillRedis;
|
$this->redis = $chillRedis;
|
||||||
$this->exportManager = $exportManager;
|
$this->exportManager = $exportManager;
|
||||||
$this->formFactory = $formFactory;
|
$this->formFactory = $formFactory;
|
||||||
@ -142,6 +155,29 @@ class ExportController extends AbstractController
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/{_locale}/exports/generate-from-saved/{id}", name="chill_main_export_generate_from_saved")
|
||||||
|
*
|
||||||
|
* @throws RedisException
|
||||||
|
*/
|
||||||
|
public function generateFromSavedExport(SavedExport $savedExport): RedirectResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted(SavedExportVoter::GENERATE, $savedExport);
|
||||||
|
|
||||||
|
$key = md5(uniqid((string) mt_rand(), false));
|
||||||
|
|
||||||
|
$this->redis->setEx($key, 3600, serialize($savedExport->getOptions()));
|
||||||
|
|
||||||
|
return $this->redirectToRoute(
|
||||||
|
'chill_main_export_download',
|
||||||
|
[
|
||||||
|
'alias' => $savedExport->getExportAlias(),
|
||||||
|
'key' => $key, 'prevent_save' => true,
|
||||||
|
'returnPath' => $this->generateUrl('chill_main_export_saved_list_my'),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the list of available exports.
|
* Render the list of available exports.
|
||||||
*/
|
*/
|
||||||
@ -203,6 +239,46 @@ class ExportController extends AbstractController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/{_locale}/export/save-from-key/{alias}/{key}", name="chill_main_export_save_from_key")
|
||||||
|
*/
|
||||||
|
public function saveFromKey(string $alias, string $key, Request $request): Response
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_USER');
|
||||||
|
$user = $this->getUser();
|
||||||
|
|
||||||
|
if (!$user instanceof User) {
|
||||||
|
throw new AccessDeniedHttpException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $this->rebuildRawData($key);
|
||||||
|
|
||||||
|
$savedExport = new SavedExport();
|
||||||
|
$savedExport
|
||||||
|
->setOptions($data)
|
||||||
|
->setExportAlias($alias)
|
||||||
|
->setUser($user);
|
||||||
|
|
||||||
|
$form = $this->createForm(SavedExportType::class, $savedExport);
|
||||||
|
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$this->entityManager->persist($savedExport);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return $this->redirectToRoute('chill_main_export_index');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render(
|
||||||
|
'@ChillMain/SavedExport/new.html.twig',
|
||||||
|
[
|
||||||
|
'form' => $form->createView(),
|
||||||
|
'saved_export' => $savedExport,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* create a form to show on different steps.
|
* create a form to show on different steps.
|
||||||
*
|
*
|
||||||
@ -418,28 +494,7 @@ class ExportController extends AbstractController
|
|||||||
|
|
||||||
protected function rebuildData($key)
|
protected function rebuildData($key)
|
||||||
{
|
{
|
||||||
if (null === $key) {
|
$rawData = $this->rebuildRawData($key);
|
||||||
throw $this->createNotFoundException('key does not exists');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->redis->exists($key) !== 1) {
|
|
||||||
$this->addFlash('error', $this->translator->trans('This report is not available any more'));
|
|
||||||
|
|
||||||
throw $this->createNotFoundException('key does not exists');
|
|
||||||
}
|
|
||||||
|
|
||||||
$serialized = $this->redis->get($key);
|
|
||||||
|
|
||||||
if (false === $serialized) {
|
|
||||||
throw new LogicException('the key could not be reached from redis');
|
|
||||||
}
|
|
||||||
|
|
||||||
$rawData = unserialize($serialized);
|
|
||||||
|
|
||||||
$this->logger->notice('[export] choices for an export unserialized', [
|
|
||||||
'key' => $key,
|
|
||||||
'rawData' => json_encode($rawData),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$alias = $rawData['alias'];
|
$alias = $rawData['alias'];
|
||||||
|
|
||||||
@ -585,4 +640,32 @@ class ExportController extends AbstractController
|
|||||||
throw new LogicException("the step {$step} is not defined.");
|
throw new LogicException("the step {$step} is not defined.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function rebuildRawData(string $key): array
|
||||||
|
{
|
||||||
|
if (null === $key) {
|
||||||
|
throw $this->createNotFoundException('key does not exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->redis->exists($key) !== 1) {
|
||||||
|
$this->addFlash('error', $this->translator->trans('This report is not available any more'));
|
||||||
|
|
||||||
|
throw $this->createNotFoundException('key does not exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
$serialized = $this->redis->get($key);
|
||||||
|
|
||||||
|
if (false === $serialized) {
|
||||||
|
throw new LogicException('the key could not be reached from redis');
|
||||||
|
}
|
||||||
|
|
||||||
|
$rawData = unserialize($serialized);
|
||||||
|
|
||||||
|
$this->logger->notice('[export] choices for an export unserialized', [
|
||||||
|
'key' => $key,
|
||||||
|
'rawData' => json_encode($rawData),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $rawData;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
185
src/Bundle/ChillMainBundle/Controller/SavedExportController.php
Normal file
185
src/Bundle/ChillMainBundle/Controller/SavedExportController.php
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Controller;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\SavedExport;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Export\ExportInterface;
|
||||||
|
use Chill\MainBundle\Export\ExportManager;
|
||||||
|
use Chill\MainBundle\Export\GroupedExportInterface;
|
||||||
|
use Chill\MainBundle\Form\SavedExportType;
|
||||||
|
use Chill\MainBundle\Repository\SavedExportRepositoryInterface;
|
||||||
|
use Chill\MainBundle\Security\Authorization\SavedExportVoter;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||||
|
use Symfony\Component\Form\FormFactoryInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
use Symfony\Component\Templating\EngineInterface;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
use function count;
|
||||||
|
|
||||||
|
class SavedExportController
|
||||||
|
{
|
||||||
|
private EntityManagerInterface $entityManager;
|
||||||
|
|
||||||
|
private ExportManager $exportManager;
|
||||||
|
|
||||||
|
private FormFactoryInterface $formFactory;
|
||||||
|
|
||||||
|
private SavedExportRepositoryInterface $savedExportRepository;
|
||||||
|
|
||||||
|
private Security $security;
|
||||||
|
|
||||||
|
private SessionInterface $session;
|
||||||
|
|
||||||
|
private EngineInterface $templating;
|
||||||
|
|
||||||
|
private TranslatorInterface $translator;
|
||||||
|
|
||||||
|
private UrlGeneratorInterface $urlGenerator;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
EngineInterface $templating,
|
||||||
|
EntityManagerInterface $entityManager,
|
||||||
|
ExportManager $exportManager,
|
||||||
|
FormFactoryInterface $formBuilder,
|
||||||
|
SavedExportRepositoryInterface $savedExportRepository,
|
||||||
|
Security $security,
|
||||||
|
SessionInterface $session,
|
||||||
|
TranslatorInterface $translator,
|
||||||
|
UrlGeneratorInterface $urlGenerator
|
||||||
|
) {
|
||||||
|
$this->exportManager = $exportManager;
|
||||||
|
$this->entityManager = $entityManager;
|
||||||
|
$this->formFactory = $formBuilder;
|
||||||
|
$this->savedExportRepository = $savedExportRepository;
|
||||||
|
$this->security = $security;
|
||||||
|
$this->session = $session;
|
||||||
|
$this->templating = $templating;
|
||||||
|
$this->translator = $translator;
|
||||||
|
$this->urlGenerator = $urlGenerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/{_locale}/exports/saved/{id}/delete", name="chill_main_export_saved_delete")
|
||||||
|
*/
|
||||||
|
public function delete(SavedExport $savedExport, Request $request): Response
|
||||||
|
{
|
||||||
|
if (!$this->security->isGranted(SavedExportVoter::DELETE, $savedExport)) {
|
||||||
|
throw new AccessDeniedHttpException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = $this->formFactory->create();
|
||||||
|
$form->add('submit', SubmitType::class, ['label' => 'Delete']);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$this->entityManager->remove($savedExport);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->session->getFlashBag()->add('success', $this->translator->trans('saved_export.Export is deleted'));
|
||||||
|
|
||||||
|
return new RedirectResponse(
|
||||||
|
$this->urlGenerator->generate('chill_main_export_saved_list_my')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
$this->templating->render(
|
||||||
|
'@ChillMain/SavedExport/delete.html.twig',
|
||||||
|
[
|
||||||
|
'saved_export' => $savedExport,
|
||||||
|
'delete_form' => $form->createView(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/{_locale}/exports/saved/{id}/edit", name="chill_main_export_saved_edit")
|
||||||
|
*/
|
||||||
|
public function edit(SavedExport $savedExport, Request $request): Response
|
||||||
|
{
|
||||||
|
if (!$this->security->isGranted(SavedExportVoter::EDIT, $savedExport)) {
|
||||||
|
throw new AccessDeniedHttpException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = $this->formFactory->create(SavedExportType::class, $savedExport);
|
||||||
|
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->session->getFlashBag()->add('success', $this->translator->trans('saved_export.Saved export is saved!'));
|
||||||
|
|
||||||
|
return new RedirectResponse(
|
||||||
|
$this->urlGenerator->generate('chill_main_export_saved_list_my')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
$this->templating->render(
|
||||||
|
'@ChillMain/SavedExport/edit.html.twig',
|
||||||
|
[
|
||||||
|
'form' => $form->createView(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/{_locale}/exports/saved/my", name="chill_main_export_saved_list_my")
|
||||||
|
*/
|
||||||
|
public function list(): Response
|
||||||
|
{
|
||||||
|
$user = $this->security->getUser();
|
||||||
|
|
||||||
|
if (!$this->security->isGranted('ROLE_USER') || !$user instanceof User) {
|
||||||
|
throw new AccessDeniedHttpException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$exports = $this->savedExportRepository->findByUser($user, ['title' => 'ASC']);
|
||||||
|
|
||||||
|
// group by center
|
||||||
|
/** @var array<string, array{saved: SavedExport, export: ExportInterface}> $exportsGrouped */
|
||||||
|
$exportsGrouped = [];
|
||||||
|
|
||||||
|
foreach ($exports as $savedExport) {
|
||||||
|
$export = $this->exportManager->getExport($savedExport->getExportAlias());
|
||||||
|
|
||||||
|
$exportsGrouped[
|
||||||
|
$export instanceof GroupedExportInterface
|
||||||
|
? $this->translator->trans($export->getGroup()) : '_'
|
||||||
|
][] = ['saved' => $savedExport, 'export' => $export];
|
||||||
|
}
|
||||||
|
|
||||||
|
ksort($exportsGrouped);
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
$this->templating->render(
|
||||||
|
'@ChillMain/SavedExport/index.html.twig',
|
||||||
|
[
|
||||||
|
'grouped_exports' => $exportsGrouped,
|
||||||
|
'total' => count($exports),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
133
src/Bundle/ChillMainBundle/Entity/SavedExport.php
Normal file
133
src/Bundle/ChillMainBundle/Entity/SavedExport.php
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Entity;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||||
|
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
|
||||||
|
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
||||||
|
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Ramsey\Uuid\Uuid;
|
||||||
|
use Ramsey\Uuid\UuidInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Entity
|
||||||
|
* @ORM\Table(name="chill_main_saved_export")
|
||||||
|
*/
|
||||||
|
class SavedExport implements TrackCreationInterface, TrackUpdateInterface
|
||||||
|
{
|
||||||
|
use TrackCreationTrait;
|
||||||
|
|
||||||
|
use TrackUpdateTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Column(type="text", nullable=false, options={"default": ""})
|
||||||
|
*/
|
||||||
|
private string $description = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Column(type="text", nullable=false, options={"default": ""})
|
||||||
|
*/
|
||||||
|
private string $exportAlias;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Id
|
||||||
|
* @ORM\Column(name="id", type="uuid", unique="true")
|
||||||
|
* @ORM\GeneratedValue(strategy="NONE")
|
||||||
|
*/
|
||||||
|
private UuidInterface $id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Column(type="json", nullable=false, options={"default": "[]"})
|
||||||
|
*/
|
||||||
|
private array $options = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Column(type="text", nullable=false, options={"default": ""})
|
||||||
|
*/
|
||||||
|
private string $title = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\ManyToOne(targetEntity=User::class)
|
||||||
|
*/
|
||||||
|
private User $user;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->id = Uuid::uuid4();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return $this->description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getExportAlias(): string
|
||||||
|
{
|
||||||
|
return $this->exportAlias;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): UuidInterface
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOptions(): array
|
||||||
|
{
|
||||||
|
return $this->options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return $this->title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUser(): User
|
||||||
|
{
|
||||||
|
return $this->user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDescription(string $description): SavedExport
|
||||||
|
{
|
||||||
|
$this->description = $description;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setExportAlias(string $exportAlias): SavedExport
|
||||||
|
{
|
||||||
|
$this->exportAlias = $exportAlias;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setOptions(array $options): SavedExport
|
||||||
|
{
|
||||||
|
$this->options = $options;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTitle(string $title): SavedExport
|
||||||
|
{
|
||||||
|
$this->title = $title;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUser(User $user): SavedExport
|
||||||
|
{
|
||||||
|
$this->user = $user;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Form\DataMapper;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||||
|
use Symfony\Component\Form\DataMapperInterface;
|
||||||
|
use Symfony\Component\Form\Exception;
|
||||||
|
|
||||||
|
class RollingDateDataMapper implements DataMapperInterface
|
||||||
|
{
|
||||||
|
public function mapDataToForms($viewData, $forms)
|
||||||
|
{
|
||||||
|
if (null === $viewData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$viewData instanceof RollingDate) {
|
||||||
|
throw new Exception\UnexpectedTypeException($viewData, RollingDate::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
$forms = iterator_to_array($forms);
|
||||||
|
|
||||||
|
$forms['roll']->setData($viewData->getRoll());
|
||||||
|
$forms['fixedDate']->setData($viewData->getFixedDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mapFormsToData($forms, &$viewData): void
|
||||||
|
{
|
||||||
|
$forms = iterator_to_array($forms);
|
||||||
|
|
||||||
|
$viewData = new RollingDate(
|
||||||
|
$forms['roll']->getData(),
|
||||||
|
$forms['fixedDate']->getData()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
40
src/Bundle/ChillMainBundle/Form/SavedExportType.php
Normal file
40
src/Bundle/ChillMainBundle/Form/SavedExportType.php
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Form;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\SavedExport;
|
||||||
|
use Chill\MainBundle\Form\Type\ChillTextareaType;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
class SavedExportType extends AbstractType
|
||||||
|
{
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->add('title', TextType::class, [
|
||||||
|
'required' => true,
|
||||||
|
])
|
||||||
|
->add('description', ChillTextareaType::class, [
|
||||||
|
'required' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'class' => SavedExport::class,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
66
src/Bundle/ChillMainBundle/Form/Type/PickRollingDateType.php
Normal file
66
src/Bundle/ChillMainBundle/Form/Type/PickRollingDateType.php
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Form\Type;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Form\DataMapper\RollingDateDataMapper;
|
||||||
|
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
use Symfony\Component\Validator\Constraints\Callback;
|
||||||
|
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||||
|
|
||||||
|
class PickRollingDateType extends AbstractType
|
||||||
|
{
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->add('roll', ChoiceType::class, [
|
||||||
|
'choices' => array_combine(
|
||||||
|
array_map(static fn (string $item) => 'rolling_date.' . $item, RollingDate::ALL_T),
|
||||||
|
RollingDate::ALL_T
|
||||||
|
),
|
||||||
|
'multiple' => false,
|
||||||
|
'expanded' => false,
|
||||||
|
'label' => 'rolling_date.roll_movement',
|
||||||
|
])
|
||||||
|
->add('fixedDate', ChillDateType::class, [
|
||||||
|
'input' => 'datetime_immutable',
|
||||||
|
'label' => 'rolling_date.fixed_date_date',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$builder->setDataMapper(new RollingDateDataMapper());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'class' => RollingDate::class,
|
||||||
|
'empty_data' => new RollingDate(RollingDate::T_TODAY),
|
||||||
|
'constraints' => [
|
||||||
|
new Callback([$this, 'validate']),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validate($data, ExecutionContextInterface $context, $payload): void
|
||||||
|
{
|
||||||
|
/** @var RollingDate $data */
|
||||||
|
if (RollingDate::T_FIXED_DATE === $data->getRoll() && null === $data->getFixedDate()) {
|
||||||
|
$context
|
||||||
|
->buildViolation('rolling_date.When fixed date is selected, you must provide a date')
|
||||||
|
->atPath('roll')
|
||||||
|
->addViolation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Repository;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\SavedExport;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements ObjectRepository<SavedExport>
|
||||||
|
*/
|
||||||
|
class SavedExportRepository implements SavedExportRepositoryInterface
|
||||||
|
{
|
||||||
|
private EntityRepository $repository;
|
||||||
|
|
||||||
|
public function __construct(EntityManagerInterface $entityManager)
|
||||||
|
{
|
||||||
|
$this->repository = $entityManager->getRepository($this->getClassName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function find($id): ?SavedExport
|
||||||
|
{
|
||||||
|
return $this->repository->find($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array|SavedExport[]
|
||||||
|
*/
|
||||||
|
public function findAll(): array
|
||||||
|
{
|
||||||
|
return $this->repository->findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
|
||||||
|
{
|
||||||
|
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByUser(User $user, ?array $orderBy = [], ?int $limit = null, ?int $offset = null): array
|
||||||
|
{
|
||||||
|
$qb = $this->repository->createQueryBuilder('se');
|
||||||
|
|
||||||
|
$qb
|
||||||
|
->where($qb->expr()->eq('se.user', ':user'))
|
||||||
|
->setParameter('user', $user);
|
||||||
|
|
||||||
|
if (null !== $limit) {
|
||||||
|
$qb->setMaxResults($limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $offset) {
|
||||||
|
$qb->setFirstResult($offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($orderBy as $field => $order) {
|
||||||
|
$qb->addOrderBy('se.' . $field, $order);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $qb->getQuery()->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findOneBy(array $criteria): ?SavedExport
|
||||||
|
{
|
||||||
|
return $this->repository->findOneBy($criteria);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getClassName(): string
|
||||||
|
{
|
||||||
|
return SavedExport::class;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Repository;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\SavedExport;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements ObjectRepository<SavedExport>
|
||||||
|
*/
|
||||||
|
interface SavedExportRepositoryInterface extends ObjectRepository
|
||||||
|
{
|
||||||
|
public function find($id): ?SavedExport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array|SavedExport[]
|
||||||
|
*/
|
||||||
|
public function findAll(): array;
|
||||||
|
|
||||||
|
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array|SavedExport[]
|
||||||
|
*/
|
||||||
|
public function findByUser(User $user, ?array $orderBy = [], ?int $limit = null, ?int $offset = null): array;
|
||||||
|
|
||||||
|
public function findOneBy(array $criteria): ?SavedExport;
|
||||||
|
|
||||||
|
public function getClassName(): string;
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
<ul class="nav nav-pills justify-content-center">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ chill_path_forward_return_path('chill_main_export_index') }}" class="nav-link {% if current == 'common' %}active{% endif %}">
|
||||||
|
{{ 'Exports list'|trans }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ chill_path_forward_return_path('chill_main_export_saved_list_my') }}" class="nav-link {% if current == 'my' %}active{% endif %}">
|
||||||
|
{{ 'saved_export.My saved exports'|trans }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
@ -49,5 +49,14 @@ window.addEventListener("DOMContentLoaded", function(e) {
|
|||||||
data-download-text="{{ "Download your report"|trans|escape('html_attr') }}"
|
data-download-text="{{ "Download your report"|trans|escape('html_attr') }}"
|
||||||
><span id="waiting_text">{{ "Waiting for your report"|trans ~ '...' }}</span></div>
|
><span id="waiting_text">{{ "Waiting for your report"|trans ~ '...' }}</span></div>
|
||||||
|
|
||||||
</div>
|
<ul class="record_actions sticky-form-buttons">
|
||||||
|
<li class="cancel"><a href="{{ chill_return_path_or('chill_main_export_index') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a></li>
|
||||||
|
|
||||||
|
{% if not app.request.query.has('prevent_save') %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ chill_path_add_return_path('chill_main_export_save_from_key', { alias: alias, key: app.request.query.get('key')}) }}" class="btn btn-save">{{ 'Save'|trans }}</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
@ -22,8 +22,10 @@
|
|||||||
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
{{ include('@ChillMain/Export/_navbar.html.twig', {'current' : 'common'}) }}
|
||||||
|
|
||||||
<div class="col-md-10">
|
<div class="col-md-10">
|
||||||
<h1>{{ 'Exports list'|trans }}</h1>
|
|
||||||
|
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
|
|
||||||
@ -32,13 +34,17 @@
|
|||||||
<div class="row grouped">
|
<div class="row grouped">
|
||||||
{% for export_alias, export in exports %}
|
{% for export_alias, export in exports %}
|
||||||
<div class="col-6 col-md-4 mb-3">
|
<div class="col-6 col-md-4 mb-3">
|
||||||
<h2>{{ export.title|trans }}</h2>
|
<div class="card">
|
||||||
<p>{{ export.description|trans }}</p>
|
<div class="card-body">
|
||||||
<p>
|
<h2 class="card-title">{{ export.title|trans }}</h2>
|
||||||
<a class="btn btn-action" href="{{ path('chill_main_export_new', { 'alias': export_alias } ) }}">
|
<p class="card-text">{{ export.description|trans }}</p>
|
||||||
{{ 'Create an export'|trans }}
|
<p>
|
||||||
</a>
|
<a class="btn btn-action" href="{{ path('chill_main_export_new', { 'alias': export_alias } ) }}">
|
||||||
</p>
|
{{ 'Create an export'|trans }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
@ -52,13 +58,17 @@
|
|||||||
{% for export_alias,export in grouped_exports['_'] %}
|
{% for export_alias,export in grouped_exports['_'] %}
|
||||||
|
|
||||||
<div class="col-6 col-md-4 mb-3">
|
<div class="col-6 col-md-4 mb-3">
|
||||||
<h2>{{ export.title|trans }}</h2>
|
<div class="card">
|
||||||
<p>{{ export.description|trans }}</p>
|
<div class="card-body">
|
||||||
<p>
|
<h2 class="card-title">{{ export.title|trans }}</h2>
|
||||||
<a class="btn btn-action" href="{{ path('chill_main_export_new', { 'alias': export_alias } ) }}">
|
<p class="card-text">{{ export.description|trans }}</p>
|
||||||
{{ 'Create an export'|trans }}
|
<p>
|
||||||
</a>
|
<a class="btn btn-action" href="{{ path('chill_main_export_new', { 'alias': export_alias } ) }}">
|
||||||
</p>
|
{{ 'Create an export'|trans }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
{% extends '@ChillMain/layout.html.twig' %}
|
||||||
|
|
||||||
|
{% block title 'saved_export.Delete saved ?'|trans %}
|
||||||
|
|
||||||
|
{% block display_content %}
|
||||||
|
<div class="col-10">
|
||||||
|
<h3>{{ saved_export.title }}</h3>
|
||||||
|
<p>{{ saved_export.description|chill_markdown_to_html }}</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container chill-md-10">
|
||||||
|
{{ include('@ChillMain/Util/confirmation_template.html.twig',
|
||||||
|
{
|
||||||
|
'title' : 'saved_export.Delete saved ?'|trans,
|
||||||
|
'confirm_question' : 'saved_export.Are you sure you want to delete this saved ?'|trans,
|
||||||
|
'display_content' : block('display_content'),
|
||||||
|
'cancel_route' : 'chill_main_export_saved_list_my',
|
||||||
|
'cancel_parameters' : {},
|
||||||
|
'form' : delete_form
|
||||||
|
} ) }}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -0,0 +1,21 @@
|
|||||||
|
{% extends "@ChillMain/layout.html.twig" %}
|
||||||
|
|
||||||
|
{% block title %}{{ 'saved_export.Edit'|trans }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{{ block('title') }}</h1>
|
||||||
|
|
||||||
|
{{ form_start(form) }}
|
||||||
|
{{ form_row(form.title) }}
|
||||||
|
{{ form_row(form.description) }}
|
||||||
|
|
||||||
|
<ul class="record_actions sticky-form-buttons">
|
||||||
|
<li class="cancel">
|
||||||
|
<a href="{{ chill_return_path_or('chill_main_homepage') }}" class="btn btn-cancel">{{ 'Cancel'|trans }}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button type="submit" class="btn btn-save">{{ 'Save'|trans }}</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{{ form_end(form) }}
|
||||||
|
{% endblock %}
|
@ -0,0 +1,82 @@
|
|||||||
|
{% extends "@ChillMain/layout.html.twig" %}
|
||||||
|
|
||||||
|
{% block title %}{{ 'saved_export.My saved exports'|trans }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="col-md-10">
|
||||||
|
|
||||||
|
{{ include('@ChillMain/Export/_navbar.html.twig', {'current' : 'my'}) }}
|
||||||
|
|
||||||
|
<div class="container mt-4">
|
||||||
|
|
||||||
|
{% if total == 0 %}
|
||||||
|
<p class="chill-no-data-statement" >{{ 'saved_export.Any saved export'|trans }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for group, saveds in grouped_exports %}
|
||||||
|
{% if group != '_' %}
|
||||||
|
<h2 class="display-6">{{ group }}</h2>
|
||||||
|
<div class="row grouped">
|
||||||
|
{% for s in saveds %}
|
||||||
|
<div class="col-6 col-md-4 mb-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title">{{ s.saved.title }}</h2>
|
||||||
|
<p class="card-subtitle"><strong>{{ s.export.title|trans }}</strong></p>
|
||||||
|
|
||||||
|
<div class="card-text">
|
||||||
|
{{ s.saved.description|chill_markdown_to_html }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="createdBy">{{ 'saved_export.Created on %date%'|trans({'%date%': s.saved.createdAt|format_datetime('long', 'short')}) }}</div>
|
||||||
|
|
||||||
|
<ul class="record_actions">
|
||||||
|
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_delete', {'id': s.saved.id }) }}" class="btn btn-delete"></a></li>
|
||||||
|
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_edit', {'id': s.saved.id }) }}" class="btn btn-edit"></a></li>
|
||||||
|
<li><a href="{{ path('chill_main_export_generate_from_saved', { id: s.saved.id }) }}" class="btn btn-action"><i class="fa fa-cog"></i></a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if grouped_exports|keys|length > 1 and grouped_exports['_']|length > 0 %}
|
||||||
|
<h2 class="display-6">{{ 'Ungrouped exports'|trans }}</h2>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="row ungrouped">
|
||||||
|
{% for saveds in grouped_exports['_']|default([]) %}
|
||||||
|
{% for s in saveds %}
|
||||||
|
<div class="col-6 col-md-4 mb-3">
|
||||||
|
<div class="col-6 col-md-4 mb-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title">{{ s.saved.title }}</h2>
|
||||||
|
<p class="card-subtitle"><strong>{{ s.export.title|trans }}</strong></p>
|
||||||
|
|
||||||
|
<div class="card-text">
|
||||||
|
{{ s.saved.description|chill_markdown_to_html }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="createdBy">{{ 'saved_export.Created on %date%'|trans({'%date%': s.saved.createdAt|format_datetime('long', 'short')}) }}</div>
|
||||||
|
|
||||||
|
<ul class="record_actions">
|
||||||
|
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_delete', {'id': s.saved.id }) }}" class="btn btn-delete"></a></li>
|
||||||
|
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_edit', {'id': s.saved.id }) }}" class="btn btn-edit"></a></li>
|
||||||
|
<li><a href="{{ path('chill_main_export_generate_from_saved', { id: s.saved.id }) }}" class="btn btn-action"><i class="fa fa-cog"></i></a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -0,0 +1,21 @@
|
|||||||
|
{% extends "@ChillMain/layout.html.twig" %}
|
||||||
|
|
||||||
|
{% block title %}{{ 'saved_export.New'|trans }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{{ block('title') }}</h1>
|
||||||
|
|
||||||
|
{{ form_start(form) }}
|
||||||
|
{{ form_row(form.title) }}
|
||||||
|
{{ form_row(form.description) }}
|
||||||
|
|
||||||
|
<ul class="record_actions sticky-form-buttons">
|
||||||
|
<li class="cancel">
|
||||||
|
<a href="{{ chill_return_path_or('chill_main_homepage') }}" class="btn btn-cancel">{{ 'Cancel'|trans }}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button type="submit" class="btn btn-save">{{ 'Save'|trans }}</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{{ form_end(form) }}
|
||||||
|
{% endblock %}
|
@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Security\Authorization;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\SavedExport;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||||
|
use UnexpectedValueException;
|
||||||
|
use function in_array;
|
||||||
|
|
||||||
|
class SavedExportVoter extends Voter
|
||||||
|
{
|
||||||
|
public const DELETE = 'CHLL_MAIN_EXPORT_SAVED_DELETE';
|
||||||
|
|
||||||
|
public const EDIT = 'CHLL_MAIN_EXPORT_SAVED_EDIT';
|
||||||
|
|
||||||
|
public const GENERATE = 'CHLL_MAIN_EXPORT_SAVED_GENERATE';
|
||||||
|
|
||||||
|
private const ALL = [
|
||||||
|
self::DELETE,
|
||||||
|
self::EDIT,
|
||||||
|
self::GENERATE,
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function supports($attribute, $subject): bool
|
||||||
|
{
|
||||||
|
return $subject instanceof SavedExport && in_array($attribute, self::ALL, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
|
||||||
|
{
|
||||||
|
/** @var SavedExport $subject */
|
||||||
|
switch ($attribute) {
|
||||||
|
case self::DELETE:
|
||||||
|
case self::EDIT:
|
||||||
|
case self::GENERATE:
|
||||||
|
return $subject->getUser() === $token->getUser();
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new UnexpectedValueException('attribute not supported: ' . $attribute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
101
src/Bundle/ChillMainBundle/Service/RollingDate/RollingDate.php
Normal file
101
src/Bundle/ChillMainBundle/Service/RollingDate/RollingDate.php
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Service\RollingDate;
|
||||||
|
|
||||||
|
use DateTimeImmutable;
|
||||||
|
|
||||||
|
class RollingDate
|
||||||
|
{
|
||||||
|
public const ALL_T = [
|
||||||
|
self::T_YEAR_PREVIOUS_START,
|
||||||
|
self::T_QUARTER_PREVIOUS_START,
|
||||||
|
self::T_MONTH_PREVIOUS_START,
|
||||||
|
self::T_WEEK_PREVIOUS_START,
|
||||||
|
self::T_YEAR_CURRENT_START,
|
||||||
|
self::T_QUARTER_CURRENT_START,
|
||||||
|
self::T_MONTH_CURRENT_START,
|
||||||
|
self::T_WEEK_CURRENT_START,
|
||||||
|
self::T_TODAY,
|
||||||
|
self::T_WEEK_NEXT_START,
|
||||||
|
self::T_MONTH_NEXT_START,
|
||||||
|
self::T_QUARTER_NEXT_START,
|
||||||
|
self::T_YEAR_NEXT_START,
|
||||||
|
self::T_FIXED_DATE,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A given fixed date.
|
||||||
|
*/
|
||||||
|
public const T_FIXED_DATE = 'fixed_date';
|
||||||
|
|
||||||
|
public const T_MONTH_CURRENT_START = 'month_current_start';
|
||||||
|
|
||||||
|
public const T_MONTH_NEXT_START = 'month_next_start';
|
||||||
|
|
||||||
|
public const T_MONTH_PREVIOUS_START = 'month_previous_start';
|
||||||
|
|
||||||
|
public const T_QUARTER_CURRENT_START = 'quarter_current_start';
|
||||||
|
|
||||||
|
public const T_QUARTER_NEXT_START = 'quarter_next_start';
|
||||||
|
|
||||||
|
public const T_QUARTER_PREVIOUS_START = 'quarter_previous_start';
|
||||||
|
|
||||||
|
public const T_TODAY = 'today';
|
||||||
|
|
||||||
|
public const T_WEEK_CURRENT_START = 'week_current_start';
|
||||||
|
|
||||||
|
public const T_WEEK_NEXT_START = 'week_next_start';
|
||||||
|
|
||||||
|
public const T_WEEK_PREVIOUS_START = 'week_previous_start';
|
||||||
|
|
||||||
|
public const T_YEAR_CURRENT_START = 'year_current_start';
|
||||||
|
|
||||||
|
public const T_YEAR_NEXT_START = 'year_next_start';
|
||||||
|
|
||||||
|
public const T_YEAR_PREVIOUS_START = 'year_previous_start';
|
||||||
|
|
||||||
|
private ?DateTimeImmutable $fixedDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The pivot date is the date from the rolling is computed. By default, it is "now".
|
||||||
|
*/
|
||||||
|
private DateTimeImmutable $pivotDate;
|
||||||
|
|
||||||
|
private string $roll;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string|self::T_* $roll
|
||||||
|
* @param DateTimeImmutable|null $pivotDate Will be "now" if null is given
|
||||||
|
* @param DateTimeImmutable|null $fixedDate Only to insert if $roll equals @see{self::T_FIXED_DATE}
|
||||||
|
*/
|
||||||
|
public function __construct(string $roll, ?DateTimeImmutable $fixedDate = null, ?DateTimeImmutable $pivotDate = null)
|
||||||
|
{
|
||||||
|
$this->roll = $roll;
|
||||||
|
$this->pivotDate = $pivotDate ?? new DateTimeImmutable('now');
|
||||||
|
$this->fixedDate = $fixedDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFixedDate(): ?DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->fixedDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPivotDate(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->pivotDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRoll(): string
|
||||||
|
{
|
||||||
|
return $this->roll;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,142 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Service\RollingDate;
|
||||||
|
|
||||||
|
use DateInterval;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use LogicException;
|
||||||
|
use UnexpectedValueException;
|
||||||
|
|
||||||
|
class RollingDateConverter implements RollingDateConverterInterface
|
||||||
|
{
|
||||||
|
public function convert(RollingDate $rollingDate): DateTimeImmutable
|
||||||
|
{
|
||||||
|
switch ($rollingDate->getRoll()) {
|
||||||
|
case RollingDate::T_MONTH_CURRENT_START:
|
||||||
|
return $this->toBeginOfMonth($rollingDate->getPivotDate());
|
||||||
|
|
||||||
|
case RollingDate::T_MONTH_NEXT_START:
|
||||||
|
return $this->toBeginOfMonth($rollingDate->getPivotDate()->add(new DateInterval('P1M')));
|
||||||
|
|
||||||
|
case RollingDate::T_MONTH_PREVIOUS_START:
|
||||||
|
return $this->toBeginOfMonth($rollingDate->getPivotDate()->sub(new DateInterval('P1M')));
|
||||||
|
|
||||||
|
case RollingDate::T_QUARTER_CURRENT_START:
|
||||||
|
return $this->toBeginOfQuarter($rollingDate->getPivotDate());
|
||||||
|
|
||||||
|
case RollingDate::T_QUARTER_NEXT_START:
|
||||||
|
return $this->toBeginOfQuarter($rollingDate->getPivotDate()->add(new DateInterval('P3M')));
|
||||||
|
|
||||||
|
case RollingDate::T_QUARTER_PREVIOUS_START:
|
||||||
|
return $this->toBeginOfQuarter($rollingDate->getPivotDate()->sub(new DateInterval('P3M')));
|
||||||
|
|
||||||
|
case RollingDate::T_WEEK_CURRENT_START:
|
||||||
|
return $this->toBeginOfWeek($rollingDate->getPivotDate());
|
||||||
|
|
||||||
|
case RollingDate::T_WEEK_NEXT_START:
|
||||||
|
return $this->toBeginOfWeek($rollingDate->getPivotDate()->add(new DateInterval('P1W')));
|
||||||
|
|
||||||
|
case RollingDate::T_WEEK_PREVIOUS_START:
|
||||||
|
return $this->toBeginOfWeek($rollingDate->getPivotDate()->sub(new DateInterval('P1W')));
|
||||||
|
|
||||||
|
case RollingDate::T_YEAR_CURRENT_START:
|
||||||
|
return $this->toBeginOfYear($rollingDate->getPivotDate());
|
||||||
|
|
||||||
|
case RollingDate::T_YEAR_PREVIOUS_START:
|
||||||
|
return $this->toBeginOfYear($rollingDate->getPivotDate()->sub(new DateInterval('P1Y')));
|
||||||
|
|
||||||
|
case RollingDate::T_YEAR_NEXT_START:
|
||||||
|
return $this->toBeginOfYear($rollingDate->getPivotDate()->add(new DateInterval('P1Y')));
|
||||||
|
|
||||||
|
case RollingDate::T_TODAY:
|
||||||
|
return $rollingDate->getPivotDate();
|
||||||
|
|
||||||
|
case RollingDate::T_FIXED_DATE:
|
||||||
|
if (null === $rollingDate->getFixedDate()) {
|
||||||
|
throw new LogicException('You must provide a fixed date when selecting a fixed date');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rollingDate->getFixedDate();
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new UnexpectedValueException(sprintf('%s rolling operation not supported', $rollingDate->getRoll()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function toBeginOfMonth(DateTimeImmutable $date): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return DateTimeImmutable::createFromFormat(
|
||||||
|
'Y-m-d His',
|
||||||
|
sprintf('%s-%s-01 000000', $date->format('Y'), $date->format('m'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function toBeginOfQuarter(DateTimeImmutable $date): DateTimeImmutable
|
||||||
|
{
|
||||||
|
switch ((int) $date->format('n')) {
|
||||||
|
case 1:
|
||||||
|
case 2:
|
||||||
|
case 3:
|
||||||
|
$month = '01';
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
case 5:
|
||||||
|
case 6:
|
||||||
|
$month = '04';
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 7:
|
||||||
|
case 8:
|
||||||
|
case 9:
|
||||||
|
$month = '07';
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 10:
|
||||||
|
case 11:
|
||||||
|
case 12:
|
||||||
|
$month = '10';
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new LogicException('this month is not valid: ' . $date->format('n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return DateTimeImmutable::createFromFormat(
|
||||||
|
'Y-m-d His',
|
||||||
|
sprintf('%s-%s-01 000000', $date->format('Y'), $month)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function toBeginOfWeek(DateTimeImmutable $date): DateTimeImmutable
|
||||||
|
{
|
||||||
|
if (1 === $dayOfWeek = (int) $date->format('N')) {
|
||||||
|
return $date->setTime(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $date
|
||||||
|
->sub(new DateInterval('P' . ($dayOfWeek - 1) . 'D'))
|
||||||
|
->setTime(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function toBeginOfYear(DateTimeImmutable $date): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return DateTimeImmutable::createFromFormat(
|
||||||
|
'Y-m-d His',
|
||||||
|
sprintf('%s-01-01 000000', $date->format('Y'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Service\RollingDate;
|
||||||
|
|
||||||
|
use DateTimeImmutable;
|
||||||
|
|
||||||
|
interface RollingDateConverterInterface
|
||||||
|
{
|
||||||
|
public function convert(RollingDate $rollingDate): DateTimeImmutable;
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Form\Type;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Form\Type\PickRollingDateType;
|
||||||
|
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||||
|
use Symfony\Component\Form\PreloadedExtension;
|
||||||
|
use Symfony\Component\Form\Test\TypeTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
final class PickRollingDateTypeTest extends TypeTestCase
|
||||||
|
{
|
||||||
|
public function testSubmitValidData()
|
||||||
|
{
|
||||||
|
$formData = [
|
||||||
|
'roll' => 'year_previous_start',
|
||||||
|
'fixedDate' => null,
|
||||||
|
];
|
||||||
|
|
||||||
|
$form = $this->factory->create(PickRollingDateType::class);
|
||||||
|
|
||||||
|
$form->submit($formData);
|
||||||
|
|
||||||
|
$this->assertTrue($form->isSynchronized());
|
||||||
|
|
||||||
|
/** @var RollingDate $rollingDate */
|
||||||
|
$rollingDate = $form->getData();
|
||||||
|
|
||||||
|
$this->assertInstanceOf(RollingDate::class, $rollingDate);
|
||||||
|
$this->assertEquals(RollingDate::T_YEAR_PREVIOUS_START, $rollingDate->getRoll());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getExtensions(): array
|
||||||
|
{
|
||||||
|
$type = new PickRollingDateType();
|
||||||
|
|
||||||
|
return [
|
||||||
|
new PreloadedExtension([$type], []),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Services\RollingDate;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||||
|
use Chill\MainBundle\Service\RollingDate\RollingDateConverter;
|
||||||
|
use DateTime;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
final class RollingDateConverterTest extends TestCase
|
||||||
|
{
|
||||||
|
private RollingDateConverter $converter;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->converter = new RollingDateConverter();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generateDataConversionDate(): iterable
|
||||||
|
{
|
||||||
|
$format = 'Y-m-d His';
|
||||||
|
|
||||||
|
yield [RollingDate::T_MONTH_CURRENT_START, '2022-11-01 000000', $format];
|
||||||
|
|
||||||
|
yield [RollingDate::T_MONTH_NEXT_START, '2022-12-01 000000', $format];
|
||||||
|
|
||||||
|
yield [RollingDate::T_MONTH_PREVIOUS_START, '2022-10-01 000000', $format];
|
||||||
|
|
||||||
|
yield [RollingDate::T_QUARTER_CURRENT_START, '2022-10-01 000000', $format];
|
||||||
|
|
||||||
|
yield [RollingDate::T_QUARTER_NEXT_START, '2023-01-01 000000', $format];
|
||||||
|
|
||||||
|
yield [RollingDate::T_QUARTER_PREVIOUS_START, '2022-07-01 000000', $format];
|
||||||
|
|
||||||
|
yield [RollingDate::T_TODAY, '2022-11-07 000000', $format];
|
||||||
|
|
||||||
|
yield [RollingDate::T_WEEK_CURRENT_START, '2022-11-07 000000', $format];
|
||||||
|
|
||||||
|
yield [RollingDate::T_WEEK_NEXT_START, '2022-11-14 000000', $format];
|
||||||
|
|
||||||
|
yield [RollingDate::T_WEEK_PREVIOUS_START, '2022-10-31 000000', $format];
|
||||||
|
|
||||||
|
yield [RollingDate::T_YEAR_CURRENT_START, '2022-01-01 000000', $format];
|
||||||
|
|
||||||
|
yield [RollingDate::T_YEAR_NEXT_START, '2023-01-01 000000', $format];
|
||||||
|
|
||||||
|
yield [RollingDate::T_YEAR_PREVIOUS_START, '2021-01-01 000000', $format];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testConversionFixedDate()
|
||||||
|
{
|
||||||
|
$rollingDate = new RollingDate(RollingDate::T_FIXED_DATE, new DateTimeImmutable('2022-01-01'));
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
'2022-01-01',
|
||||||
|
$this->converter->convert($rollingDate)->format('Y-m-d')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testConvertOnDateNow()
|
||||||
|
{
|
||||||
|
$rollingDate = new RollingDate(RollingDate::T_YEAR_PREVIOUS_START);
|
||||||
|
|
||||||
|
$actual = $this->converter->convert($rollingDate);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
(int) (new DateTimeImmutable('now'))->format('Y') - 1,
|
||||||
|
(int) $actual->format('Y')
|
||||||
|
);
|
||||||
|
$this->assertEquals(1, (int) $actual->format('m'));
|
||||||
|
$this->assertEquals(1, (int) $actual->format('d'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider generateDataConversionDate
|
||||||
|
*/
|
||||||
|
public function testConvertOnPivotDate(string $roll, string $expectedDateTime, string $format)
|
||||||
|
{
|
||||||
|
$pivot = DateTimeImmutable::createFromFormat('Y-m-d His', '2022-11-07 000000');
|
||||||
|
$rollingDate = new RollingDate($roll, null, $pivot);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
DateTime::createFromFormat($format, $expectedDateTime),
|
||||||
|
$this->converter->convert($rollingDate)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -105,3 +105,8 @@ services:
|
|||||||
resource: '../Service/Import/'
|
resource: '../Service/Import/'
|
||||||
autowire: true
|
autowire: true
|
||||||
autoconfigure: true
|
autoconfigure: true
|
||||||
|
|
||||||
|
Chill\MainBundle\Service\RollingDate\:
|
||||||
|
resource: '../Service/RollingDate/'
|
||||||
|
autowire: true
|
||||||
|
autoconfigure: true
|
||||||
|
@ -50,6 +50,8 @@ services:
|
|||||||
tags:
|
tags:
|
||||||
- { name: security.voter }
|
- { name: security.voter }
|
||||||
|
|
||||||
|
Chill\MainBundle\Security\Authorization\SavedExportVoter: ~
|
||||||
|
|
||||||
Chill\MainBundle\Security\PasswordRecover\TokenManager:
|
Chill\MainBundle\Security\PasswordRecover\TokenManager:
|
||||||
arguments:
|
arguments:
|
||||||
$secret: '%kernel.secret%'
|
$secret: '%kernel.secret%'
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\Migrations\Main;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20221107212201 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('DROP TABLE chill_main_saved_export');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Create table for saved exports';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('CREATE TABLE chill_main_saved_export (id UUID NOT NULL, user_id INT DEFAULT NULL, description TEXT DEFAULT \'\' NOT NULL, exportAlias TEXT DEFAULT \'\' NOT NULL, options JSONB DEFAULT \'[]\' NOT NULL, title TEXT DEFAULT \'\' NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, createdBy_id INT DEFAULT NULL, updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))');
|
||||||
|
$this->addSql('CREATE INDEX IDX_C2029B22A76ED395 ON chill_main_saved_export (user_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_C2029B223174800F ON chill_main_saved_export (createdBy_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_C2029B2265FF1AEC ON chill_main_saved_export (updatedBy_id)');
|
||||||
|
$this->addSql('COMMENT ON COLUMN chill_main_saved_export.id IS \'(DC2Type:uuid)\'');
|
||||||
|
$this->addSql('COMMENT ON COLUMN chill_main_saved_export.options IS \'(DC2Type:json)\'');
|
||||||
|
$this->addSql('COMMENT ON COLUMN chill_main_saved_export.createdAt IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
$this->addSql('COMMENT ON COLUMN chill_main_saved_export.updatedAt IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_saved_export ADD CONSTRAINT FK_C2029B22A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_saved_export ADD CONSTRAINT FK_C2029B223174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_saved_export ADD CONSTRAINT FK_C2029B2265FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
}
|
||||||
|
}
|
@ -538,3 +538,32 @@ export:
|
|||||||
isNoAddress: Adresse incomplète ?
|
isNoAddress: Adresse incomplète ?
|
||||||
_lat: Latitude
|
_lat: Latitude
|
||||||
_lon: Longitude
|
_lon: Longitude
|
||||||
|
|
||||||
|
rolling_date:
|
||||||
|
year_previous_start: Début de l'année précédente
|
||||||
|
quarter_previous_start: Début du trimestre précédent
|
||||||
|
month_previous_start: Début du mois précédent
|
||||||
|
week_previous_start: Début de la semaine précédente
|
||||||
|
year_current_start: Début de l'année courante
|
||||||
|
quarter_current_start: Début du trimestre courant
|
||||||
|
month_current_start: Début du mois courant
|
||||||
|
week_current_start: Début de la semaine courante
|
||||||
|
today: Aujourd'hui (aucune modification de la date courante)
|
||||||
|
year_next_start: Début de l'année suivante
|
||||||
|
quarter_next_start: Début du trimestre suivante
|
||||||
|
month_next_start: Début du mois suivant
|
||||||
|
week_next_start: Début de la semaine suivante
|
||||||
|
fixed_date: Date fixe
|
||||||
|
roll_movement: Modification par rapport à aujourd'hui
|
||||||
|
fixed_date_date: Date fixe
|
||||||
|
|
||||||
|
saved_export:
|
||||||
|
Any saved export: Aucun rapport enregistré
|
||||||
|
New: Nouveau rapport enregistré
|
||||||
|
Edit: Modifier un rapport enregistré
|
||||||
|
Delete saved ?: Supprimer un rapport enregistré ?
|
||||||
|
Are you sure you want to delete this saved ?: Êtes-vous sûr·e de vouloir supprimer ce rapport ?
|
||||||
|
My saved exports: Mes rapports enregistrés
|
||||||
|
Export is deleted: Le rapport est supprimé
|
||||||
|
Saved export is saved!: Le rapport est enregistré
|
||||||
|
Created on %date%: Créé le %date%
|
@ -12,10 +12,11 @@ declare(strict_types=1);
|
|||||||
namespace Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators;
|
namespace Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators;
|
||||||
|
|
||||||
use Chill\MainBundle\Export\AggregatorInterface;
|
use Chill\MainBundle\Export\AggregatorInterface;
|
||||||
use Chill\MainBundle\Form\Type\ChillDateType;
|
use Chill\MainBundle\Form\Type\PickRollingDateType;
|
||||||
|
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||||
|
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||||
use Chill\PersonBundle\Export\Declarations;
|
use Chill\PersonBundle\Export\Declarations;
|
||||||
use DateTime;
|
|
||||||
use Doctrine\ORM\QueryBuilder;
|
use Doctrine\ORM\QueryBuilder;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
@ -27,11 +28,15 @@ final class StepAggregator implements AggregatorInterface
|
|||||||
|
|
||||||
private const P = 'acp_step_agg_date';
|
private const P = 'acp_step_agg_date';
|
||||||
|
|
||||||
|
private RollingDateConverterInterface $rollingDateConverter;
|
||||||
|
|
||||||
private TranslatorInterface $translator;
|
private TranslatorInterface $translator;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
RollingDateConverterInterface $rollingDateConverter,
|
||||||
TranslatorInterface $translator
|
TranslatorInterface $translator
|
||||||
) {
|
) {
|
||||||
|
$this->rollingDateConverter = $rollingDateConverter;
|
||||||
$this->translator = $translator;
|
$this->translator = $translator;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,7 +65,7 @@ final class StepAggregator implements AggregatorInterface
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
->setParameter(self::P, $data['on_date'])
|
->setParameter(self::P, $this->rollingDateConverter->convert($data['on_date']))
|
||||||
->addGroupBy('step_aggregator');
|
->addGroupBy('step_aggregator');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,8 +76,8 @@ final class StepAggregator implements AggregatorInterface
|
|||||||
|
|
||||||
public function buildForm(FormBuilderInterface $builder)
|
public function buildForm(FormBuilderInterface $builder)
|
||||||
{
|
{
|
||||||
$builder->add('on_date', ChillDateType::class, [
|
$builder->add('on_date', PickRollingDateType::class, [
|
||||||
'data' => new DateTime(),
|
'data' => new RollingDate(RollingDate::T_TODAY),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,15 +12,23 @@ declare(strict_types=1);
|
|||||||
namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters;
|
namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters;
|
||||||
|
|
||||||
use Chill\MainBundle\Export\FilterInterface;
|
use Chill\MainBundle\Export\FilterInterface;
|
||||||
use Chill\MainBundle\Form\Type\ChillDateType;
|
use Chill\MainBundle\Form\Type\PickRollingDateType;
|
||||||
|
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||||
|
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||||
use Chill\PersonBundle\Export\Declarations;
|
use Chill\PersonBundle\Export\Declarations;
|
||||||
use DateTime;
|
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\QueryBuilder;
|
use Doctrine\ORM\QueryBuilder;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
|
||||||
class OpenBetweenDatesFilter implements FilterInterface
|
class OpenBetweenDatesFilter implements FilterInterface
|
||||||
{
|
{
|
||||||
|
private RollingDateConverterInterface $rollingDateConverter;
|
||||||
|
|
||||||
|
public function __construct(RollingDateConverterInterface $rollingDateConverter)
|
||||||
|
{
|
||||||
|
$this->rollingDateConverter = $rollingDateConverter;
|
||||||
|
}
|
||||||
|
|
||||||
public function addRole(): ?string
|
public function addRole(): ?string
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
@ -34,8 +42,8 @@ class OpenBetweenDatesFilter implements FilterInterface
|
|||||||
);
|
);
|
||||||
|
|
||||||
$qb->andWhere($clause);
|
$qb->andWhere($clause);
|
||||||
$qb->setParameter('datefrom', $data['date_from'], Types::DATE_MUTABLE);
|
$qb->setParameter('datefrom', $this->rollingDateConverter->convert($data['date_from']), Types::DATE_IMMUTABLE);
|
||||||
$qb->setParameter('dateto', $data['date_to'], Types::DATE_MUTABLE);
|
$qb->setParameter('dateto', $this->rollingDateConverter->convert($data['date_to']), Types::DATE_IMMUTABLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function applyOn(): string
|
public function applyOn(): string
|
||||||
@ -46,19 +54,19 @@ class OpenBetweenDatesFilter implements FilterInterface
|
|||||||
public function buildForm(FormBuilderInterface $builder)
|
public function buildForm(FormBuilderInterface $builder)
|
||||||
{
|
{
|
||||||
$builder
|
$builder
|
||||||
->add('date_from', ChillDateType::class, [
|
->add('date_from', PickRollingDateType::class, [
|
||||||
'data' => new DateTime(),
|
'data' => new RollingDate(RollingDate::T_MONTH_PREVIOUS_START),
|
||||||
])
|
])
|
||||||
->add('date_to', ChillDateType::class, [
|
->add('date_to', PickRollingDateType::class, [
|
||||||
'data' => new DateTime(),
|
'data' => new RollingDate(RollingDate::T_TODAY),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function describeAction($data, $format = 'string'): array
|
public function describeAction($data, $format = 'string'): array
|
||||||
{
|
{
|
||||||
return ['Filtered by opening dates: between %datefrom% and %dateto%', [
|
return ['Filtered by opening dates: between %datefrom% and %dateto%', [
|
||||||
'%datefrom%' => $data['date_from']->format('d-m-Y'),
|
'%datefrom%' => $this->rollingDateConverter->convert($data['date_from'])->format('d-m-Y'),
|
||||||
'%dateto%' => $data['date_to']->format('d-m-Y'),
|
'%dateto%' => $this->rollingDateConverter->convert($data['date_to'])->format('d-m-Y'),
|
||||||
]];
|
]];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,31 +12,40 @@ declare(strict_types=1);
|
|||||||
namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters;
|
namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters;
|
||||||
|
|
||||||
use Chill\MainBundle\Export\FilterInterface;
|
use Chill\MainBundle\Export\FilterInterface;
|
||||||
|
use Chill\MainBundle\Form\Type\PickRollingDateType;
|
||||||
|
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||||
use Chill\PersonBundle\Export\Declarations;
|
use Chill\PersonBundle\Export\Declarations;
|
||||||
use Doctrine\ORM\Query\Expr\Andx;
|
|
||||||
use Doctrine\ORM\QueryBuilder;
|
use Doctrine\ORM\QueryBuilder;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
use function in_array;
|
||||||
|
|
||||||
class StepFilter implements FilterInterface
|
class StepFilter implements FilterInterface
|
||||||
{
|
{
|
||||||
|
private const A = 'acp_filter_bystep_stephistories';
|
||||||
|
|
||||||
private const DEFAULT_CHOICE = AccompanyingPeriod::STEP_CONFIRMED;
|
private const DEFAULT_CHOICE = AccompanyingPeriod::STEP_CONFIRMED;
|
||||||
|
|
||||||
|
private const P = 'acp_step_filter_date';
|
||||||
|
|
||||||
private const STEPS = [
|
private const STEPS = [
|
||||||
'Draft' => AccompanyingPeriod::STEP_DRAFT,
|
'Draft' => AccompanyingPeriod::STEP_DRAFT,
|
||||||
'Confirmed' => AccompanyingPeriod::STEP_CONFIRMED,
|
'Confirmed' => AccompanyingPeriod::STEP_CONFIRMED,
|
||||||
'Closed' => AccompanyingPeriod::STEP_CLOSED,
|
'Closed' => AccompanyingPeriod::STEP_CLOSED,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private RollingDateConverterInterface $rollingDateConverter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var TranslatorInterface
|
* @var TranslatorInterface
|
||||||
*/
|
*/
|
||||||
protected $translator;
|
private $translator;
|
||||||
|
|
||||||
public function __construct(TranslatorInterface $translator)
|
public function __construct(RollingDateConverterInterface $rollingDateConverter, TranslatorInterface $translator)
|
||||||
{
|
{
|
||||||
|
$this->rollingDateConverter = $rollingDateConverter;
|
||||||
$this->translator = $translator;
|
$this->translator = $translator;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,17 +56,25 @@ class StepFilter implements FilterInterface
|
|||||||
|
|
||||||
public function alterQuery(QueryBuilder $qb, $data)
|
public function alterQuery(QueryBuilder $qb, $data)
|
||||||
{
|
{
|
||||||
$where = $qb->getDQLPart('where');
|
if (!in_array(self::A, $qb->getAllAliases(), true)) {
|
||||||
$clause = $qb->expr()->eq('acp.step', ':step');
|
$qb->leftJoin('acp.stepHistories', self::A);
|
||||||
|
|
||||||
if ($where instanceof Andx) {
|
|
||||||
$where->add($clause);
|
|
||||||
} else {
|
|
||||||
$where = $qb->expr()->andX($clause);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$qb->add('where', $where);
|
$qb
|
||||||
$qb->setParameter('step', $data['accepted_steps']);
|
->andWhere(
|
||||||
|
$qb->expr()->andX(
|
||||||
|
$qb->expr()->lte(self::A . '.startDate', ':' . self::P),
|
||||||
|
$qb->expr()->orX(
|
||||||
|
$qb->expr()->isNull(self::A . '.endDate'),
|
||||||
|
$qb->expr()->lt(self::A . '.endDate', ':' . self::P)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
->andWhere(
|
||||||
|
$qb->expr()->in(self::A . '.step', ':acp_filter_by_step_steps')
|
||||||
|
)
|
||||||
|
->setParameter(self::P, $this->rollingDateConverter->convert($data['calc_date']))
|
||||||
|
->setParameter('acp_filter_by_step_steps', $data['accepted_steps']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function applyOn()
|
public function applyOn()
|
||||||
@ -67,13 +84,17 @@ class StepFilter implements FilterInterface
|
|||||||
|
|
||||||
public function buildForm(FormBuilderInterface $builder)
|
public function buildForm(FormBuilderInterface $builder)
|
||||||
{
|
{
|
||||||
$builder->add('accepted_steps', ChoiceType::class, [
|
$builder
|
||||||
'choices' => self::STEPS,
|
->add('accepted_steps', ChoiceType::class, [
|
||||||
'multiple' => false,
|
'choices' => self::STEPS,
|
||||||
'expanded' => true,
|
'multiple' => false,
|
||||||
'empty_data' => self::DEFAULT_CHOICE,
|
'expanded' => true,
|
||||||
'data' => self::DEFAULT_CHOICE,
|
'empty_data' => self::DEFAULT_CHOICE,
|
||||||
]);
|
'data' => self::DEFAULT_CHOICE,
|
||||||
|
])
|
||||||
|
->add('calc_date', PickRollingDateType::class, [
|
||||||
|
'label' => 'export.acp.filter.by_step.date_calc',
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function describeAction($data, $format = 'string')
|
public function describeAction($data, $format = 'string')
|
||||||
|
Loading…
x
Reference in New Issue
Block a user