diff --git a/app/AdminModule/ConfigurationModule/Forms/GroupFormFactory.php b/app/AdminModule/ConfigurationModule/Forms/GroupFormFactory.php index dc7f7681c..87bb5cda0 100644 --- a/app/AdminModule/ConfigurationModule/Forms/GroupFormFactory.php +++ b/app/AdminModule/ConfigurationModule/Forms/GroupFormFactory.php @@ -44,7 +44,7 @@ public function __construct( public function create(): Form { $form = $this->baseFormFactory->create(); - + $renderer = $form->getRenderer(); assert($renderer instanceof Bs4FormRenderer); $renderer->wrappers['control']['container'] = 'div class="col-7"'; @@ -60,8 +60,6 @@ public function create(): Form $groupFillTerm->addRule(Form::FILLED, 'admin.configuration.group_fill_term_empty'); $form->addComponent($groupFillTerm, 'groupFillTerm'); - - $form->addSubmit('submit', 'admin.common.save'); $form->setDefaults([ diff --git a/app/AdminModule/GroupsModule/Components/GroupsGridControl.php b/app/AdminModule/GroupsModule/Components/GroupsGridControl.php index 0ca28d3f3..e1f3dd58b 100644 --- a/app/AdminModule/GroupsModule/Components/GroupsGridControl.php +++ b/app/AdminModule/GroupsModule/Components/GroupsGridControl.php @@ -6,18 +6,15 @@ use App\Model\Group\Commands\RemoveGroup; use App\Model\Group\Commands\SaveGroup; -use App\Model\Group\Repositories\GroupRepository; use App\Model\Group\Group; +use App\Model\Group\Repositories\GroupRepository; +use App\Model\User\Repositories\UserRepository; use App\Services\CommandBus; -use App\Services\ExcelExportService; -use Exception; use Nette\Application\AbortException; use Nette\Application\UI\Control; use Nette\Application\UI\Form; use Nette\Forms\Container; use Nette\Forms\Controls\TextInput; -use Nette\Http\Session; -use Nette\Http\SessionSection; use Nette\Localization\Translator; use stdClass; use Ublaboo\DataGrid\DataGrid; @@ -30,14 +27,11 @@ */ class GroupsGridControl extends Control { - private SessionSection $sessionSection; - public function __construct( private CommandBus $commandBus, private Translator $translator, private GroupRepository $groupRepository, - private ExcelExportService $excelExportService, - private Session $session, + private readonly UserRepository $userRepository, ) { $this->sessionSection = $session->getSection('srs'); } @@ -64,12 +58,31 @@ public function createComponentGroupsGrid(string $name): void $grid->setDefaultSort(['name' => 'ASC']); $grid->setPagination(false); + $grid->addColumnText('name', 'admin.groups.group.column.name'); + + $grid->addColumnText('group_status', 'admin.groups.group.column.group_status'); - $grid->addColumnText('name', 'admin.program.groups.column.name'); + $grid->addColumnText('leader_id', 'admin.groups.group.column.leader_name') + ->setRenderer(function (Group $row) { + $user = $this->userRepository->findById($row->getLeaderId()); - $grid->addColumnText('leader_email', 'admin.program.groups.column.name'); - $grid->addColumnText('places', 'admin.program.groups.column.name'); - $grid->addColumnText('price', 'admin.program.groups.column.name'); + return $user->getFirstName() . ' ' . $user->getLastName(); + }) +/* + ->setRenderer(static fn (Group $row) => Html::el('span') + ->setText( + $user = $this->userRepository->findById((int) $id); + $this->userService->setApproved($user, (bool) $approved); + $row->getEmail(); + ) + ) +*/ + ->setFilterText(); + + $grid->addColumnText('leader_email', 'admin.groups.group.column.leader_email'); + $grid->addColumnText('places', 'admin.groups.group.column.places'); + + $grid->addColumnText('price', 'admin.groups.group.column.price'); $grid->addInlineAdd()->setPositionTop()->onControlAdd[] = function (Container $container): void { $container->addText('name', '') @@ -93,7 +106,7 @@ public function createComponentGroupsGrid(string $name): void $grid->getInlineEdit()->onSetDefaults[] = function (Container $container, Group $item): void { $nameText = $container['name']; assert($nameText instanceof TextInput); - $nameText->addRule(Form::IS_NOT_IN, 'admin.program.groups.column.name_exists', $this->groupRepository->findOthersNames($item->getId())); + $nameText->addRule(Form::IS_NOT_IN, 'admin.program.groups.column.name_exists', $this->groupRepository->findAll()); $container->setDefaults([ 'name' => $item->getName(), @@ -161,6 +174,4 @@ public function handleDelete(int $id): void $p->flashMessage('admin.program.groups.message.delete_success', 'success'); $p->redirect('this'); } - - } diff --git a/app/AdminModule/GroupsModule/Components/StatusGridControl.php b/app/AdminModule/GroupsModule/Components/StatusGridControl.php index 1e5bd3bc3..9b340fc90 100644 --- a/app/AdminModule/GroupsModule/Components/StatusGridControl.php +++ b/app/AdminModule/GroupsModule/Components/StatusGridControl.php @@ -5,11 +5,10 @@ namespace App\AdminModule\GroupsModule\Components; use App\Model\Acl\Repositories\RoleRepository; -use App\Model\Acl\Role; -use App\Model\Group\Status; use App\Model\Group\Commands\RemoveStatus; use App\Model\Group\Commands\SaveStatus; use App\Model\Group\Repositories\StatusRepository; +use App\Model\Group\Status; use App\Services\AclService; use App\Services\CommandBus; use Nette\Application\AbortException; @@ -30,7 +29,7 @@ */ class StatusGridControl extends Control { - public function __construct(private CommandBus $commandBus, private Translator $translator, private StatusRepository $statusRepository, private RoleRepository $roleRepository, private AclService $aclService) + public function __construct(private CommandBus $commandBus, private Translator $translator, private StatusRepository $statusRepository) { } @@ -62,14 +61,12 @@ public function createComponentStatusGrid(string $name): void $container->addText('name', '') ->addRule(Form::FILLED, 'admin.groups.status.column.name_empty') ->addRule(Form::IS_NOT_IN, 'admin.groups.status.column.name_exists', $this->statusRepository->findAllNames()); - }; $grid->getInlineAdd()->onSubmit[] = [$this, 'add']; $grid->addInlineEdit()->onControlAdd[] = static function (Container $container): void { $container->addText('name', '') ->addRule(Form::FILLED, 'admin.groups.status.column.name_empty'); - }; $grid->getInlineEdit()->onSetDefaults[] = function (Container $container, Status $item): void { $nameText = $container['name']; @@ -97,8 +94,7 @@ public function createComponentStatusGrid(string $name): void */ public function add(stdClass $values): void { - $status = new Status($values->name); - + $status = new Status(); $this->commandBus->handle(new SaveStatus($status)); @@ -136,12 +132,8 @@ public function handleDelete(int $id): void $p = $this->getPresenter(); - if ($status->getBlocks()->isEmpty()) { - $this->commandBus->handle(new RemoveStatus($status)); - $p->flashMessage('admin.groups.status.message.delete_success', 'success'); - } else { - $p->flashMessage('admin.groups.status.message.delete_failed', 'danger'); - } + $this->commandBus->handle(new RemoveStatus($status)); + $p->flashMessage('admin.groups.status.message.delete_success', 'success'); $p->redirect('this'); } diff --git a/app/AdminModule/GroupsModule/Presenters/GroupsPresenter.php b/app/AdminModule/GroupsModule/Presenters/GroupsPresenter.php index d1e233206..cd72f22f1 100644 --- a/app/AdminModule/GroupsModule/Presenters/GroupsPresenter.php +++ b/app/AdminModule/GroupsModule/Presenters/GroupsPresenter.php @@ -4,8 +4,8 @@ namespace App\AdminModule\GroupsModule\Presenters; -use App\AdminModule\GroupsModule\Components\IGroupsGridControlFactory; use App\AdminModule\GroupsModule\Components\GroupsGridControl; +use App\AdminModule\GroupsModule\Components\IGroupsGridControlFactory; use App\Model\Acl\Permission; use App\Model\Group\Repositories\GroupRepository; use Nette\Application\AbortException; @@ -22,7 +22,6 @@ class GroupsPresenter extends GroupsBasePresenter #[Inject] public IGroupsGridControlFactory $groupsGridControlFactory; - /** @throws AbortException */ public function startup(): void { diff --git a/app/AdminModule/Presenters/templates/Dashboard/default.latte b/app/AdminModule/Presenters/templates/Dashboard/default.latte index 824a60db1..3dedffe49 100644 --- a/app/AdminModule/Presenters/templates/Dashboard/default.latte +++ b/app/AdminModule/Presenters/templates/Dashboard/default.latte @@ -206,6 +206,11 @@
{_admin.configuration.menu.skautis} +
+ +
{_admin.configuration.menu.group} +
+

{_admin.configuration.menu.web} diff --git a/app/Model/Acl/Role.php b/app/Model/Acl/Role.php index 9612ad1a3..de6450b5d 100644 --- a/app/Model/Acl/Role.php +++ b/app/Model/Acl/Role.php @@ -62,6 +62,21 @@ class Role */ public const ADMIN = 'admin'; + /** + * Role uživatele nepřihlášeného na seminář. + */ + public const GROUP_NONREGISTERED = 'group_nonregistered'; + + /** + * Role organizátora. + */ + public const GROUP_LEADER = 'group_leader'; + + /** + * Role administrátora. + */ + public const GROUP_MEMBER = 'group_member'; + /** * Role, která je uživateli nastavena při testování jiné role. */ @@ -77,6 +92,9 @@ class Role self::LECTOR, self::ORGANIZER, self::ADMIN, + self::GROUP_LEADER, + self::GROUP_MEMBER, + self::GROUP_NONREGISTERED, ]; #[ORM\Id] #[ORM\GeneratedValue] diff --git a/app/Model/Cms/ApplicationGroupContent.php b/app/Model/Cms/ApplicationGroupContent.php new file mode 100644 index 000000000..db9e1ee32 --- /dev/null +++ b/app/Model/Cms/ApplicationGroupContent.php @@ -0,0 +1,17 @@ + TextContent::class, 'document_content' => DocumentContent::class, 'application_content' => ApplicationContent::class, + 'application_group_content' => ApplicationGroupContent::class, + 'manage_group_content' => ManageGroupContent::class, 'html_content' => HtmlContent::class, 'faq_content' => FaqContent::class, 'news_content' => NewsContent::class, @@ -62,6 +64,16 @@ abstract class Content implements IContent */ public const APPLICATION = 'application'; + /** + * ApplicationContent. + */ + public const APPLICATION_GROUP = 'application_group'; + + /** + * ApplicationContent. + */ + public const MANAGE_GROUP = 'manage_group'; + /** * HtmlContent. */ @@ -156,6 +168,8 @@ abstract class Content implements IContent self::BLOCKS, self::CAPACITIES, self::ORGANIZER, + self::APPLICATION_GROUP, + self::MANAGE_GROUP, ]; /** @var string[] */ diff --git a/app/Model/Cms/ManageGroupContent.php b/app/Model/Cms/ManageGroupContent.php new file mode 100644 index 000000000..3992e7314 --- /dev/null +++ b/app/Model/Cms/ManageGroupContent.php @@ -0,0 +1,17 @@ +em->wrapInTransaction(function () use ($command): void { $status = $command->getStatus(); - $this->statusRepository->remove($status); }); } diff --git a/app/Model/Group/Commands/SaveStatus.php b/app/Model/Group/Commands/SaveStatus.php index 066cf3dba..6647fee70 100644 --- a/app/Model/Group/Commands/SaveStatus.php +++ b/app/Model/Group/Commands/SaveStatus.php @@ -16,5 +16,4 @@ public function getStatus(): Status { return $this->status; } - } diff --git a/app/Model/Group/Group.php b/app/Model/Group/Group.php index 190aad342..81748716f 100644 --- a/app/Model/Group/Group.php +++ b/app/Model/Group/Group.php @@ -4,35 +4,17 @@ namespace App\Model\Group; -use App\Model\Acl\Permission; -use App\Model\Acl\Role; -use App\Model\Acl\SrsResource; -use App\Model\Application\Application; -use App\Model\Application\RolesApplication; -use App\Model\Application\SubeventsApplication; -use App\Model\CustomInput\CustomInput; -use App\Model\CustomInput\CustomInputValue; -use App\Model\Enums\ApplicationState; -use App\Model\Program\Block; -use App\Model\Program\Program; -use App\Model\Program\ProgramApplication; -use App\Model\Structure\Subevent; use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\Common\Collections\Collection; -use Doctrine\Common\Collections\Criteria; use Doctrine\ORM\Mapping as ORM; -use function implode; - /** * Entita uživatele. */ #[ORM\Entity] -#[ORM\Table(name: 'group')] +#[ORM\Table(name: 'user_group')] class Group { - #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(type: 'integer', nullable: false)] @@ -65,8 +47,8 @@ class Group /** * Status */ - #[ORM\Column(type: 'integer')] - protected int|null $groupStatusId = null; + #[ORM\Column(type: 'string')] + protected string|null $groupStatus = null; /** * Pocet mist @@ -86,6 +68,12 @@ class Group #[ORM\Column(type: 'text', nullable: true)] protected string|null $note = null; + /** + * Kod + */ + #[ORM\Column(type: 'string')] + protected string|null $code = null; + public function __construct() { $this->applications = new ArrayCollection(); @@ -141,16 +129,16 @@ public function setCreateDate(DateTimeImmutable|null $createDate): void $this->createDate = $createDate; } - public function getGroupStatusId(): int|null + public function getGroupStatus(): string|null { - return $this->groupStatusId; + return $this->groupStatus; } - public function setGroupStatusId(int|null $groupStatusId): void + public function setGroupStatus(string|null $groupStatus): void { - $this->groupStatusId = $groupStatusId; + $this->groupStatus = $groupStatus; } - + public function getPlaces(): string|null { return $this->places; @@ -170,7 +158,7 @@ public function setPrice(int|null $price): void { $this->price = $price; } - + public function getNote(): string|null { return $this->note; @@ -181,6 +169,13 @@ public function setNote(string|null $note): void $this->note = $note; } - + public function getCode(): string|null + { + return $this->code; + } + public function setCode(string|null $code): void + { + $this->code = $code; + } } diff --git a/app/Model/Group/Repositories/GroupRepository.php b/app/Model/Group/Repositories/GroupRepository.php index d442a29bf..21ca58c3c 100644 --- a/app/Model/Group/Repositories/GroupRepository.php +++ b/app/Model/Group/Repositories/GroupRepository.php @@ -4,21 +4,17 @@ namespace App\Model\Group\Repositories; -use App\Model\Acl\Role; use App\Model\Enums\ApplicationState; +use App\Model\Group\Group; use App\Model\Infrastructure\Repositories\AbstractRepository; use App\Model\Program\Block; use App\Model\Program\Program; -use App\Model\Group\Group; -use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Criteria; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\QueryBuilder; -use function count; - /** * Třída spravující uživatele. */ @@ -45,6 +41,22 @@ public function findById(int|null $id): Group|null return $this->getRepository()->findOneBy(['id' => $id]); } + /** + * Vrací uživatele podle id. + */ + public function findByCode(int|null $code): Group|null + { + return $this->getRepository()->findOneBy(['code' => $code]); + } + + /** + * Vrací uživatele podle id. + */ + public function findByLeaderId(int|null $leaderId): Group|null + { + return $this->getRepository()->findOneBy(['leaderId' => $leaderId]); + } + /** * Vrací uživatele podle id. * @@ -101,7 +113,6 @@ public function findAllWithWaitingForPaymentApplication(): Collection return new ArrayCollection($result); } - /** * Vrací uživatele jako možnosti pro select. * @@ -123,7 +134,6 @@ public function getUsersOptions(): array return $options; } - /** * Uloží uživatele. */ @@ -134,19 +144,11 @@ public function save(Group $group): void } /** - * Odstraní externího uživatele. + * Odstraní skupinu. */ public function remove(Group $group): void { - foreach ($user->getCustomInputValues() as $customInputValue) { - $this->em->remove($customInputValue); - } - - foreach ($user->getApplications() as $application) { - $this->em->remove($application); - } - - $this->em->remove($user); + $this->em->remove($group); $this->em->flush(); } diff --git a/app/Model/Group/Repositories/StatusRepository.php b/app/Model/Group/Repositories/StatusRepository.php index 04ac710dd..422376e17 100644 --- a/app/Model/Group/Repositories/StatusRepository.php +++ b/app/Model/Group/Repositories/StatusRepository.php @@ -4,8 +4,8 @@ namespace App\Model\Group\Repositories; -use App\Model\Infrastructure\Repositories\AbstractRepository; use App\Model\Group\Status; +use App\Model\Infrastructure\Repositories\AbstractRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\EntityManagerInterface; diff --git a/app/Model/Group/Status.php b/app/Model/Group/Status.php index 3a4d79245..5543f08b4 100644 --- a/app/Model/Group/Status.php +++ b/app/Model/Group/Status.php @@ -4,13 +4,8 @@ namespace App\Model\Group; -use App\Model\Acl\Role; -use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; -use function implode; - /** * Entita kategorie programového bloku. */ @@ -29,7 +24,6 @@ class Status #[ORM\Column(type: 'string', unique: true)] protected string $name; - public function getId(): int|null { return $this->id; @@ -44,6 +38,4 @@ public function setName(string $name): void { $this->name = $name; } - - } diff --git a/app/Model/Settings/Settings.php b/app/Model/Settings/Settings.php index b2a5d7f54..492746b29 100644 --- a/app/Model/Settings/Settings.php +++ b/app/Model/Settings/Settings.php @@ -218,17 +218,17 @@ class Settings * Skupina - minimalni pocet clenu */ public const GROUP_MIN_MEMBERS = 'group_min_members'; - + /** * Skupina - maximalni pocet clenu */ public const GROUP_MAX_MEMBERS = 'group_max_members'; - + /** * Skupina - termin pro naplneni skupiny */ public const GROUP_FILL_TERM = 'group_fill_term'; - + /** * Název položky nastavení. */ diff --git a/app/Model/User/Repositories/UserRepository.php b/app/Model/User/Repositories/UserRepository.php index cc7049aec..139e283fc 100644 --- a/app/Model/User/Repositories/UserRepository.php +++ b/app/Model/User/Repositories/UserRepository.php @@ -98,6 +98,20 @@ public function findAllApprovedInRole(Role $role): array ->getQuery()->execute(); } + /** + * Vrací schválené uživatele v roli. + * + * @return User[] + */ + public function findAllInGroup(int $groupId): array + { + return $this->createQueryBuilder('u') + ->where('u.groupId = :groupId') + ->setParameter('groupId', $groupId) + ->orderBy('u.lastName') + ->getQuery()->execute(); + } + /** * Vrací schválené uživatele v rolích. * diff --git a/app/Model/User/User.php b/app/Model/User/User.php index 43af1a12a..927dab13d 100644 --- a/app/Model/User/User.php +++ b/app/Model/User/User.php @@ -300,6 +300,12 @@ class User #[ORM\Column(type: 'datetime_immutable', nullable: true)] protected DateTimeImmutable|null $photoUpdate = null; + /** + * Id skupiny. + */ + #[ORM\Column(name: 'group_id', type: 'integer', nullable: true)] + protected int|null $groupId = null; + public function __construct() { $this->applications = new ArrayCollection(); @@ -605,6 +611,16 @@ public function setPhone(string|null $phone): void $this->phone = $phone; } + public function getGroupId(): int|null + { + return $this->groupId; + } + + public function setGroupId(int|null $groupId): void + { + $this->groupId = $groupId; + } + /** * Vrátí adresu uživatele. */ diff --git a/app/Services/ApplicationGroupService.php b/app/Services/ApplicationGroupService.php new file mode 100644 index 000000000..21de2d7f6 --- /dev/null +++ b/app/Services/ApplicationGroupService.php @@ -0,0 +1,1133 @@ + $roles + * @param Collection $subevents + * + * @throws Throwable + */ + public function register( + User $user, + Collection $roles, + User $createdBy, + bool $approve = false, + ): void { + $rolesApplication = $this->createRolesApplication($user, $roles, $createdBy, $approve); + + $this->eventBus->handle(new ApplicationUpdatedEvent($user)); + $this->updateUserPaymentInfo($user); + + $applicatonMaturity = '-'; + $applicationFee = '0'; + $applicationVariableSymbol = '-'; + + if ($rolesApplication->getFee() > 0 && $subeventsApplication->getFee() > 0) { + if ($rolesApplication->getMaturityDate()) { + $applicatonMaturity = $rolesApplication->getMaturityDateText(); + } + + $applicationFee = $rolesApplication->getFee() . ', ' . $subeventsApplication->getFee(); + $applicationVariableSymbol = $rolesApplication->getVariableSymbolText() . ', ' . $subeventsApplication->getVariableSymbolText(); + } elseif ($rolesApplication->getFee() > 0) { + if ($rolesApplication->getMaturityDate()) { + $applicatonMaturity = $rolesApplication->getMaturityDateText(); + } + + $applicationFee = $rolesApplication->getFee(); + $applicationVariableSymbol = $rolesApplication->getVariableSymbolText(); + } elseif ($subeventsApplication->getFee() > 0) { + if ($subeventsApplication->getMaturityDate()) { + $applicatonMaturity = $subeventsApplication->getMaturityDateText(); + } + + $applicationFee = $subeventsApplication->getFee(); + $applicationVariableSymbol = $subeventsApplication->getVariableSymbolText(); + } + + $editRegistrationToText = $this->queryBus->handle( + new SettingDateValueAsTextQuery(Settings::EDIT_REGISTRATION_TO), + ); + + $this->commandBus->handle(new CreateTemplateMail( + new ArrayCollection( + [$user], + ), + null, + Template::REGISTRATION, + [ + TemplateVariable::SEMINAR_NAME => $this->queryBus->handle(new SettingStringValueQuery(Settings::SEMINAR_NAME)), + TemplateVariable::EDIT_REGISTRATION_TO => $editRegistrationToText ?? '-', + TemplateVariable::APPLICATION_MATURITY => $applicatonMaturity, + TemplateVariable::APPLICATION_FEE => $applicationFee, + TemplateVariable::APPLICATION_VARIABLE_SYMBOL => $applicationVariableSymbol, + TemplateVariable::BANK_ACCOUNT => $this->queryBus->handle( + new SettingStringValueQuery(Settings::ACCOUNT_NUMBER), + ), + ], + )); + } + + public function register_group( + User $user, + int $roleId, + User $createdBy, + Group $group, + bool $approve = false, + ): void { + $rolesApplication = $this->createRolesApplication($user, $roleId, $createdBy, $group, $approve); + + $this->eventBus->handle(new ApplicationUpdatedEvent($user)); + $this->updateUserPaymentInfo($user); + + $applicatonMaturity = '-'; + $applicationFee = '0'; + $applicationVariableSymbol = '-'; + + if ($rolesApplication->getMaturityDate()) { + $applicatonMaturity = $rolesApplication->getMaturityDateText(); + } + + $applicationFee = $rolesApplication->getFee(); + $applicationVariableSymbol = $rolesApplication->getVariableSymbolText(); + + $editRegistrationToText = $this->queryBus->handle( + new SettingDateValueAsTextQuery(Settings::EDIT_REGISTRATION_TO), + ); + + $this->commandBus->handle(new CreateTemplateMail( + new ArrayCollection( + [$user], + ), + null, + Template::REGISTRATION, + [ + TemplateVariable::SEMINAR_NAME => $this->queryBus->handle(new SettingStringValueQuery(Settings::SEMINAR_NAME)), + TemplateVariable::EDIT_REGISTRATION_TO => $editRegistrationToText ?? '-', + TemplateVariable::APPLICATION_MATURITY => $applicatonMaturity, + TemplateVariable::APPLICATION_FEE => $applicationFee, + TemplateVariable::APPLICATION_VARIABLE_SYMBOL => $applicationVariableSymbol, + TemplateVariable::BANK_ACCOUNT => $this->queryBus->handle( + new SettingStringValueQuery(Settings::ACCOUNT_NUMBER), + ), + ], + )); + } + + /** + * Změní role uživatele a provede historizaci přihlášky. + * + * @param Collection $roles + * + * @throws SettingsItemNotFoundException + * @throws Throwable + * @throws MailingMailCreationException + */ + public function updateRoles(User $user, Collection $roles, User|null $createdBy, bool $approve = false): void + { + $rolesOld = clone $user->getRoles(); + + if (Helpers::collectionsEquals($roles, $rolesOld)) { + return; + } + + $this->em->wrapInTransaction(function () use ($user, $roles, $createdBy, $approve, $rolesOld): void { + if ($rolesOld->contains($this->roleRepository->findBySystemName(Role::NONREGISTERED))) { + $this->createRolesApplication($user, $roles, $createdBy, $approve); + $this->createSubeventsApplication( + $user, + new ArrayCollection( + [$this->subeventRepository->findImplicit()], + ), + $createdBy, + ); + } else { + $this->incrementRolesOccupancy($roles); + + $user->setRoles($roles); + $this->userRepository->save($user); + + if ( + $roles->forAll( + static fn (int $key, Role $role) => $role->isApprovedAfterRegistration() + ) + ) { + $user->setApproved(true); + } elseif ( + ! $approve + && $roles->exists( + static fn (int $key, Role $role) => ! $role->isApprovedAfterRegistration() && ! $rolesOld->contains($role) + ) + ) { + $user->setApproved(false); + } + + foreach ($user->getNotCanceledApplications() as $application) { + if ($application instanceof RolesApplication) { + $newApplication = clone $application; + $newApplication->setRoles($roles); + $newApplication->setFee($this->countRolesFee($roles)); + $newApplication->setState($this->getApplicationState($newApplication)); + $newApplication->setCreatedBy($createdBy); + $newApplication->setValidFrom(new DateTimeImmutable()); + $this->applicationRepository->save($newApplication); + + $application->setValidTo(new DateTimeImmutable()); + $this->applicationRepository->save($application); + } else { + $fee = $this->countSubeventsFee($roles, $application->getSubevents()); + + if ($application->getFee() !== $fee) { + $newApplication = clone $application; + $newApplication->setFee($fee); + $newApplication->setState($this->getApplicationState($newApplication)); + $newApplication->setCreatedBy($createdBy); + $newApplication->setValidFrom(new DateTimeImmutable()); + $this->applicationRepository->save($newApplication); + + $application->setValidTo(new DateTimeImmutable()); + $this->applicationRepository->save($application); + } + } + } + + $this->userRepository->save($user); + + $this->decrementRolesOccupancy($user->getRolesApplication()->getRoles()); + } + + $this->eventBus->handle(new ApplicationUpdatedEvent($user)); + $this->updateUserPaymentInfo($user); + }); + + $this->commandBus->handle(new CreateTemplateMail( + new ArrayCollection( + [$user], + ), + null, + Template::ROLES_CHANGED, + [ + TemplateVariable::SEMINAR_NAME => $this->queryBus->handle(new SettingStringValueQuery(Settings::SEMINAR_NAME)), + TemplateVariable::USERS_ROLES => implode(', ', $roles->map(static fn (Role $role) => $role->getName())->toArray()), + ], + )); + } + + /** + * Zruší registraci uživatele na seminář a provede historizaci přihlášky. + * + * @throws SettingsItemNotFoundException + * @throws Throwable + * @throws MailingMailCreationException + */ + public function cancelRegistration(User $user, string $state, User|null $createdBy): void + { + $this->em->wrapInTransaction(function () use ($user, $state, $createdBy): void { + $user->setApproved(true); + $user->getRoles()->clear(); + $user->setRolesApplicationDate(null); + $user->addRole($this->roleRepository->findBySystemName(Role::NONREGISTERED)); + $this->userRepository->save($user); + + foreach ($user->getNotCanceledApplications() as $application) { + $newApplication = clone $application; + $newApplication->setState($state); + $newApplication->setCreatedBy($createdBy); + $newApplication->setValidFrom(new DateTimeImmutable()); + + if ($newApplication->getPayment() !== null) { + if ($newApplication->getPayment()->getPairedValidApplications()->count() === 1) { + $newApplication->getPayment()->setState(PaymentState::NOT_PAIRED_CANCELED); + } + + $newApplication->setPayment(null); + } + + $this->applicationRepository->save($newApplication); + + $application->setValidTo(new DateTimeImmutable()); + $this->applicationRepository->save($application); + + if ($application instanceof RolesApplication) { + $this->decrementRolesOccupancy($application->getRoles()); + } elseif ($application instanceof SubeventsApplication) { + $this->decrementSubeventsOccupancy($application->getSubevents()); + } + } + + $this->userRepository->save($user); + + $this->eventBus->handle(new ApplicationUpdatedEvent($user)); + $this->updateUserPaymentInfo($user); + }); + + if ($state === ApplicationState::CANCELED) { + $this->commandBus->handle(new CreateTemplateMail( + new ArrayCollection( + [$user], + ), + null, + Template::REGISTRATION_CANCELED, + [ + TemplateVariable::SEMINAR_NAME => $this->queryBus->handle( + new SettingStringValueQuery(Settings::SEMINAR_NAME), + ), + ], + )); + } elseif ($state === ApplicationState::CANCELED_NOT_PAID) { + $this->commandBus->handle(new CreateTemplateMail( + new ArrayCollection( + [$user], + ), + null, + Template::REGISTRATION_CANCELED_NOT_PAID, + [ + TemplateVariable::SEMINAR_NAME => $this->queryBus->handle( + new SettingStringValueQuery( + Settings::SEMINAR_NAME, + ), + ), + ], + )); + } + } + + /** + * Vytvoří novou přihlášku na podakce a provede její historizaci. + * + * @param Collection $subevents + * + * @throws Throwable + */ + public function addSubeventsApplication(User $user, Collection $subevents, User $createdBy): void + { + $this->em->wrapInTransaction(function () use ($user, $subevents, $createdBy): void { + $this->incrementSubeventsOccupancy($subevents); + + $this->createSubeventsApplication($user, $subevents, $createdBy); + + $this->eventBus->handle(new ApplicationUpdatedEvent($user)); + $this->updateUserPaymentInfo($user); + }); + + $this->commandBus->handle(new CreateTemplateMail( + new ArrayCollection( + [$user], + ), + null, + Template::SUBEVENTS_CHANGED, + [ + TemplateVariable::SEMINAR_NAME => $this->queryBus->handle( + new SettingStringValueQuery(Settings::SEMINAR_NAME), + ), + TemplateVariable::USERS_SUBEVENTS => $user->getSubeventsText(), + ], + )); + } + + /** + * Aktualizuje podakce přihlášky a provede její historizaci. + * + * @param Collection $subevents + * + * @throws SettingsItemNotFoundException + * @throws Throwable + * @throws MailingMailCreationException + */ + public function updateSubeventsApplication(SubeventsApplication $application, Collection $subevents, User $createdBy): void + { + if (! $application->isValid()) { + return; + } + + $subeventsOld = clone $application->getSubevents(); + + if (Helpers::collectionsEquals($subevents, $subeventsOld)) { + return; + } + + $this->em->wrapInTransaction(function () use ($application, $subevents, $createdBy): void { + $this->incrementSubeventsOccupancy($subevents); + + $user = $application->getUser(); + + $newApplication = clone $application; + $newApplication->setSubevents($subevents); + $newApplication->setFee($this->countSubeventsFee($user->getRoles(), $subevents)); + $newApplication->setState($this->getApplicationState($newApplication)); + $newApplication->setCreatedBy($createdBy); + $newApplication->setValidFrom(new DateTimeImmutable()); + $this->applicationRepository->save($newApplication); + + $application->setValidTo(new DateTimeImmutable()); + $this->applicationRepository->save($application); + + $this->eventBus->handle(new ApplicationUpdatedEvent($user)); + $this->updateUserPaymentInfo($user); + + $this->decrementSubeventsOccupancy($application->getSubevents()); + }); + + $this->commandBus->handle(new CreateTemplateMail( + new ArrayCollection( + [$application->getUser()], + ), + null, + Template::SUBEVENTS_CHANGED, + [ + TemplateVariable::SEMINAR_NAME => $this->queryBus->handle( + new SettingStringValueQuery(Settings::SEMINAR_NAME), + ), + TemplateVariable::USERS_SUBEVENTS => $application->getUser()->getSubeventsText(), + ], + )); + } + + /** + * Zruší přihlášku na podakce a provede její historizaci. + * + * @throws SettingsItemNotFoundException + * @throws Throwable + * @throws MailingMailCreationException + */ + public function cancelSubeventsApplication(SubeventsApplication $application, string $state, User|null $createdBy): void + { + if (! $application->isValid()) { + return; + } + + $this->em->wrapInTransaction(function () use ($application, $state, $createdBy): void { + $user = $application->getUser(); + + $newApplication = clone $application; + $newApplication->setState($state); + $newApplication->setCreatedBy($createdBy); + $newApplication->setValidFrom(new DateTimeImmutable()); + + if ($newApplication->getPayment() !== null) { + if ($newApplication->getPayment()->getPairedValidApplications()->count() === 1) { + $newApplication->getPayment()->setState(PaymentState::NOT_PAIRED_CANCELED); + } + + $newApplication->setPayment(null); + } + + $this->applicationRepository->save($newApplication); + + $application->setValidTo(new DateTimeImmutable()); + $this->applicationRepository->save($application); + + $this->eventBus->handle(new ApplicationUpdatedEvent($user)); + $this->updateUserPaymentInfo($user); + + $this->decrementSubeventsOccupancy($application->getSubevents()); + }); + + $this->commandBus->handle(new CreateTemplateMail( + new ArrayCollection( + [$application->getUser()], + ), + null, + Template::SUBEVENTS_CHANGED, + [ + TemplateVariable::SEMINAR_NAME => $this->queryBus->handle( + new SettingStringValueQuery(Settings::SEMINAR_NAME), + ), + TemplateVariable::USERS_SUBEVENTS => $application->getUser()->getSubeventsText(), + ], + )); + } + + /** + * Aktualizuje stav platby přihlášky a provede její historizaci. + * + * @throws Throwable + */ + public function updateApplicationPayment( + Application $application, + string|null $paymentMethod, + DateTimeImmutable|null $paymentDate, + DateTimeImmutable|null $maturityDate, + User|null $createdBy, + ): void { + $oldPaymentMethod = $application->getPaymentMethod(); + $oldPaymentDate = $application->getPaymentDate(); + $oldMaturityDate = $application->getMaturityDate(); + + // pokud neni zmena, nic se neprovede + if ($paymentMethod === $oldPaymentMethod && $paymentDate == $oldPaymentDate && $maturityDate == $oldMaturityDate) { + return; + } + + $this->em->wrapInTransaction(function () use ($application, $paymentMethod, $paymentDate, $maturityDate, $createdBy): void { + $user = $application->getUser(); + + $newApplication = clone $application; + $newApplication->setPaymentMethod($paymentMethod); + $newApplication->setPaymentDate($paymentDate); + $newApplication->setMaturityDate($maturityDate); + $newApplication->setState($this->getApplicationState($newApplication)); + $newApplication->setCreatedBy($createdBy); + $newApplication->setValidFrom(new DateTimeImmutable()); + $this->applicationRepository->save($newApplication); + + $application->setValidTo(new DateTimeImmutable()); + $this->applicationRepository->save($application); + + $this->eventBus->handle(new ApplicationUpdatedEvent($user)); + $this->updateUserPaymentInfo($user); + }); + + if ($paymentDate !== null && $oldPaymentDate === null) { + $this->commandBus->handle(new CreateTemplateMail( + new ArrayCollection( + [ + $application->getUser(), + ], + ), + null, + Template::PAYMENT_CONFIRMED, + [ + TemplateVariable::SEMINAR_NAME => $this->queryBus->handle( + new SettingStringValueQuery(Settings::SEMINAR_NAME), + ), + TemplateVariable::APPLICATION_SUBEVENTS => $application->getSubeventsText(), + ], + )); + } + } + + /** + * Vytvoří platbu a označí spárované přihlášky jako zaplacené. + * + * @throws Throwable + */ + public function createPayment( + DateTimeImmutable $date, + float $amount, + string|null $variableSymbol, + string|null $transactionId, + string|null $accountNumber, + string|null $accountName, + string|null $message, + User|null $createdBy = null, + ): void { + $this->em->wrapInTransaction(function () use ($date, $amount, $variableSymbol, $transactionId, $accountNumber, $accountName, $message, $createdBy): void { + $payment = new Payment(); + + $payment->setDate($date); + $payment->setAmount($amount); + $payment->setVariableSymbol($variableSymbol); + $payment->setTransactionId($transactionId); + $payment->setAccountNumber($accountNumber); + $payment->setAccountName($accountName); + $payment->setMessage($message); + + $pairedApplication = $this->applicationRepository->findValidByVariableSymbol($variableSymbol); + + if ($pairedApplication) { + if ( + $pairedApplication->getState() === ApplicationState::PAID || + $pairedApplication->getState() === ApplicationState::PAID_FREE + ) { + $payment->setState(PaymentState::NOT_PAIRED_PAID); + } elseif ( + $pairedApplication->getState() === ApplicationState::CANCELED || + $pairedApplication->getState() === ApplicationState::CANCELED_NOT_PAID + ) { + $payment->setState(PaymentState::NOT_PAIRED_CANCELED); + } elseif (abs($pairedApplication->getFee() - $amount) >= 0.01) { + $payment->setState(PaymentState::NOT_PAIRED_FEE); + } else { + $payment->setState(PaymentState::PAIRED_AUTO); + $pairedApplication->setPayment($payment); + $this->updateApplicationPayment($pairedApplication, PaymentType::BANK, $date, $pairedApplication->getMaturityDate(), $createdBy); + } + } else { + $payment->setState(PaymentState::NOT_PAIRED_VS); + } + + $this->paymentRepository->save($payment); + }); + } + + /** + * Vytvoří platbu a označí spárované přihlášky jako zaplacené (bez údajů z banky). + * + * @throws Throwable + */ + public function createPaymentManual( + DateTimeImmutable $date, + float $amount, + string $variableSymbol, + User $createdBy, + ): void { + $this->createPayment($date, $amount, $variableSymbol, null, null, null, null, $createdBy); + } + + /** + * Aktualizuje platbu a stav spárovaných přihlášek. + * + * @param Collection $pairedApplications + * + * @throws Throwable + */ + public function updatePayment( + Payment $payment, + DateTimeImmutable|null $date, + float|null $amount, + string|null $variableSymbol, + Collection $pairedApplications, + User $createdBy, + ): void { + $this->em->wrapInTransaction(function () use ($payment, $date, $amount, $variableSymbol, $pairedApplications, $createdBy): void { + if ($date !== null) { + $payment->setDate($date); + } + + if ($amount !== null) { + $payment->setAmount($amount); + } + + if ($variableSymbol !== null) { + $payment->setVariableSymbol($variableSymbol); + } + + $oldPairedApplications = clone $payment->getPairedValidApplications(); + $newPairedApplications = clone $pairedApplications; + + $pairedApplicationsModified = false; + + foreach ($oldPairedApplications as $pairedApplication) { + if (! $newPairedApplications->contains($pairedApplication)) { + $pairedApplication->setPayment(null); + $this->updateApplicationPayment($pairedApplication, null, null, $pairedApplication->getMaturityDate(), $createdBy); + $pairedApplicationsModified = true; + } + } + + foreach ($newPairedApplications as $pairedApplication) { + if (! $oldPairedApplications->contains($pairedApplication)) { + $pairedApplication->setPayment($payment); + $this->updateApplicationPayment($pairedApplication, PaymentType::BANK, $payment->getDate(), $pairedApplication->getMaturityDate(), $createdBy); + $pairedApplicationsModified = true; + } + } + + if ($pairedApplicationsModified) { + if ($pairedApplications->isEmpty()) { + $payment->setState(PaymentState::NOT_PAIRED); + } else { + $payment->setState(PaymentState::PAIRED_MANUAL); + } + } + + $this->paymentRepository->save($payment); + }); + } + + /** + * Odstraní platbu a označí spárované přihlášky jako nezaplacené. + * + * @throws Throwable + */ + public function removePayment(Payment $payment, User $createdBy): void + { + $this->em->wrapInTransaction(function () use ($payment, $createdBy): void { + foreach ($payment->getPairedValidApplications() as $pairedApplication) { + $this->updateApplicationPayment($pairedApplication, null, null, $pairedApplication->getMaturityDate(), $createdBy); + } + + $this->paymentRepository->remove($payment); + }); + } + + /** + * Vytvoří příjmový doklad a provede historizaci přihlášky. + */ + public function createIncomeProof(Application $application, User $createdBy): void + { + if ($application->getPaymentMethod() !== PaymentType::CASH) { + return; + } + + $this->em->wrapInTransaction(function () use ($application, $createdBy): void { + $incomeProof = new IncomeProof(); + $this->incomeProofRepository->save($incomeProof); + + $newApplication = clone $application; + $newApplication->setIncomeProof($incomeProof); + $newApplication->setCreatedBy($createdBy); + $newApplication->setValidFrom(new DateTimeImmutable()); + $this->applicationRepository->save($newApplication); + + $application->setValidTo(new DateTimeImmutable()); + $this->applicationRepository->save($application); + }); + } + + /** + * Vrací stav přihlášky jako text. + */ + public function getStateText(Application $application): string + { + $state = $this->translator->translate('common.application_state.' . $application->getState()); + + if ($application->getState() === ApplicationState::PAID) { + $state .= ' (' . $application->getPaymentDate()->format(Helpers::DATE_FORMAT) . ')'; + } + + return $state; + } + + /** + * Může uživatel upravovat role? + * + * @throws Throwable + */ + public function isAllowedEditRegistration(User $user): bool + { + return ! $user->isInRole($this->roleRepository->findBySystemName(Role::NONREGISTERED)) + && ! $user->hasPaidAnyApplication() + && $this->queryBus->handle( + new SettingDateValueQuery(Settings::EDIT_REGISTRATION_TO), + ) >= (new DateTimeImmutable()) + ->setTime(0, 0); + } + + /** + * Je uživateli povoleno upravit nebo zrušit přihlášku? + * + * @throws Throwable + */ + public function isAllowedEditApplication(Application $application): bool + { + return $application instanceof SubeventsApplication + && ! $application->isCanceled() + && $application->getState() !== ApplicationState::PAID + && $this->queryBus->handle( + new SettingDateValueQuery(Settings::EDIT_REGISTRATION_TO), + ) >= (new DateTimeImmutable()) + ->setTime(0, 0); + } + + /** + * Může uživatel dodatečně přidávat podakce? + * + * @throws Throwable + */ + public function isAllowedAddApplication(User $user): bool + { + return ! $user->isInRole( + $this->roleRepository->findBySystemName(Role::NONREGISTERED), + ) + && $user->hasPaidEveryApplication() + && $this->queryBus->handle( + new SettingBoolValueQuery(Settings::IS_ALLOWED_ADD_SUBEVENTS_AFTER_PAYMENT), + ) + && $this->queryBus->handle( + new SettingDateValueQuery(Settings::EDIT_REGISTRATION_TO), + ) >= (new DateTimeImmutable()) + ->setTime(0, 0); + } + + /** + * Může uživatel upravovat vlastní pole přihlášky? + * + * @throws Throwable + */ + public function isAllowedEditCustomInputs(): bool + { + return $this->queryBus->handle( + new SettingDateValueQuery(Settings::EDIT_CUSTOM_INPUTS_TO), + ) >= (new DateTimeImmutable()) + ->setTime(0, 0); + } + + /** + * @param Collection $roles + * + * @throws SettingsItemNotFoundException + * @throws OptimisticLockException + * @throws ReflectionException + * @throws Throwable + */ + private function createRolesApplication(User $user, $roleId, User $createdBy, $group, bool $approve = false): RolesApplication + { + if (! $user->isInRole($this->roleRepository->findBySystemName(Role::NONREGISTERED))) { + throw new InvalidArgumentException('User is already registered.'); + } + +// $this->incrementRolesOccupancy($roles); + + $user->setApproved(true); + +/* + if ( + ! $approve && $roles->exists( + static fn (int $key, Role $role) => ! $role->isApprovedAfterRegistration() + ) + ) { + $user->setApproved(false); + } +*/ +// $user->setRoles($roles); + $user->setRolesApplicationDate(new DateTimeImmutable()); + $this->userRepository->save($user); + + $groupUsersArr = $this->userRepository->findAllInGroup(intval($groupId)); + $groupUsersCount = count($groupUsersArr); + + $userRoleLeader = $this->roleRepository->findById(9); + $roleFee = $userRoleLeader->getFee(); + + $totalGroupFee = $groupUsersCount * $roleFee; +/* + if ($user->getRolesApplication() != null) { + throw new InvalidArgumentException('User is already registered.'); + } +*/ + $application = new RolesApplication($user); +// $application->setRoles($roles); + $application->setApplicationDate(new DateTimeImmutable()); + $application->setFee($totalGroupFee); + $application->setMaturityDate($this->countMaturityDate()); + $application->setState($this->getApplicationState($application)); + $application->setCreatedBy($createdBy); + $application->setValidFrom(new DateTimeImmutable()); + $application->setVariableSymbol($this->generateVariableSymbol()); + $this->applicationRepository->save($application); + + $application->setApplicationId($application->getId()); + $this->applicationRepository->save($application); + + return $application; + } + + /** + * @param Collection $subevents + * + * @throws SettingsItemNotFoundException + * @throws OptimisticLockException + * @throws ReflectionException + * @throws Throwable + */ + private function createSubeventsApplication( + User $user, + Collection $subevents, + User $createdBy, + ): SubeventsApplication { + $this->incrementSubeventsOccupancy($subevents); + + $application = new SubeventsApplication($user); + $application->setSubevents($subevents); + $application->setApplicationDate(new DateTimeImmutable()); + $application->setFee($this->countSubeventsFee($user->getRoles(), $subevents)); + $application->setMaturityDate($this->countMaturityDate()); + $application->setState($this->getApplicationState($application)); + $application->setCreatedBy($createdBy); + $application->setValidFrom(new DateTimeImmutable()); + $application->setVariableSymbol($this->generateVariableSymbol()); + $this->applicationRepository->save($application); + + $application->setApplicationId($application->getId()); + $this->applicationRepository->save($application); + + return $application; + } + + /** @throws Throwable */ + private function generateVariableSymbol(): VariableSymbol + { + $variableSymbolCode = $this->queryBus->handle(new SettingStringValueQuery(Settings::VARIABLE_SYMBOL_CODE)); + + $variableSymbol = new VariableSymbol(); + $this->variableSymbolRepository->save($variableSymbol); + + $variableSymbolText = $variableSymbolCode . str_pad(strval($variableSymbol->getId()), 6, '0', STR_PAD_LEFT); + + $variableSymbol->setVariableSymbol($variableSymbolText); + $this->variableSymbolRepository->save($variableSymbol); + + return $variableSymbol; + } + + /** + * Vypočítá datum splatnosti podle zvolené metody. + * + * @throws Throwable + */ + private function countMaturityDate(): DateTimeImmutable|null + { + switch ( + $this->queryBus->handle( + new SettingStringValueQuery(Settings::MATURITY_TYPE), + ) + ) { + case MaturityType::DATE: + return $this->queryBus->handle(new SettingDateValueQuery(Settings::MATURITY_DATE)); + + case MaturityType::DAYS: + return (new DateTimeImmutable())->modify('+' . $this->queryBus->handle(new SettingIntValueQuery(Settings::MATURITY_DAYS)) . ' days'); + + case MaturityType::WORK_DAYS: + $workDays = $this->queryBus->handle(new SettingIntValueQuery(Settings::MATURITY_WORK_DAYS)); + $date = new DateTimeImmutable(); + + for ($i = 0; $i < $workDays;) { + $date = $date->modify('+1 days'); + $holidays = Yasumi::create('CzechRepublic', (int) $date->format('Y')); + + if ($holidays->isWorkingDay($date)) { + $i++; + } + } + + return $date; + } + + return null; + } + + /** + * Vypočítá poplatek za role. + * + * @param Collection $roles + */ + private function countRolesFee(Collection $roles): int + { + $fee = 0; + + foreach ($roles as $role) { + if ($role->getFee() === 0) { + return 0; + } elseif ($role->getFee() > 0) { + $fee += $role->getFee(); + } + } + + return $fee; + } + + /** + * Vypočítá poplatek za podakce přihlášky. + * + * @param Collection $roles + * @param Collection $subevents + */ + private function countSubeventsFee(Collection $roles, Collection $subevents): int + { + $fee = 0; + + foreach ($roles as $role) { + if ($role->getFee() === 0) { + return 0; + } + } + + foreach ($roles as $role) { + if ($role->getFee() === null) { + foreach ($subevents as $subevent) { + $fee += $subevent->getFee(); + } + + break; + } + } + + $discount = $this->discountService->countDiscount($this->subeventRepository->findSubeventsIds($subevents)); + + return $fee - $discount; + } + + /** + * Určí stav přihlášky. + */ + private function getApplicationState(Application $application): string + { + if ($application->getState() === ApplicationState::CANCELED) { + return ApplicationState::CANCELED; + } + + if ($application->getState() === ApplicationState::CANCELED_NOT_PAID) { + return ApplicationState::CANCELED_NOT_PAID; + } + + if ($application->getFee() === 0) { + return ApplicationState::PAID_FREE; + } + + if ($application->getPaymentDate()) { + return ApplicationState::PAID; + } + + return ApplicationState::WAITING_FOR_PAYMENT; + } + + /** + * Zvýší obsazenost rolí. + * + * @param Collection $roles + */ + private function incrementRolesOccupancy(Collection $roles): void + { + foreach ($roles as $role) { + $this->roleRepository->incrementOccupancy($role); + $this->aclService->saveRole($role); + } + } + + /** + * Sníží obsazenost rolí. + * + * @param Collection $roles + */ + private function decrementRolesOccupancy(Collection $roles): void + { + foreach ($roles as $role) { + $this->roleRepository->decrementOccupancy($role); + $this->aclService->saveRole($role); + } + } + + /** + * Zvýší obsazenost podakcí. + * + * @param Collection $subevents + */ + private function incrementSubeventsOccupancy(Collection $subevents): void + { + foreach ($subevents as $subevent) { + $this->subeventRepository->incrementOccupancy($subevent); + $this->subeventRepository->save($subevent); + } + } + + /** + * Sníží obsazenost podakcí. + * + * @param Collection $subevents + */ + private function decrementSubeventsOccupancy(Collection $subevents): void + { + foreach ($subevents as $subevent) { + $this->subeventRepository->decrementOccupancy($subevent); + $this->subeventRepository->save($subevent); + } + } + + private function updateUserPaymentInfo(User $user): void + { + $fee = 0; + foreach ($user->getNotCanceledApplications() as $application) { + $fee += $application->getFee(); + } + + $user->setFee($fee); + + $feeRemaining = 0; + foreach ($user->getWaitingForPaymentApplications() as $application) { + $feeRemaining += $application->getFee(); + } + + $user->setFeeRemaining($feeRemaining); + + $user->setPaymentMethod($this->userService->getPaymentMethod($user)); + + $maxDate = null; + foreach ($user->getValidApplications() as $application) { + if ($maxDate < $application->getPaymentDate()) { + $maxDate = $application->getPaymentDate(); + } + } + + $user->setLastPaymentDate($maxDate); + + $this->userRepository->save($user); + } +} diff --git a/app/WebModule/Components/ApplicationGroupContentControl.php b/app/WebModule/Components/ApplicationGroupContentControl.php new file mode 100644 index 000000000..17f2087b0 --- /dev/null +++ b/app/WebModule/Components/ApplicationGroupContentControl.php @@ -0,0 +1,223 @@ +template; + $template->setFile(__DIR__ . '/templates/application_group_content.latte'); + + if ($content) { + $template->heading = $content->getHeading(); + } + + $presenter = $this->getPresenter(); + assert($presenter instanceof WebBasePresenter); + + $template->backlink = $presenter->getHttpRequest()->getUrl()->getPath(); + + $user = $presenter->getUser(); + $template->guestRole = $user->isInRole($this->roleRepository->findBySystemName(Role::GUEST)->getName()); + $template->testRole = Role::TEST; + + $explicitSubeventsExists = $this->subeventRepository->explicitSubeventsExists(); + + if ($user->isLoggedIn()) { + $dbUser = $presenter->getDbUser(); + $userHasFixedFeeRole = $dbUser->hasFixedFeeRole(); + $userGroupId = $dbUser->getGroupId(); + $group = $this->groupRepository->findById($userGroupId); + $groupLeaderId = $group->getLeaderId(); + $groupLeader = $this->userRepository->findById($groupLeaderId); + $groupLeaderName = $groupLeader->getFirstName() . ' ' . $groupLeader->getLastName(); + $groupUsersArr = $this->userRepository->findAllInGroup($userGroupId); + $groupUsersCount = count($groupUsersArr); + + $template->unapprovedRole = $user->isInRole($this->roleRepository->findBySystemName(Role::UNAPPROVED)->getName()); + $template->nonregisteredRole = $user->isInRole($this->roleRepository->findBySystemName(Role::NONREGISTERED)->getName()); + $template->groupLeaderRole = $user->isInRole($this->roleRepository->findBySystemName(Role::GROUP_LEADER)->getName()); + $template->groupMemberRole = $user->isInRole($this->roleRepository->findBySystemName(Role::GROUP_MEMBER)->getName()); + $template->noRegisterableRole = $this->roleRepository->findFilteredRoles(true, false, false)->isEmpty(); + $template->registrationStart = $this->roleRepository->getRegistrationStart(); + $template->registrationEnd = $this->roleRepository->getRegistrationEnd(); + $template->bankAccount = $this->queryBus->handle(new SettingStringValueQuery(Settings::ACCOUNT_NUMBER)); + $template->dbUser = $dbUser; + $template->group = $group; + $template->groupLeaderName = $groupLeaderName; + $template->groupUsersCount = $groupUsersCount; + $template->userHasFixedFeeRole = $userHasFixedFeeRole; + + $template->usersApplications = $explicitSubeventsExists && $userHasFixedFeeRole + ? $dbUser->getNotCanceledApplications() + : ($explicitSubeventsExists + ? $dbUser->getNotCanceledSubeventsApplications() + : $dbUser->getNotCanceledRolesApplications() + ); + } + + $template->explicitSubeventsExists = $explicitSubeventsExists; + + $template->render(); + } + + public function renderScripts(): void + { + $template = $this->template; + $template->setFile(__DIR__ . '/templates/application_group_content_scripts.latte'); + $template->render(); + } + + /** + * @throws SettingsItemNotFoundException + * @throws NonUniqueResultException + * @throws Throwable + */ + protected function createComponentApplicationGroupForm(): Form + { + $p = $this->getPresenter(); + assert($p instanceof WebBasePresenter); + + $form = $this->applicationGroupFormFactory->create($p->getDbUser()); + + $form->onSuccess[] = function (Form $form, stdClass $values) use ($p): void { + $p->flashMessage('web.application_content.register_successful', 'success'); + + $this->authenticator->updateRoles($p->getUser()); + + $p->redirect('this'); + }; + + $this->applicationGroupFormFactory->onSkautIsError[] = static function () use ($p): void { + $p->flashMessage('web.application_content.register_synchronization_failed', 'danger'); + }; + + return $form; + } + + protected function createComponentApplicationsGrid(): ApplicationsGridControl + { + return $this->applicationsGridControlFactory->create(); + } + + protected function createComponentGroupUsersGrid(): GroupUsersGridControl + { + return $this->groupUsersGridControlFactory->create(); + } + + public function handleRemoveUserFromGroup($id): void + { + $p = $this->getPresenter(); + + $removeUser = $this->userRepository->findById(intval($id)); + $userGroupId = $removeUser->setGroupId(0); + $this->userRepository->save($removeUser); + + if ($user->isInRole($this->roleRepository->findBySystemName(Role::GROUP_MEMBER)->getName())) { + $userRoleUnregistered = $this->roleRepository->findById(2); + $removeUser->addRole($userRoleUnregistered); + $userRoleMember = $this->roleRepository->findById(10); + $removeUser->removeRole($userRoleMember); + } + + $p->redirect('this'); + } + + public function handleRemoveGroup($id): void + { + $p = $this->getPresenter(); + + $groupUsersArr = $this->userRepository->findAllInGroup(intval($groupId)); + foreach ($groupUsersArr as $groupUser) { + $userGroupId = $groupUser->setGroupId(0); + $this->userRepository->save($removeUser); + + if ($user->isInRole($this->roleRepository->findBySystemName(Role::GROUP_MEMBER)->getName())) { + $userRoleUnregistered = $this->roleRepository->findById(2); + $groupUser->addRole($userRoleUnregistered); + $userRoleMember = $this->roleRepository->findById(10); + $groupUser->removeRole($userRoleMember); + } + + if ($user->isInRole($this->roleRepository->findBySystemName(Role::GROUP_LEADER)->getName())) { + $userRoleUnregistered = $this->roleRepository->findById(2); + $groupUser->addRole($userRoleUnregistered); + $userRoleLeader = $this->roleRepository->findById(9); + $groupUser->removeRole($userRoleLeader); + } + } + + $removeGroup = $this->groupRepository->findById(intval($id)); + $this->groupRepository->remove($group); + + $p->redirect('this'); + } + + public function handleCloseGroup($id): void + { + $p = $this->getPresenter(); + + $group = $this->groupRepository->findById(intval($id)); + +/* + $groupUsersArr = $this->userRepository->findAllInGroup(intval($groupId)); + $groupUsersCount = count($groupUsersArr); + + $userRoleLeader = $this->roleRepository->findById(9); + $roleFee = $userRoleLeader->getFee(); + + $totalGroupFee = $groupUsersCount*$roleFee; +*/ + $this->applicationGroupService->register_group($presenter->getDbUser(), 9, $presenter->getDbUser(), $group); + + $p->redirect('this'); + } +} diff --git a/app/WebModule/Components/ApplicationsGroupGridControl.php b/app/WebModule/Components/ApplicationsGroupGridControl.php new file mode 100644 index 000000000..0123ac27f --- /dev/null +++ b/app/WebModule/Components/ApplicationsGroupGridControl.php @@ -0,0 +1,341 @@ +template->setFile(__DIR__ . '/templates/applications_grid.latte'); + $this->template->render(); + } + + /** + * Vytvoří komponentu. + * + * @throws SettingsItemNotFoundException + * @throws NonUniqueResultException + * @throws Throwable + * @throws DataGridException + */ + public function createComponentApplicationsGrid(string $name): void + { + $presenter = $this->getPresenter(); + assert($presenter instanceof WebBasePresenter); + + $this->user = $presenter->getDbUser(); + + $explicitSubeventsExists = $this->subeventRepository->explicitSubeventsExists(); + $userHasFixedFeeRole = $this->user->hasFixedFeeRole(); + + $grid = new DataGrid($this, $name); + $grid->setTranslator($this->translator); + + if (! $explicitSubeventsExists) { + $qb = $this->rolesApplicationRepository; + } elseif (! $userHasFixedFeeRole) { + $qb = $this->subeventsApplicationRepository; + } else { + $qb = $this->applicationRepository; + } + + $qb = $qb->createQueryBuilder('a') + ->join('a.user', 'u') + ->where('u = :user') + ->andWhere('a.validTo IS NULL') + ->setParameter('user', $this->user) + ->orderBy('a.applicationId'); + + $grid->setDataSource($qb); + $grid->setPagination(false); + + $grid->addColumnDateTime('applicationDate', 'web.profile.seminar.applications.application_date') + ->setFormat(Helpers::DATETIME_FORMAT); + + if ($userHasFixedFeeRole) { + $grid->addColumnText('roles', 'web.profile.seminar.applications.roles', 'rolesText'); + } + + if ($explicitSubeventsExists) { + $grid->addColumnText('subevents', 'web.profile.seminar.applications.subevents', 'subeventsText'); + } + + $grid->addColumnNumber('fee', 'web.profile.seminar.applications.fee'); + + $grid->addColumnText('variable_symbol', 'web.profile.seminar.applications.variable_symbol', 'variableSymbolText'); + + $grid->addColumnDateTime('maturityDate', 'web.profile.seminar.applications.maturity_date') + ->setFormat(Helpers::DATE_FORMAT); + + $grid->addColumnText('state', 'web.profile.seminar.applications.state') + ->setRenderer(fn (Application $row) => $this->applicationService->getStateText($row)); + + if ($explicitSubeventsExists) { + if ($this->applicationService->isAllowedAddApplication($this->user)) { + $grid->addInlineAdd()->setPositionTop()->onControlAdd[] = function (Container $container): void { + $options = $this->subeventService->getSubeventsOptionsWithCapacity(true, true, true, false, $this->user); + $container->addMultiSelect('subevents', '', $options) + ->setHtmlAttribute('class', 'datagrid-multiselect') + ->addRule(Form::FILLED, 'web.profile.seminar.applications.subevents_empty'); + }; + $grid->getInlineAdd()->setText($this->translator->translate('web.profile.seminar.applications.add_subevents')); + $grid->getInlineAdd()->onSubmit[] = [$this, 'add']; + } + + $grid->addInlineEdit()->onControlAdd[] = function (Container $container): void { + $options = $this->subeventService->getSubeventsOptionsWithCapacity(true, true, false, true, $this->user); + $container->addMultiSelect('subevents', '', $options) + ->setHtmlAttribute('class', 'datagrid-multiselect') + ->addRule(Form::FILLED, 'web.profile.seminar.applications.subevents_empty'); + }; + $grid->getInlineEdit()->setText($this->translator->translate('web.profile.seminar.applications.edit')); + $grid->getInlineEdit()->onSetDefaults[] = function (Container $container, SubeventsApplication $item): void { + $container->setDefaults([ + 'subevents' => $this->subeventRepository->findSubeventsIds($item->getSubevents()), + ]); + }; + $grid->getInlineEdit()->onSubmit[] = [$this, 'edit']; + $grid->allowRowsInlineEdit(fn (Application $item) => $this->applicationService->isAllowedEditApplication($item)); + } + + $grid->addAction('generatePaymentProofBank', 'web.profile.seminar.applications.download_payment_proof'); + $grid->allowRowsAction('generatePaymentProofBank', static fn (Application $item) => $item->getState() === ApplicationState::PAID + && $item->getPaymentMethod() === PaymentType::BANK + && $item->getPaymentDate()); + + if ($this->user->getNotCanceledSubeventsApplications()->count() > 1) { + $grid->addAction('cancelApplication', 'web.profile.seminar.applications.cancel_application') + ->addAttributes([ + 'data-toggle' => 'confirmation', + 'data-content' => $this->translator->translate('web.profile.seminar.applications.cancel_application_confirm'), + ])->setClass('btn btn-xs btn-danger'); + $grid->allowRowsAction('cancelApplication', fn (Application $item) => $this->applicationService->isAllowedEditApplication($item)); + } + + $grid->setItemsDetail() + ->setRenderCondition(static fn (Application $item) => $item->isWaitingForPayment()) + ->setText($this->translator->translate('web.profile.seminar.applications.pay')) + ->setIcon('money-bill-1') + ->setClass('btn btn-xs btn-primary ajax') + ->setTemplateParameters([ + 'account' => $this->queryBus->handle(new SettingStringValueQuery(Settings::ACCOUNT_NUMBER)), + 'message' => $this->queryBus->handle(new SettingStringValueQuery(Settings::SEMINAR_NAME)), + ]); + $grid->setTemplateFile(__DIR__ . '/templates/applications_grid_detail.latte'); + + $grid->setColumnsSummary(['fee'], static fn (Application $item, $column) => $item->isCanceled() ? 0 : $item->getFee()); + + $grid->setRowCallback(static function (Application $application, Html $tr): void { + if ($application->isCanceled()) { + $tr->addClass('disabled'); + } + }); + } + + /** + * Zpracuje přidání podakcí. + * + * @throws AbortException + * @throws Throwable + */ + public function add(stdClass $values): void + { + $selectedSubevents = $this->subeventRepository->findSubeventsByIds($values->subevents); + $selectedAndUsersSubevents = clone $this->user->getSubevents(); + foreach ($selectedSubevents as $subevent) { + $selectedAndUsersSubevents->add($subevent); + } + + $p = $this->getPresenter(); + + if (! $this->validators->validateSubeventsCapacities($selectedSubevents, $this->user)) { + $p->flashMessage('web.profile.seminar.applications.subevents_capacity_occupied', 'danger'); + $p->redrawControl('flashes'); + + return; + } + + foreach ($this->subeventRepository->findFilteredSubevents(true, false, false, false) as $subevent) { + if (! $this->validators->validateSubeventsIncompatible($selectedAndUsersSubevents, $subevent)) { + $message = $this->translator->translate( + 'web.profile.seminar.applications.incompatible_subevents_selected', + null, + ['subevent' => $subevent->getName(), 'incompatibleSubevents' => $subevent->getIncompatibleSubeventsText()], + ); + $p->flashMessage($message, 'danger'); + $p->redrawControl('flashes'); + + return; + } + + if (! $this->validators->validateSubeventsRequired($selectedAndUsersSubevents, $subevent)) { + $message = $this->translator->translate( + 'web.profile.seminar.applications.required_subevents_not_selected', + null, + ['subevent' => $subevent->getName(), 'requiredSubevents' => $subevent->getRequiredSubeventsTransitiveText()], + ); + $p->flashMessage($message, 'danger'); + $p->redrawControl('flashes'); + + return; + } + } + + $this->applicationService->addSubeventsApplication($this->user, $selectedSubevents, $this->user); + + $p->flashMessage('web.profile.seminar.applications.add_subevents_successful', 'success'); + $p->redrawControl('flashes'); + } + + /** + * Zpracuje úpravu přihlášky. + * + * @throws SettingsItemNotFoundException + * @throws AbortException + * @throws Throwable + * @throws MailingMailCreationException + */ + public function edit(string $id, stdClass $values): void + { + $application = $this->applicationRepository->findById((int) $id); + + if ($application instanceof SubeventsApplication) { + $selectedSubevents = $this->subeventRepository->findSubeventsByIds($values->subevents); + $selectedAndUsersSubevents = clone $this->user->getSubevents(); + foreach ($selectedSubevents as $subevent) { + $selectedAndUsersSubevents->add($subevent); + } + + foreach ($application->getSubevents() as $subevent) { + $selectedAndUsersSubevents->removeElement($subevent); + } + + $p = $this->getPresenter(); + + if (! $this->validators->validateSubeventsCapacities($selectedSubevents, $this->user)) { + $p->flashMessage('web.profile.seminar.applications.subevents_capacity_occupied', 'danger'); + $p->redrawControl('flashes'); + + return; + } + + foreach ($this->subeventRepository->findFilteredSubevents(true, false, false, false) as $subevent) { + if (! $this->validators->validateSubeventsIncompatible($selectedAndUsersSubevents, $subevent)) { + $message = $this->translator->translate( + 'web.profile.seminar.applications.incompatible_subevents_selected', + null, + ['subevent' => $subevent->getName(), 'incompatibleSubevents' => $subevent->getIncompatibleSubeventsText()], + ); + $p->flashMessage($message, 'danger'); + $p->redrawControl('flashes'); + + return; + } + + if (! $this->validators->validateSubeventsRequired($selectedAndUsersSubevents, $subevent)) { + $message = $this->translator->translate( + 'web.profile.seminar.applications.required_subevents_not_selected', + null, + ['subevent' => $subevent->getName(), 'requiredSubevents' => $subevent->getRequiredSubeventsTransitiveText()], + ); + $p->flashMessage($message, 'danger'); + $p->redrawControl('flashes'); + + return; + } + } + + $this->applicationService->updateSubeventsApplication($application, $selectedSubevents, $this->user); + + $p->flashMessage('web.profile.seminar.applications.edit_successful', 'success'); + $p->redrawControl('flashes'); + } + } + + /** + * Vygeneruje potvrzení o přijetí platby. + * + * @throws AbortException + */ + public function handleGeneratePaymentProofBank(int $id): void + { + $this->presenter->redirect(':Export:IncomeProof:application', ['id' => $id]); + } + + /** + * Zruší přihlášku. + * + * @throws SettingsItemNotFoundException + * @throws AbortException + * @throws Throwable + */ + public function handleCancelApplication(int $id): void + { + $application = $this->applicationRepository->findById($id); + + $p = $this->getPresenter(); + + if ($application instanceof SubeventsApplication && $this->applicationService->isAllowedEditApplication($application)) { + $this->applicationService->cancelSubeventsApplication($application, ApplicationState::CANCELED, $application->getUser()); + $p->flashMessage('web.profile.seminar.applications.application_canceled', 'success'); + } + + $p->redirect('this'); + } +} diff --git a/app/WebModule/Components/GroupUsersGridControl.php b/app/WebModule/Components/GroupUsersGridControl.php new file mode 100644 index 000000000..b18f7fc7f --- /dev/null +++ b/app/WebModule/Components/GroupUsersGridControl.php @@ -0,0 +1,650 @@ +sessionSection = $session->getSection('srs'); + } + + /** + * Vykreslí komponentu. + */ + public function render(): void + { + $this->template->setFile(__DIR__ . '/templates/groupUsers_grid.latte'); + $this->template->render(); + } + + /** + * Vytvoří komponentu. + * + * @throws Throwable + * @throws DataGridColumnStatusException + * @throws DataGridException + */ + public function createComponentGroupUsersGrid(string $name): DataGrid + { + $this->user = $this->getPresenter()->getDbUser(); + $groupId = $this->user->getGroupId(); + + $grid = new DataGrid($this, $name); + $grid->setTranslator($this->translator); + $grid->setDataSource($this->userRepository->createQueryBuilder('u') + ->where('u.groupId = :groupId') + ->setParameter('groupId', $groupId)); + $grid->setDefaultSort(['lastName' => 'ASC']); + $grid->setColumnsHideable(); + $grid->setItemsPerPageList([25, 50, 100, 250, 500]); + $grid->setStrictSessionFilterValues(false); + + $grid->addColumnText('displayName', 'admin.users.users_name') + ->setSortable() + ->setFilterText(); + + $grid->addColumnText('username', 'admin.users.users_username') + ->setSortable() + ->setFilterText(); + + $grid->addColumnText('roles', 'admin.users.users_roles', 'rolesText') + ->setFilterMultiSelect($this->aclService->getRolesWithoutRolesOptions([Role::GUEST, Role::UNAPPROVED])) + ->setCondition(static function (QueryBuilder $qb, ArrayHash $values): void { + $qb->join('u.roles', 'uR') + ->andWhere('uR.id IN (:rids)') + ->setParameter('rids', (array) $values); + }); + + $columnApproved = $grid->addColumnStatus('approved', 'admin.users.users_approved'); + $columnApproved + ->addOption(false, 'admin.users.users_approved_unapproved') + ->setClass('btn-danger') + ->endOption() + ->addOption(true, 'admin.users.users_approved_approved') + ->setClass('btn-success') + ->endOption() + ->onChange[] = [$this, 'changeApproved']; + $columnApproved + ->setSortable() + ->setFilterSelect([ + '' => 'admin.common.all', + '0' => 'admin.users.users_approved_unapproved', + '1' => 'admin.users.users_approved_approved', + ]) + ->setTranslateOptions(); + + $grid->addColumnText('unit', 'admin.users.users_membership') + ->setRendererOnCondition( + fn (User $row) => Html::el('span') + ->class('text-danger') + ->setText($this->userService->getMembershipText($row)), + static fn (User $row) => $row->getUnit() === null + ) + ->setSortable() + ->setSortableCallback(static function (QueryBuilder $qb, array $sort): void { + $sortOrig = $sort['unit']; + $sortRev = $sort['unit'] === 'DESC' ? 'ASC' : 'DESC'; + $qb->orderBy('u.unit', $sortOrig) + ->addOrderBy('u.externalLector', $sortRev) + ->addOrderBy('u.member', $sortRev); + }) + ->setFilterText(); + + $grid->addColumnNumber('age', 'admin.users.users_age') + ->setSortable() + ->setSortableCallback(static function (QueryBuilder $qb, array $sort): void { + $sortRev = $sort['age'] === 'DESC' ? 'ASC' : 'DESC'; + $qb->orderBy('u.birthdate', $sortRev); + }); + + $grid->addColumnText('email', 'admin.users.users_email') + ->setRenderer(static fn (User $row) => Html::el('a') + ->href('mailto:' . $row->getEmail()) + ->setText($row->getEmail())) + ->setSortable() + ->setFilterText(); + + $grid->addColumnText('phone', 'admin.users.users_phone') + ->setRenderer(static fn (User $row) => Html::el('a') + ->href('tel:' . $row->getPhone()) + ->setText($row->getPhone())) + ->setFilterText(); + + $grid->addAction('delete', '', 'delete!') + ->setIcon('trash') + ->setTitle('admin.common.delete') + ->setClass('btn btn-xs btn-danger') + ->addAttributes([ + 'data-toggle' => 'confirmation', + 'data-content' => $this->translator->translate('admin.users.users_delete_confirm'), + ]); +// $grid->allowRowsAction('delete', static fn (User $item) => $item->isExternalLector()); + + return $grid; + } + + /** + * Zpracuje odstranění externího uživatele. + * + * @throws AbortException + */ + public function handleDelete(int $id): void + { + $p = $this->getPresenter(); + + $removeUser = $this->userRepository->findById($id); + $userGroupId = $removeUser->setGroupId(0); + $this->userRepository->save($removeUser); + + if ($user->isInRole($this->roleRepository->findBySystemName(Role::GROUP_MEMBER)->getName())) { + $userRoleUnregistered = $this->roleRepository->findById(2); + $removeUser->addRole($userRoleUnregistered); + $userRoleMember = $this->roleRepository->findById(10); + $removeUser->removeRole($userRoleMember); + } + + $p->flashMessage('admin.users.users_deleted', 'success'); + $p->redirect('this'); + } + + /** + * Změní stav uživatele. + * + * @throws AbortException + */ + public function changeApproved(string $id, string $approved): void + { + $user = $this->userRepository->findById((int) $id); + + $this->userService->setApproved($user, (bool) $approved); + + $p = $this->getPresenter(); + $p->flashMessage('admin.users.users_changed_approved', 'success'); + + if ($p->isAjax()) { + $p->redrawControl('flashes'); + $this->getComponent('usersGrid')->redrawItem($id); + } else { + $p->redirect('this'); + } + } + + /** + * Změní účast uživatele na semináři. + * + * @throws AbortException + */ + public function changeAttended(string $id, string $attended): void + { + $user = $this->userRepository->findById((int) $id); + $user->setAttended((bool) $attended); + $this->userRepository->save($user); + + $p = $this->getPresenter(); + $p->flashMessage('admin.users.users_changed_attended', 'success'); + + if ($p->isAjax()) { + $p->redrawControl('flashes'); + $this->getComponent('usersGrid')->redrawItem($id); + } else { + $p->redirect('this'); + } + } + + /** + * Hromadně schválí uživatele. + * + * @param int[] $ids + * + * @throws Throwable + */ + public function groupApprove(array $ids): void + { + $users = $this->userRepository->findUsersByIds($ids); + + $this->em->wrapInTransaction(function () use ($users): void { + foreach ($users as $user) { + $this->userService->setApproved($user, true); + } + }); + + $p = $this->getPresenter(); + $p->flashMessage('admin.users.users_group_action_approved', 'success'); + + $this->reload(); + } + + /** + * Hromadně nastaví role. + * + * @param int[] $ids + * @param int[] $value + * + * @throws Throwable + */ + public function groupChangeRoles(array $ids, array $value): void + { + $users = $this->userRepository->findUsersByIds($ids); + $selectedRoles = $this->roleRepository->findRolesByIds($value); + + $p = $this->getPresenter(); + assert($p instanceof AdminBasePresenter); + + // neni vybrana zadna role + if ($selectedRoles->isEmpty()) { + $p->flashMessage('admin.users.users_group_action_change_roles_error_empty', 'danger'); + $this->reload(); + + return; + } + + // v rolich musi byt dostatek volnych mist + $capacitiesOk = $selectedRoles->forAll(static function (int $key, Role $role) use ($users) { + if (! $role->hasLimitedCapacity()) { + return true; + } + + $capacityNeeded = $users->count(); + + if ($capacityNeeded <= $role->getCapacity()) { + return true; + } + + foreach ($users as $user) { + if ($user->isInRole($role)) { + $capacityNeeded--; + } + } + + return $capacityNeeded <= $role->getCapacity(); + }); + + if (! $capacitiesOk) { + $p->flashMessage('admin.users.users_group_action_change_roles_error_capacity', 'danger'); + $this->reload(); + + return; + } + + $loggedUser = $p->getDbUser(); + + $this->em->wrapInTransaction(function () use ($selectedRoles, $users, $loggedUser): void { + foreach ($users as $user) { + $this->applicationService->updateRoles($user, $selectedRoles, $loggedUser, true); + } + }); + + $p->flashMessage('admin.users.users_group_action_changed_roles', 'success'); + $this->reload(); + } + + /** + * Hromadně označí uživatele jako zúčastněné. + * + * @param int[] $ids + * + * @throws Throwable + */ + public function groupMarkAttended(array $ids): void + { + $users = $this->userRepository->findUsersByIds($ids); + + $this->em->wrapInTransaction(function () use ($users): void { + foreach ($users as $user) { + $user->setAttended(true); + $this->userRepository->save($user); + } + }); + + $p = $this->getPresenter(); + $p->flashMessage('admin.users.users_group_action_marked_attended', 'success'); + $this->reload(); + } + + /** + * Hromadně označí uživatele jako zaplacené dnes. + * + * @param int[] $ids + * + * @throws Throwable + */ + public function groupMarkPaidToday(array $ids, string $paymentMethod): void + { + $users = $this->userRepository->findUsersByIds($ids); + + $p = $this->getPresenter(); + assert($p instanceof AdminBasePresenter); + + $loggedUser = $p->getDbUser(); + + $this->em->wrapInTransaction(function () use ($users, $paymentMethod, $loggedUser): void { + foreach ($users as $user) { + foreach ($user->getWaitingForPaymentApplications() as $application) { + $this->applicationService->updateApplicationPayment( + $application, + $paymentMethod, + new DateTimeImmutable(), + $application->getMaturityDate(), + $loggedUser, + ); + } + } + }); + + $p->flashMessage('admin.users.users_group_action_marked_paid_today', 'success'); + $this->reload(); + } + + /** + * Hromadně vloží uživatele jako účastníky do skautIS. + * + * @param int[] $ids + * + * @throws Throwable + */ + public function groupInsertIntoSkautIs(array $ids, int|null $accept): void + { + $users = $this->userRepository->findUsersByIds($ids); + + $p = $this->getPresenter(); + + $eventId = $this->queryBus->handle(new SettingIntValueQuery(Settings::SKAUTIS_EVENT_ID)); + + if ($eventId === null) { + $p->flashMessage('admin.users.users_group_action_insert_into_skaut_is_error_not_connected', 'danger'); + $this->reload(); + + return; + } + + switch ($this->queryBus->handle(new SettingStringValueQuery(Settings::SKAUTIS_EVENT_TYPE))) { + case SkautIsEventType::GENERAL: + $skautIsEventService = $this->skautIsEventGeneralService; + break; + + case SkautIsEventType::EDUCATION: + $skautIsEventService = $this->skautIsEventEducationService; + if (! $skautIsEventService->isSubeventConnected()) { + $p->flashMessage('admin.users.users_group_action_insert_into_skaut_is_error_subevent_not_connected', 'danger'); + $this->reload(); + + return; + } + + break; + + default: + throw new InvalidArgumentException(); + } + + if (! $skautIsEventService->isEventDraft($eventId)) { + $p->flashMessage('admin.users.users_group_action_insert_into_skaut_is_error_not_draft', 'danger'); + $this->reload(); + + return; + } + + if ($skautIsEventService->insertParticipants($eventId, $users, $accept === 1)) { + $p->flashMessage('admin.users.users_group_action_insert_into_skaut_is_successful', 'success'); + } else { + $p->flashMessage('admin.users.users_group_action_insert_into_skaut_is_error_skaut_is', 'danger'); + } + + $this->reload(); + } + + /** + * Hromadně vygeneruje potvrzení o zaplacení. + * + * @param int[] $ids + * + * @throws AbortException + */ + public function groupGeneratePaymentProofs(array $ids): void + { + $this->sessionSection->userIds = $ids; + $this->getPresenter()->redirect(':Export:IncomeProof:users'); + } + + /** + * Hromadně vyexportuje seznam uživatelů. + * + * @param int[] $ids + * + * @throws AbortException + */ + public function groupExportUsers(array $ids): void + { + $this->sessionSection->userIds = $ids; + $this->redirect('exportusers'); + } + + /** + * Zpracuje export seznamu uživatelů. + * + * @throws AbortException + * @throws Exception + */ + public function handleExportUsers(): void + { + $ids = $this->session->getSection('srs')->userIds; + + $users = $this->userRepository->findUsersByIds($ids); + + $response = $this->excelExportService->exportUsersList($users, 'seznam-uzivatelu.xlsx'); + + $this->getPresenter()->sendResponse($response); + } + + /** + * Hromadně vyexportuje seznam uživatelů s rolemi. + * + * @param int[] $ids + * + * @throws AbortException + */ + public function groupExportRoles(array $ids): void + { + $this->sessionSection->userIds = $ids; + $this->redirect('exportroles'); + } + + /** + * Zpracuje export seznamu uživatelů s rolemi. + * + * @throws AbortException + * @throws Exception + */ + public function handleExportRoles(): void + { + $ids = $this->session->getSection('srs')->userIds; + + $users = $this->userRepository->findUsersByIds($ids); + $roles = $this->roleRepository->findAll(); + + $response = $this->excelExportService->exportUsersRoles($users, $roles, 'role-uzivatelu.xlsx'); + + $this->getPresenter()->sendResponse($response); + } + + /** + * Hromadně vyexportuje seznam uživatelů s podakcemi a programy podle kategorií. + * + * @param int[] $ids + * + * @throws AbortException + */ + public function groupExportSubeventsAndCategories(array $ids): void + { + $this->sessionSection->userIds = $ids; + $this->redirect('exportsubeventsandcategories'); + } + + /** + * Zpracuje export seznamu uživatelů s podakcemi a programy podle kategorií. + * + * @throws AbortException + * @throws Exception + */ + public function handleExportSubeventsAndCategories(): void + { + $ids = $this->session->getSection('srs')->userIds; + + $users = $this->userRepository->findUsersByIds($ids); + + $response = $this->excelExportService->exportUsersSubeventsAndCategories($users, 'podakce-a-kategorie.xlsx'); + + $this->getPresenter()->sendResponse($response); + } + + /** + * Hromadně vyexportuje harmonogramy uživatelů. + * + * @param int[] $ids + * + * @throws AbortException + */ + public function groupExportSchedules(array $ids): void + { + $this->sessionSection->userIds = $ids; + $this->redirect('exportschedules'); + } + + /** + * Zpracuje export harmonogramů uživatelů. + * + * @throws AbortException + * @throws Exception + */ + public function handleExportSchedules(): void + { + $ids = $this->session->getSection('srs')->userIds; + + $users = $this->userRepository->findUsersByIds($ids); + + $response = $this->excelExportService->exportUsersSchedules($users, 'harmonogramy-uzivatelu.xlsx'); + + $this->getPresenter()->sendResponse($response); + } + + /** + * Vrátí platební metody jako možnosti pro select. Bez prázdné možnosti. + * + * @return string[] + */ + private function preparePaymentMethodOptionsWithoutEmpty(): array + { + $options = []; + foreach (PaymentType::$types as $type) { + $options[$type] = 'common.payment.' . $type; + } + + return $options; + } + + /** + * Vrátí platební metody jako možnosti pro select. Včetně smíšené. + * + * @return string[] + */ + private function preparePaymentMethodOptionsWithMixed(): array + { + $options = []; + foreach (PaymentType::$types as $type) { + $options[$type] = 'common.payment.' . $type; + } + + $options[PaymentType::MIXED] = 'common.payment.' . PaymentType::MIXED; + + return $options; + } + + /** + * Vrátí možnosti vložení účastníků do vzdělávací akce skautIS. + * + * @return string[] + */ + private function prepareInsertIntoSkautIsOptions(): array + { + $options = []; + $options[0] = 'common.skautis_event_insert_type.registered'; + $options[1] = 'common.skautis_event_insert_type.accepted'; + + return $options; + } + + /** @throws AbortException */ + private function reload(): void + { + $p = $this->getPresenter(); + if ($p->isAjax()) { + $p->redrawControl('flashes'); + $this->getComponent('usersGrid')->reload(); + } else { + $p->redirect('this'); + } + } +} diff --git a/app/WebModule/Components/IApplicationGroupContentControlFactory.php b/app/WebModule/Components/IApplicationGroupContentControlFactory.php new file mode 100644 index 000000000..c6b6ba9ca --- /dev/null +++ b/app/WebModule/Components/IApplicationGroupContentControlFactory.php @@ -0,0 +1,13 @@ +
+ +
+ + {if $guestRole} +
+ +
+ + {elseif $groupLeaderRole} +
+
+
+ {_web.application_content.approved_registration, ['roles' => $dbUser->getRolesText()]} +
+
+
+
+
+
+
+ Název skupiny: {$group->getName()} +
+
+ Status: {$group->getGroupStatus()} +
+
+ Max pocet mist: {$group->getPlaces()} +
+
+ Kod: {$group->getCode()} +
+
+ Pocet clenu: {$groupUsersCount} +
+
+ Pocet volnych mist: {($group->getPlaces())-$groupUsersCount} +
+
+ + + Uzavřít skupinu + +
+
+ + + Zrušit skupinu + +
+
+
+
+
+ {control groupUsersGrid} +
+
+ {elseif $groupMemberRole} +
+
+
+ {_web.application_content.approved_registration, ['roles' => $dbUser->getRolesText()]} +
+
+
+
+
+
+
+ Název skupiny: {$group->getName()} +
+
+ Status: {$group->getGroupStatus()} +
+
+ Vedoucí: {$groupLeaderName} +
+
+ Email vedoucího: {$group->getLeaderEmail()} +
+
+ + + Odhlásit ze skupiny + +
+
+
+
+ {elseif $nonregisteredRole} + {if $noRegisterableRole} +
+
+
+ {if $registrationStart && $registrationEnd} + {_web.application_content.no_registerable_role_start_end, ['start' => $registrationStart->format('j. n. Y H:i'), 'end' => $registrationEnd->format('j. n. Y H:i')]} + {elseif $registrationStart} + {_web.application_content.no_registerable_role_start, ['start' => $registrationStart->format('j. n. Y H:i')]} + {elseif $registrationEnd} + {_web.application_content.no_registerable_role_end, ['end' => $registrationEnd->format('j. n. Y H:i')]} + {else} + {_web.application_content.no_registerable_role} + {/if} +
+
+
+ {else} +
+
+
+ {control applicationGroupForm} +
+
+
+ {/if} + {else} + {if $unapprovedRole} +
+
+
+ {_web.application_content.unapproved_registration, ['roles' => $dbUser->getRolesText()]} +
+
+
+ {else} +
+
+
+ {_web.application_content.approved_registration, ['roles' => $dbUser->getRolesText()]} +
+
+
+
+
+ {control applicationsGrid} +
+
+ {/if} + {/if} +
diff --git a/app/WebModule/Components/templates/application_group_content_scripts.latte b/app/WebModule/Components/templates/application_group_content_scripts.latte new file mode 100644 index 000000000..db92a4b7d --- /dev/null +++ b/app/WebModule/Components/templates/application_group_content_scripts.latte @@ -0,0 +1,46 @@ + diff --git a/app/WebModule/Components/templates/groupUsers_grid.latte b/app/WebModule/Components/templates/groupUsers_grid.latte new file mode 100644 index 000000000..44d7f0ef3 --- /dev/null +++ b/app/WebModule/Components/templates/groupUsers_grid.latte @@ -0,0 +1 @@ +{control groupUsersGrid} diff --git a/app/WebModule/Forms/ApplicationGroupFormFactory.php b/app/WebModule/Forms/ApplicationGroupFormFactory.php new file mode 100644 index 000000000..9024ab2b6 --- /dev/null +++ b/app/WebModule/Forms/ApplicationGroupFormFactory.php @@ -0,0 +1,674 @@ +user = $user; + + $form = $this->baseFormFactory->create(); + + $inputSex = $form->addRadioList('sex', 'web.application_content.sex', Sex::getSexOptions()); + + $inputFirstName = $form->addText('firstName', 'web.application_content.firstname') + ->addRule(Form::FILLED, 'web.application_content.firstname_empty'); + + $inputLastName = $form->addText('lastName', 'web.application_content.lastname') + ->addRule(Form::FILLED, 'web.application_content.lastname_empty'); + + $inputNickName = $form->addText('nickName', 'web.application_content.nickname'); + + $inputBirthdateDate = new DateControl('web.application_content.birthdate'); + $inputBirthdateDate->addRule(Form::FILLED, 'web.application_content.birthdate_empty'); + $form->addComponent($inputBirthdateDate, 'birthdate'); + + if ($this->user->isMember()) { + $inputSex->setDisabled(); + $inputFirstName->setDisabled(); + $inputLastName->setDisabled(); + $inputNickName->setDisabled(); + $inputBirthdateDate->setDisabled(); + } + + $form->addText('email', 'web.application_content.email') + ->setDisabled(); + + $form->addText('phone', 'web.application_content.phone') + ->setDisabled(); + + $form->addText('street', 'web.application_content.street') + ->addRule(Form::FILLED, 'web.application_content.street_empty') + ->addRule(Form::PATTERN, 'web.application_content.street_format', '^(.*[^0-9]+) (([1-9][0-9]*)/)?([1-9][0-9]*[a-cA-C]?)$'); + + $form->addText('city', 'web.application_content.city') + ->addRule(Form::FILLED, 'web.application_content.city_empty'); + + $form->addText('postcode', 'web.application_content.postcode') + ->addRule(Form::FILLED, 'web.application_content.postcode_empty') + ->addRule(Form::PATTERN, 'web.application_content.postcode_format', '^\d{3} ?\d{2}$'); + + $form->addText('state', 'web.application_content.state') + ->addRule(Form::FILLED, 'web.application_content.state_empty'); + + $form->addSelect('roles', 'web.application_content.roles', [9 => 'Vedoucí skupiny', 10 => 'Člen skupiny']) + ->addCondition($form::EQUAL, 9) + ->toggle('group-name') + ->toggle('group-members-count') + ->elseCondition($form::EQUAL, 10) + ->toggle('group-code') + + ->addRule(Form::FILLED, 'web.application_content.custom_input_empty'); + + $form->addText('groupName', 'groupName') + ->setOption('id', 'group-name') + ->addCondition(Form::FILLED); + + $form->addText('groupMembersCount', 'groupMembersCount') + ->setOption('id', 'group-members-count') + ->addCondition(Form::FILLED); + + $form->addText('groupCode', 'groupCode') + ->setOption('id', 'group-code') + ->addCondition(Form::FILLED); + +// $this->addRolesSelect($form); + +// $this->addSubeventsSelect($form); + +// $this->addCustomInputs($form); + + $form->addCheckbox('agreement', $this->queryBus->handle(new SettingStringValueQuery(Settings::APPLICATION_AGREEMENT))) + ->addRule(Form::FILLED, 'web.application_content.agreement_empty'); + + $form->addSubmit('submit', 'web.application_content.register'); + + $form->setDefaults([ + 'sex' => $this->user->getSex(), + 'firstName' => $this->user->getFirstName(), + 'lastName' => $this->user->getLastName(), + 'nickName' => $this->user->getNickName(), + 'birthdate' => $this->user->getBirthdate(), + 'email' => $this->user->getEmail(), + 'phone' => $this->user->getPhone(), + 'street' => $this->user->getStreet(), + 'city' => $this->user->getCity(), + 'postcode' => $this->user->getPostcode(), + 'state' => $this->user->getState(), + ]); + + $form->onSuccess[] = [$this, 'processForm']; + + return $form; + } + + /** + * Zpracuje formulář. + * + * @throws Throwable + */ + public function processForm(Form $form, stdClass $values): void + { + $this->em->wrapInTransaction(function () use ($values): void { + if (property_exists($values, 'sex')) { + $this->user->setSex($values->sex); + } + + if (property_exists($values, 'firstName')) { + $this->user->setFirstName($values->firstName); + } + + if (property_exists($values, 'lastName')) { + $this->user->setLastName($values->lastName); + } + + if (property_exists($values, 'nickName')) { + $this->user->setNickName($values->nickName); + } + + if (property_exists($values, 'birthdate')) { + $this->user->setBirthdate($values->birthdate); + } + + $this->user->setStreet($values->street); + $this->user->setCity($values->city); + $this->user->setPostcode($values->postcode); + $this->user->setState($values->state); + +/* + // role + if (property_exists($values, 'roles')) { + $roles = $this->roleRepository->findRolesByIds($values->roles); + } else { + $roles = $this->roleRepository->findFilteredRoles(true, false, false); + } +*/ + //group akce + switch ($values->roles) { + //vedouci skupiny + case 9: + if (property_exists($values, 'groupName') && (property_exists($values, 'groupMembersCount'))) { + $group = new Group(); + + $group->setName($values->groupName); + $group->setLeaderId($this->user->getId()); + $group->setLeaderEmail($this->user->getEmail()); + $group->setPlaces($values->groupMembersCount); + $group->setGroupStatus('waiting_for_filling'); + + $this->groupRepository->save($group); + + $groupGeneratedCode = $group->getId() . time(); + $group->setCode($groupGeneratedCode); + + $this->groupRepository->save($group); + + $this->user->setGroupId($group->getId()); + + $userRole = $this->roleRepository->findById($values->roles); + $this->user->addRole($userRole); + } + + break; + + //clen skupiny + case 10: + if (property_exists($values, 'groupCode')) { + $group = $this->groupRepository->findByCode($values->groupCode); + if (! $group) { + throw new InvalidArgumentException('Invalid code.'); + } + + $this->user->setGroupId($group->getId()); + $userRole = $this->roleRepository->findById($values->roles); + $this->user->addRole($userRole); + } + + break; + } + +/* + // vlastni pole + foreach ($this->customInputRepository->findByRolesOrderedByPosition($roles) as $customInput) { + $customInputId = 'custom' . $customInput->getId(); + $customInputValue = $this->user->getCustomInputValue($customInput); + + if ($customInput instanceof CustomText) { + $customInputValue = $customInputValue ?: new CustomTextValue($customInput, $this->user); + assert($customInputValue instanceof CustomTextValue); + $customInputValue->setValue($values->$customInputId); + } elseif ($customInput instanceof CustomCheckbox) { + $customInputValue = $customInputValue ?: new CustomCheckboxValue($customInput, $this->user); + assert($customInputValue instanceof CustomCheckboxValue); + $customInputValue->setValue($values->$customInputId); + } elseif ($customInput instanceof CustomSelect) { + $customInputValue = $customInputValue ?: new CustomSelectValue($customInput, $this->user); + assert($customInputValue instanceof CustomSelectValue); + $customInputValue->setValue($values->$customInputId); + } elseif ($customInput instanceof CustomMultiSelect) { + $customInputValue = $customInputValue ?: new CustomMultiSelectValue($customInput, $this->user); + assert($customInputValue instanceof CustomMultiSelectValue); + $customInputValue->setValue($values->$customInputId); + } elseif ($customInput instanceof CustomFile) { + $customInputValue = $customInputValue ?: new CustomFileValue($customInput, $this->user); + assert($customInputValue instanceof CustomFileValue); + $file = $values->$customInputId; + assert($file instanceof FileUpload); + if ($file->getError() === UPLOAD_ERR_OK) { + $path = $this->filesService->save($file, CustomFile::PATH, true, $file->name); + $customInputValue->setValue($path); + } + } elseif ($customInput instanceof CustomDate) { + $customInputValue = $customInputValue ?: new CustomDateValue($customInput, $this->user); + assert($customInputValue instanceof CustomDateValue); + $customInputValue->setValue($values->$customInputId); + } elseif ($customInput instanceof CustomDateTime) { + $customInputValue = $customInputValue ?: new CustomDateTimeValue($customInput, $this->user); + assert($customInputValue instanceof CustomDateTimeValue); + $customInputValue->setValue($values->$customInputId); + } + + $this->customInputValueRepository->save($customInputValue); + } +*/ + // podakce + $subevents = ''; +/* + $subevents = $this->subeventRepository->explicitSubeventsExists() && ! empty($values->subevents) + ? $this->subeventRepository->findSubeventsByIds($values->subevents) + : new ArrayCollection([$this->subeventRepository->findImplicit()]); +*/ + // aktualizace údajů ve skautIS, jen pokud nemá propojený účet + if (! $this->user->isMember()) { + try { + $this->skautIsService->updatePersonBasic( + $this->user->getSkautISPersonId(), + $this->user->getSex(), + $this->user->getBirthdate(), + $this->user->getFirstName(), + $this->user->getLastName(), + $this->user->getNickName(), + ); + + $this->skautIsService->updatePersonAddress( + $this->user->getSkautISPersonId(), + $this->user->getStreet(), + $this->user->getCity(), + $this->user->getPostcode(), + $this->user->getState(), + ); + } catch (WsdlException $ex) { + Debugger::log($ex, ILogger::WARNING); + $this->onSkautIsError(); + } + } + + // vytvoreni prihlasky +// $this->applicationGroupService->register($this->user, $roles, $this->user); +// $this->applicationGroupService->register_group($this->user, $roles, $this->user, $group); + }); + } + + /** + * Přidá vlastní pole přihlášky. + */ + private function addCustomInputs(Form $form): void + { + foreach ($this->customInputRepository->findAllOrderedByPosition() as $customInput) { + $customInputId = 'custom' . $customInput->getId(); + $customInputName = $customInput->getName(); + + switch (true) { + case $customInput instanceof CustomText: + $custom = $form->addText($customInputId, $customInputName); + break; + + case $customInput instanceof CustomCheckbox: + $custom = $form->addCheckbox($customInputId, $customInputName); + break; + + case $customInput instanceof CustomSelect: + $custom = $form->addSelect($customInputId, $customInputName, $customInput->getSelectOptions()); + break; + + case $customInput instanceof CustomMultiSelect: + $custom = $form->addMultiSelect($customInputId, $customInputName, $customInput->getSelectOptions()); + break; + + case $customInput instanceof CustomFile: + $custom = $form->addUpload($customInputId, $customInputName); + $custom->setHtmlAttribute('data-show-preview', 'true'); + break; + + case $customInput instanceof CustomDate: + $custom = new DateControl($customInputName); + $form->addComponent($custom, $customInputId); + break; + + case $customInput instanceof CustomDateTime: + $custom = new DateTimeControl($customInputName); + $form->addComponent($custom, $customInputId); + break; + + default: + throw new InvalidArgumentException(); + } + + $custom->setOption('id', 'form-group-' . $customInputId); + + if ($customInput->isMandatory()) { + $rolesSelect = $form['roles']; + assert($rolesSelect instanceof MultiSelectBox); + $custom->addConditionOn($rolesSelect, self::class . '::toggleCustomInputRequired', ['id' => $customInputId, 'roles' => Helpers::getIds($customInput->getRoles())]) + ->addRule(Form::FILLED, 'web.application_content.custom_input_empty'); + } + } + } + + /** + * Přidá select pro výběr podakcí. + * + * @throws NonUniqueResultException + * @throws NoResultException + */ + private function addSubeventsSelect(Form $form): void + { + if (! $this->subeventRepository->explicitSubeventsExists()) { + return; + } + + $subeventsOptions = $this->subeventService->getSubeventsOptionsWithCapacity(true, true, false, false); + + $subeventsSelect = $form->addMultiSelect('subevents', 'web.application_content.subevents')->setItems( + $subeventsOptions, + ); + $subeventsSelect->setOption('id', 'form-group-subevents'); + + $rolesSelect = $form['roles']; + assert($rolesSelect instanceof MultiSelectBox); + $subeventsSelect->addConditionOn( + $rolesSelect, + self::class . '::toggleSubeventsRequired', + Helpers::getIds($this->roleRepository->findFilteredRoles(false, true, false)), + )->addRule(Form::FILLED, 'web.application_content.subevents_empty'); + + $subeventsSelect + ->setRequired(false) + ->addRule([$this, 'validateSubeventsCapacities'], 'web.application_content.subevents_capacity_occupied'); + + // generovani chybovych hlasek pro vsechny kombinace podakci + foreach ($this->subeventRepository->findFilteredSubevents(true, false, false, false) as $subevent) { + if (! $subevent->getIncompatibleSubevents()->isEmpty()) { + $subeventsSelect->addRule( + [$this, 'validateSubeventsIncompatible'], + $this->translator->translate( + 'web.application_content.incompatible_subevents_selected', + null, + ['subevent' => $subevent->getName(), 'incompatibleSubevents' => $subevent->getIncompatibleSubeventsText()], + ), + [$subevent], + ); + } + + if (! $subevent->getRequiredSubeventsTransitive()->isEmpty()) { + $subeventsSelect->addRule( + [$this, 'validateSubeventsRequired'], + $this->translator->translate( + 'web.application_content.required_subevents_not_selected', + null, + ['subevent' => $subevent->getName(), 'requiredSubevents' => $subevent->getRequiredSubeventsTransitiveText()], + ), + [$subevent], + ); + } + } + } + + /** + * Přidá select pro výběr rolí. + */ + private function addRolesSelect(Form $form): void + { + $registerableOptions = $this->aclService->getRolesOptionsWithCapacity(true, false); + + $rolesSelect = $form->addMultiSelect('roles', 'web.application_content.roles')->setItems( + $registerableOptions, + ); + + foreach ($this->customInputRepository->findAll() as $customInput) { + $customInputId = 'custom' . $customInput->getId(); + $rolesSelect->addCondition(self::class . '::toggleCustomInputVisibility', Helpers::getIds($customInput->getRoles())) + ->toggle('form-group-' . $customInputId); + } + + $rolesSelect->addRule(Form::FILLED, 'web.application_content.roles_empty') + ->addRule([$this, 'validateRolesCapacities'], 'web.application_content.roles_capacity_occupied') + ->addRule([$this, 'validateRolesRegisterable'], 'web.application_content.roles_not_registerable') + ->addRule([$this, 'validateRolesMinimumAge'], 'web.application_content.roles_require_minimum_age'); + + // generovani chybovych hlasek pro vsechny kombinace roli + foreach ($this->roleRepository->findFilteredRoles(true, false, true, $this->user) as $role) { + if (! $role->getIncompatibleRoles()->isEmpty()) { + $rolesSelect->addRule( + [$this, 'validateRolesIncompatible'], + $this->translator->translate( + 'web.application_content.incompatible_roles_selected', + null, + ['role' => $role->getName(), 'incompatibleRoles' => $role->getIncompatibleRolesText()], + ), + [$role], + ); + } + + if (! $role->getRequiredRolesTransitive()->isEmpty()) { + $rolesSelect->addRule( + [$this, 'validateRolesRequired'], + $this->translator->translate( + 'web.application_content.required_roles_not_selected', + null, + ['role' => $role->getName(), 'requiredRoles' => $role->getRequiredRolesTransitiveText()], + ), + [$role], + ); + } + } + + // pokud je na vyber jen jedna role, je oznacena + if (count($registerableOptions) === 1) { + $rolesSelect->setDisabled(); + $rolesSelect->setDefaultValue(array_keys($registerableOptions)); + } + } + + /** + * Ověří kapacity podakcí. + */ + public function validateSubeventsCapacities(MultiSelectBox $field): bool + { + $selectedSubevents = $this->subeventRepository->findSubeventsByIds($field->getVaLue()); + + return $this->validators->validateSubeventsCapacities($selectedSubevents, $this->user); + } + + /** + * Ověří kapacity rolí. + */ + public function validateRolesCapacities(MultiSelectBox $field): bool + { + $selectedRoles = $this->roleRepository->findRolesByIds($field->getValue()); + + return $this->validators->validateRolesCapacities($selectedRoles, $this->user); + } + + /** + * Ověří kompatibilitu podakcí. + * + * @param Subevent[] $args + */ + public function validateSubeventsIncompatible(MultiSelectBox $field, array $args): bool + { + $selectedSubevents = $this->subeventRepository->findSubeventsByIds($field->getValue()); + $testSubevent = $args[0]; + + return $this->validators->validateSubeventsIncompatible($selectedSubevents, $testSubevent); + } + + /** + * Ověří výběr požadovaných podakcí. + * + * @param Subevent[] $args + */ + public function validateSubeventsRequired(MultiSelectBox $field, array $args): bool + { + $selectedSubevents = $this->subeventRepository->findSubeventsByIds($field->getValue()); + $testSubevent = $args[0]; + + return $this->validators->validateSubeventsRequired($selectedSubevents, $testSubevent); + } + + /** + * Ověří kompatibilitu rolí. + * + * @param Role[] $args + */ + public function validateRolesIncompatible(MultiSelectBox $field, array $args): bool + { + $selectedRoles = $this->roleRepository->findRolesByIds($field->getValue()); + $testRole = $args[0]; + + return $this->validators->validateRolesIncompatible($selectedRoles, $testRole); + } + + /** + * Ověří výběr požadovaných rolí. + * + * @param Role[] $args + */ + public function validateRolesRequired(MultiSelectBox $field, array $args): bool + { + $selectedRoles = $this->roleRepository->findRolesByIds($field->getValue()); + $testRole = $args[0]; + + return $this->validators->validateRolesRequired($selectedRoles, $testRole); + } + + /** + * Ověří registrovatelnost rolí. + */ + public function validateRolesRegisterable(MultiSelectBox $field): bool + { + $selectedRoles = $this->roleRepository->findRolesByIds($field->getValue()); + + return $this->validators->validateRolesRegisterable($selectedRoles, $this->user); + } + + /** + * Ověří požadovaný minimální věk. + * + * @throws SettingsItemNotFoundException + * @throws Throwable + */ + public function validateRolesMinimumAge(MultiSelectBox $field): bool + { + $selectedRoles = $this->roleRepository->findRolesByIds($field->getValue()); + + return $this->validators->validateRolesMinimumAge($selectedRoles, $this->user); + } + + /** + * Přepíná povinnost podakcí podle kombinace rolí. + + * @param int[] $rolesWithSubevents + */ + public static function toggleSubeventsRequired(MultiSelectBox $field, array $rolesWithSubevents): bool + { + foreach ($field->getValue() as $roleId) { + if (in_array($roleId, $rolesWithSubevents)) { + return true; + } + } + + return false; + } + + /** + * Přepíná povinnost vlastních polí podle kombinace rolí. + * + * @param array $customInput + */ + public static function toggleCustomInputRequired(MultiSelectBox $field, array $customInput): bool + { + foreach ($field->getValue() as $roleId) { + if (in_array($roleId, $customInput['roles'])) { + return true; + } + } + + return false; + } + + /** + * Přepíná zobrazení vlastních polí podle kombinace rolí. + * Je nutná, na výsledku nezáleží (používá se javascript funkce). + * + * @param int[] $customInputRoles + */ + public static function toggleCustomInputVisibility(MultiSelectBox $field, array $customInputRoles): bool + { + return false; + } +} diff --git a/app/WebModule/Presenters/PagePresenter.php b/app/WebModule/Presenters/PagePresenter.php index 6c56480f2..81fb676ba 100644 --- a/app/WebModule/Presenters/PagePresenter.php +++ b/app/WebModule/Presenters/PagePresenter.php @@ -5,6 +5,7 @@ namespace App\WebModule\Presenters; use App\WebModule\Components\ApplicationContentControl; +use App\WebModule\Components\ApplicationGroupContentControl; use App\WebModule\Components\BlocksContentControl; use App\WebModule\Components\CapacitiesContentControl; use App\WebModule\Components\ContactFormContentControl; @@ -12,6 +13,7 @@ use App\WebModule\Components\FaqContentControl; use App\WebModule\Components\HtmlContentControl; use App\WebModule\Components\IApplicationContentControlFactory; +use App\WebModule\Components\IApplicationGroupContentControlFactory; use App\WebModule\Components\IBlocksContentControlFactory; use App\WebModule\Components\ICapacitiesContentControlFactory; use App\WebModule\Components\IContactFormContentControlFactory; @@ -50,6 +52,9 @@ class PagePresenter extends WebBasePresenter #[Inject] public IApplicationContentControlFactory $applicationContentControlFactory; + #[Inject] + public IApplicationGroupContentControlFactory $applicationGroupContentControlFactory; + #[Inject] public IBlocksContentControlFactory $blocksContentControlFactory; @@ -137,6 +142,11 @@ protected function createComponentApplicationContent(): ApplicationContentContro return $this->applicationContentControlFactory->create(); } + protected function createComponentApplicationGroupContent(): ApplicationGroupContentControl + { + return $this->applicationGroupContentControlFactory->create(); + } + protected function createComponentBlocksContent(): BlocksContentControl { return $this->blocksContentControlFactory->create(); diff --git a/app/lang/admin.cs_CZ.neon b/app/lang/admin.cs_CZ.neon index 8d75d9bb3..f91cc7191 100644 --- a/app/lang/admin.cs_CZ.neon +++ b/app/lang/admin.cs_CZ.neon @@ -645,6 +645,11 @@ groups: column: name: "Název" + group_status: "Status" + leader_name: "Jméno vedoucího" + leader_email: "Email vedoucího" + places: "Počet míst" + price: "Cena" configuration: menu: diff --git a/migrations/Version20231122111336.php b/migrations/Version20231122111336.php deleted file mode 100644 index 47b26f1a6..000000000 --- a/migrations/Version20231122111336.php +++ /dev/null @@ -1,39 +0,0 @@ -abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); - - $this->addSql('CREATE TABLE `group` (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, leader_id INT NOT NULL, leader_email VARCHAR(255) NOT NULL, create_date DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\', group_status_id INT NOT NULL, places VARCHAR(255) NOT NULL, price INT NOT NULL, note LONGTEXT DEFAULT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); - $this->addSql('CREATE TABLE status (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, UNIQUE INDEX UNIQ_7B00651C5E237E06 (name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); - $this->addSql('INSERT INTO `settings` (`item`, `value`) VALUES (\'group_fill_term\', \'2023-10-20\')'); - $this->addSql('INSERT INTO `settings` (`item`, `value`) VALUES (\'group_max_members\', \'11\')'); - $this->addSql('INSERT INTO `settings` (`item`, `value`) VALUES (\'group_min_members\', \'5\')'); - $this->addSql('INSERT INTO `resource` (`id`,`name`) VALUES (9, \'groups\')'); - $this->addSql('INSERT INTO `permission` (`id`, `resource_id`, `name`) VALUES (15, 9, \'manage\')'); - $this->addSql('INSERT INTO `role_permission` (`role_id`, `permission_id`) VALUES (8, 15)'); - $this->addSql('INSERT INTO `role_permission` (`role_id`, `permission_id`) VALUES (7, 15)'); - $this->addSql('INSERT INTO `status` (`name`) VALUES (\'čeká na naplnění\')'); - $this->addSql('INSERT INTO `status` (`name`) VALUES (\'čeká na zaplacení\')'); - $this->addSql('INSERT INTO `status` (`name`) VALUES (\'zaplacená\')'); - $this->addSql('INSERT INTO `status` (`name`) VALUES (\'zrušená\')'); - } - - public function down(Schema $schema): void - { - } -}