diff --git a/.github/workflows/deploy-obrok-dev.yml b/.github/workflows/deploy-obrok-dev.yml new file mode 100644 index 000000000..d94aa0ab7 --- /dev/null +++ b/.github/workflows/deploy-obrok-dev.yml @@ -0,0 +1,74 @@ +name: deploy-obrok-dev + +on: + push: + branches: [obrok/master] + +concurrency: + group: environment-dev + +jobs: + deploy: + name: "Deploy to srs-obrok.skauting.cz" + environment: srs-obrok.skauting.cz + runs-on: ubuntu-22.04 + container: + image: skaut/lebeda:8.1 + env: + CONFIG_DATABASE_HOST: ${{ secrets.CONFIG_DATABASE_HOST }} + CONFIG_DATABASE_NAME: ${{ secrets.CONFIG_DATABASE_NAME }} + CONFIG_DATABASE_PASSWORD: ${{ secrets.CONFIG_DATABASE_PASSWORD }} + CONFIG_DATABASE_USER: ${{ secrets.CONFIG_DATABASE_USER }} + CONFIG_MAIL_HOST: + CONFIG_MAIL_PASSWORD: + CONFIG_MAIL_PORT: 0 + CONFIG_MAIL_SECURE: + CONFIG_MAIL_SMTP: false + CONFIG_MAIL_USERNAME: + CONFIG_MAILING_SENDER_EMAIL: ${{ secrets.CONFIG_MAILING_SENDER_EMAIL }} + CONFIG_SKAUTIS_APPLICATION_ID: ${{ secrets.CONFIG_SKAUTIS_APPLICATION_ID }} + CONFIG_SKAUTIS_TEST_MODE: ${{ secrets.CONFIG_SKAUTIS_TEST_MODE }} + CONFIG_RECAPTCHA_SITE_KEY: ${{ secrets.CONFIG_RECAPTCHA_SITE_KEY }} + CONFIG_RECAPTCHA_SECRET_KEY: ${{ secrets.CONFIG_RECAPTCHA_SECRET_KEY }} + DEPLOY_DIRECTORY: ${{ secrets.DEPLOY_DIRECTORY }} + DEPLOY_LEBEDA: ${{ secrets.DEPLOY_LEBEDA }} + DEPLOY_SSH_HOST: ${{ secrets.DEPLOY_SSH_HOST }} + DEPLOY_SSH_IP: ${{ secrets.DEPLOY_SSH_IP }} + DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} + DEPLOY_SSH_PORT: ${{ secrets.DEPLOY_SSH_PORT }} + DEPLOY_SSH_USERNAME: ${{ secrets.DEPLOY_SSH_USERNAME }} + steps: + - uses: actions/checkout@v3 + - run: git config --global --add safe.directory '*' + # Copy & paste from https://github.com/actions/cache/blob/master/examples.md#php---composer + - name: Get composer cache + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + - uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + - name: Install yarn + run: | + apt-get update + apt-get install -y npm + npm install --global yarn + #Copy & paste from https://github.com/actions/cache/blob/master/examples.md#node---yarn + - name: Get yarn cache + id: yarn-cache + run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT + - uses: actions/cache@v3 + with: + path: ${{ steps.yarn-cache.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: Setup SSH key and deploy + run: | + mkdir -p /root/.ssh + ssh-keyscan -H "${DEPLOY_SSH_HOST}","${DEPLOY_SSH_IP}" >> /root/.ssh/known_hosts + eval `ssh-agent -s` + echo "${DEPLOY_SSH_KEY}" | tr -d '\r' | ssh-add - + phing deploy diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ab92ab0e8..9b5bf91b6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,9 +2,9 @@ name: test on: push: - branches: [master] + branches: [master,obrok/master] pull_request: - branches: [master] + branches: [master,obrok/master] concurrency: group: test-${{ github.ref }} diff --git a/app/AdminModule/ConfigurationModule/Forms/GroupFormFactory.php b/app/AdminModule/ConfigurationModule/Forms/GroupFormFactory.php new file mode 100644 index 000000000..87bb5cda0 --- /dev/null +++ b/app/AdminModule/ConfigurationModule/Forms/GroupFormFactory.php @@ -0,0 +1,117 @@ +baseFormFactory->create(); + + $renderer = $form->getRenderer(); + assert($renderer instanceof Bs4FormRenderer); + $renderer->wrappers['control']['container'] = 'div class="col-7"'; + $renderer->wrappers['label']['container'] = 'div class="col-5 col-form-label"'; + + $form->addText('groupMinMembers', 'admin.configuration.group_min_members') + ->addRule(Form::FILLED, 'admin.configuration.group_min_members_empty'); + + $form->addText('groupMaxMembers', 'admin.configuration.group_max_members') + ->addRule(Form::FILLED, 'admin.configuration.group_max_members_empty'); + + $groupFillTerm = new DateControl('admin.configuration.group_fill_term'); + $groupFillTerm->addRule(Form::FILLED, 'admin.configuration.group_fill_term_empty'); + $form->addComponent($groupFillTerm, 'groupFillTerm'); + + $form->addSubmit('submit', 'admin.common.save'); + + $form->setDefaults([ + 'groupMinMembers' => $this->queryBus->handle(new SettingStringValueQuery(Settings::GROUP_MIN_MEMBERS)), + 'groupMaxMembers' => $this->queryBus->handle(new SettingStringValueQuery(Settings::GROUP_MAX_MEMBERS)), + 'groupFillTerm' => $this->queryBus->handle(new SettingDateValueQuery(Settings::GROUP_FILL_TERM)), + ]); + + $form->onSuccess[] = [$this, 'processForm']; + + return $form; + } + + /** + * Zpracuje formulář. + * + * @throws Throwable + */ + public function processForm(Form $form, stdClass $values): void + { + $this->commandBus->handle(new SetSettingStringValue(Settings::GROUP_MIN_MEMBERS, $values->groupMinMembers)); + $this->commandBus->handle(new SetSettingStringValue(Settings::GROUP_MAX_MEMBERS, $values->groupMaxMembers)); + $this->commandBus->handle(new SetSettingDateValue(Settings::GROUP_FILL_TERM, $values->groupFillTerm)); + } + + /** + * Ověří, že datum začátku semináře je dříve než konce. + * + * @param DateTime[] $args + */ + public function validateSeminarFromDate(DateControl $field, array $args): bool + { + return $args[0] <= $args[1]; + } + + /** + * Ověří, že datum konce semináře je později než začátku. + * + * @param DateTime[] $args + */ + public function validateSeminarToDate(DateControl $field, array $args): bool + { + return $args[0] >= $args[1]; + } + + /** + * Ověří, že datum uzavření registrace je dříve než začátek semináře. + * + * @param DateTime[] $args + */ + public function validateEditRegistrationTo(DateControl $field, array $args): bool + { + return $args[0] < $args[1]; + } +} diff --git a/app/AdminModule/ConfigurationModule/Presenters/GroupPresenter.php b/app/AdminModule/ConfigurationModule/Presenters/GroupPresenter.php new file mode 100644 index 000000000..214815ace --- /dev/null +++ b/app/AdminModule/ConfigurationModule/Presenters/GroupPresenter.php @@ -0,0 +1,37 @@ +groupFormFactory->create(); + + $form->onSuccess[] = function (Form $form, stdClass $values): void { + $this->flashMessage('admin.configuration.configuration_saved', 'success'); + $this->redirect('this'); + }; + + return $form; + } +} diff --git a/app/AdminModule/ConfigurationModule/Presenters/templates/Group/default.latte b/app/AdminModule/ConfigurationModule/Presenters/templates/Group/default.latte new file mode 100644 index 000000000..15324a3ce --- /dev/null +++ b/app/AdminModule/ConfigurationModule/Presenters/templates/Group/default.latte @@ -0,0 +1,6 @@ +{block main} +

{_admin.configuration.group_heading}

+
+ {control groupForm} +
+{/block} \ No newline at end of file diff --git a/app/AdminModule/ConfigurationModule/Presenters/templates/sidebar.latte b/app/AdminModule/ConfigurationModule/Presenters/templates/sidebar.latte index e992fd693..cd16a581e 100644 --- a/app/AdminModule/ConfigurationModule/Presenters/templates/sidebar.latte +++ b/app/AdminModule/ConfigurationModule/Presenters/templates/sidebar.latte @@ -31,6 +31,9 @@ {_admin.configuration.menu.skautis} + + {_admin.configuration.menu.group} + {_admin.configuration.menu.web} diff --git a/app/AdminModule/GroupsModule/Components/GroupsGridControl.php b/app/AdminModule/GroupsModule/Components/GroupsGridControl.php new file mode 100644 index 000000000..e1f3dd58b --- /dev/null +++ b/app/AdminModule/GroupsModule/Components/GroupsGridControl.php @@ -0,0 +1,177 @@ +sessionSection = $session->getSection('srs'); + } + + /** + * Vykreslí komponentu. + */ + public function render(): void + { + $this->template->setFile(__DIR__ . '/templates/groups_grid.latte'); + $this->template->render(); + } + + /** + * Vytvoří komponentu. + * + * @throws DataGridException + */ + public function createComponentGroupsGrid(string $name): void + { + $grid = new DataGrid($this, $name); + $grid->setTranslator($this->translator); + $grid->setDataSource($this->groupRepository->createQueryBuilder('g')); + $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('leader_id', 'admin.groups.group.column.leader_name') + ->setRenderer(function (Group $row) { + $user = $this->userRepository->findById($row->getLeaderId()); + + 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', '') + ->addRule(Form::FILLED, 'admin.program.groups.column.name_empty') + ->addRule(Form::IS_NOT_IN, 'admin.program.groups.column.name_exists', $this->groupRepository->findAll()); + + $container->addText('places', '') + ->addCondition(Form::FILLED) + ->addRule(Form::INTEGER, 'admin.program.groups.column.capacity_format'); + }; + $grid->getInlineAdd()->onSubmit[] = [$this, 'add']; + + $grid->addInlineEdit()->onControlAdd[] = static function (Container $container): void { + $container->addText('name', '') + ->addRule(Form::FILLED, 'admin.program.groups.column.name_empty'); + + $container->addText('places', '') + ->addCondition(Form::FILLED) + ->addRule(Form::INTEGER, 'admin.program.groups.column.capacity_format'); + }; + $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->findAll()); + + $container->setDefaults([ + 'name' => $item->getName(), + 'places' => $item->getPlaces(), + ]); + }; + $grid->getInlineEdit()->onSubmit[] = [$this, 'edit']; + + $grid->addAction('detail', 'admin.common.detail', 'Groups:detail') + ->setClass('btn btn-xs btn-primary'); + + $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.program.groups.action.delete_confirm'), + ]); + } + + /** + * Zpracuje přidání místnosti. + */ + public function add(stdClass $values): void + { + $group = new Group($values->name, $values->capacity !== '' ? $values->capacity : null); + + $this->commandBus->handle(new SaveGroup($group)); + + $p = $this->getPresenter(); + $p->flashMessage('admin.program.groups.message.save_success', 'success'); + $p->redrawControl('flashes'); + } + + /** + * Zpracuje úpravu místnosti. + */ + public function edit(string $id, stdClass $values): void + { + $group = $this->groupRepository->findById((int) $id); + + $group->setName($values->name); + $group->setCapacity($values->capacity !== '' ? $values->capacity : null); + + $this->commandBus->handle(new SaveGroup($group)); + + $p = $this->getPresenter(); + $p->flashMessage('admin.program.groups.message.save_success', 'success'); + $p->redrawControl('flashes'); + } + + /** + * Odstraní místnost. + * + * @throws AbortException + */ + public function handleDelete(int $id): void + { + $group = $this->groupRepository->findById($id); + + $this->commandBus->handle(new RemoveGroup($group)); + + $p = $this->getPresenter(); + $p->flashMessage('admin.program.groups.message.delete_success', 'success'); + $p->redirect('this'); + } +} diff --git a/app/AdminModule/GroupsModule/Components/IGroupsGridControlFactory.php b/app/AdminModule/GroupsModule/Components/IGroupsGridControlFactory.php new file mode 100644 index 000000000..af0adfb4b --- /dev/null +++ b/app/AdminModule/GroupsModule/Components/IGroupsGridControlFactory.php @@ -0,0 +1,16 @@ +template->setFile(__DIR__ . '/templates/status_grid.latte'); + $this->template->render(); + } + + /** + * Vytvoří komponentu. + * + * @throws DataGridException + */ + public function createComponentStatusGrid(string $name): void + { + $grid = new DataGrid($this, $name); + $grid->setTranslator($this->translator); + $grid->setDataSource($this->statusRepository->createQueryBuilder('c')); + $grid->setDefaultSort(['name' => 'ASC']); + $grid->setPagination(false); + + $grid->addColumnText('name', 'admin.groups.status.column.name'); + + $grid->addInlineAdd()->setPositionTop()->onControlAdd[] = function (Container $container): 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']; + assert($nameText instanceof TextInput); + $nameText->addRule(Form::IS_NOT_IN, 'admin.groups.status.column.name_exists', $this->statusRepository->findOthersNames($item->getId())); + + $container->setDefaults([ + 'name' => $item->getName(), + ]); + }; + $grid->getInlineEdit()->onSubmit[] = [$this, 'edit']; + + $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.groups.status.action.delete_confirm'), + ]); + } + + /** + * Zpracuje přidání kategorie. + */ + public function add(stdClass $values): void + { + $status = new Status(); + + $this->commandBus->handle(new SaveStatus($status)); + + $this->getPresenter()->flashMessage('admin.groups.status.message.save_success', 'success'); + $this->getPresenter()->redrawControl('flashes'); + } + + /** + * Zpracuje úpravu kategorie. + * + * @throws Throwable + */ + public function edit(string $id, stdClass $values): void + { + $status = $this->statusRepository->findById((int) $id); + $statusOld = clone $status; + + $status->setName($values->name); + + $this->commandBus->handle(new SaveStatus($status)); + + $this->getPresenter()->flashMessage('admin.groups.status.message.save_success', 'success'); + $this->getPresenter()->redrawControl('flashes'); + } + + /** + * Odstraní kategorii. + * + * @throws AbortException + * @throws Throwable + */ + public function handleDelete(int $id): void + { + $status = $this->statusRepository->findById($id); + + $p = $this->getPresenter(); + + $this->commandBus->handle(new RemoveStatus($status)); + $p->flashMessage('admin.groups.status.message.delete_success', 'success'); + + $p->redirect('this'); + } +} diff --git a/app/AdminModule/GroupsModule/Components/templates/groups_grid.latte b/app/AdminModule/GroupsModule/Components/templates/groups_grid.latte new file mode 100644 index 000000000..b84b0d0e4 --- /dev/null +++ b/app/AdminModule/GroupsModule/Components/templates/groups_grid.latte @@ -0,0 +1 @@ +{control groupsGrid} \ No newline at end of file diff --git a/app/AdminModule/GroupsModule/Components/templates/status_grid.latte b/app/AdminModule/GroupsModule/Components/templates/status_grid.latte new file mode 100644 index 000000000..cb03db51c --- /dev/null +++ b/app/AdminModule/GroupsModule/Components/templates/status_grid.latte @@ -0,0 +1 @@ +{control statusGrid} \ No newline at end of file diff --git a/app/AdminModule/GroupsModule/Presenters/GroupsBasePresenter.php b/app/AdminModule/GroupsModule/Presenters/GroupsBasePresenter.php new file mode 100644 index 000000000..180535821 --- /dev/null +++ b/app/AdminModule/GroupsModule/Presenters/GroupsBasePresenter.php @@ -0,0 +1,26 @@ +checkPermission(Permission::MANAGE); + } +} diff --git a/app/AdminModule/GroupsModule/Presenters/GroupsPresenter.php b/app/AdminModule/GroupsModule/Presenters/GroupsPresenter.php new file mode 100644 index 000000000..cd72f22f1 --- /dev/null +++ b/app/AdminModule/GroupsModule/Presenters/GroupsPresenter.php @@ -0,0 +1,44 @@ +checkPermission(Permission::MANAGE); + } + + public function renderDetail(int $id): void + { + $group = $this->groupRepository->findById($id); + + $this->template->group = $group; + } + + protected function createComponentGroupsGrid(): GroupsGridControl + { + return $this->groupsGridControlFactory->create(); + } +} diff --git a/app/AdminModule/GroupsModule/Presenters/StatusPresenter.php b/app/AdminModule/GroupsModule/Presenters/StatusPresenter.php new file mode 100644 index 000000000..cb706769c --- /dev/null +++ b/app/AdminModule/GroupsModule/Presenters/StatusPresenter.php @@ -0,0 +1,33 @@ +checkPermission(Permission::MANAGE); + } + + protected function createComponentStatusGrid(): StatusGridControl + { + return $this->statusGridControlFactory->create(); + } +} diff --git a/app/AdminModule/GroupsModule/Presenters/templates/@layout.latte b/app/AdminModule/GroupsModule/Presenters/templates/@layout.latte new file mode 100644 index 000000000..1cc7452c7 --- /dev/null +++ b/app/AdminModule/GroupsModule/Presenters/templates/@layout.latte @@ -0,0 +1,2 @@ +{layout '../../../Presenters/templates/@layout.latte'} +{import 'sidebar.latte'} \ No newline at end of file diff --git a/app/AdminModule/GroupsModule/Presenters/templates/Groups/default.latte b/app/AdminModule/GroupsModule/Presenters/templates/Groups/default.latte new file mode 100644 index 000000000..e3374c03a --- /dev/null +++ b/app/AdminModule/GroupsModule/Presenters/templates/Groups/default.latte @@ -0,0 +1,4 @@ +{block main} +

{_admin.groups.group.heading}

+ {control groupsGrid} +{/block} \ No newline at end of file diff --git a/app/AdminModule/GroupsModule/Presenters/templates/Groups/detail.latte b/app/AdminModule/GroupsModule/Presenters/templates/Groups/detail.latte new file mode 100644 index 000000000..ffa3bba41 --- /dev/null +++ b/app/AdminModule/GroupsModule/Presenters/templates/Groups/detail.latte @@ -0,0 +1,6 @@ +{block main} +

{_admin.program.groups.detail.heading, ['name' => $room->getName()]}

+ +

{_admin.program.groups.schedule.heading}

+ {control groupscheduleGrid} +{/block} \ No newline at end of file diff --git a/app/AdminModule/GroupsModule/Presenters/templates/Status/default.latte b/app/AdminModule/GroupsModule/Presenters/templates/Status/default.latte new file mode 100644 index 000000000..c81cb73a3 --- /dev/null +++ b/app/AdminModule/GroupsModule/Presenters/templates/Status/default.latte @@ -0,0 +1,4 @@ +{block main} +

{_admin.groups.status.heading}

+ {control statusGrid} +{/block} \ No newline at end of file diff --git a/app/AdminModule/GroupsModule/Presenters/templates/sidebar.latte b/app/AdminModule/GroupsModule/Presenters/templates/sidebar.latte new file mode 100644 index 000000000..05f1d9ac9 --- /dev/null +++ b/app/AdminModule/GroupsModule/Presenters/templates/sidebar.latte @@ -0,0 +1,12 @@ +{block sidebar} +
+ +
+{/block} \ No newline at end of file diff --git a/app/AdminModule/Presenters/AdminBasePresenter.php b/app/AdminModule/Presenters/AdminBasePresenter.php index d311660ac..5ec80872d 100644 --- a/app/AdminModule/Presenters/AdminBasePresenter.php +++ b/app/AdminModule/Presenters/AdminBasePresenter.php @@ -91,6 +91,7 @@ public function beforeRender(): void $this->template->resourcePayments = SrsResource::PAYMENTS; $this->template->resourceMailing = SrsResource::MAILING; $this->template->resourceProgram = SrsResource::PROGRAM; + $this->template->resourceGroups = SrsResource::GROUPS; $this->template->permissionAccess = Permission::ACCESS; $this->template->permissionManage = Permission::MANAGE; diff --git a/app/AdminModule/Presenters/templates/Dashboard/default.latte b/app/AdminModule/Presenters/templates/Dashboard/default.latte index 079890a8d..3dedffe49 100644 --- a/app/AdminModule/Presenters/templates/Dashboard/default.latte +++ b/app/AdminModule/Presenters/templates/Dashboard/default.latte @@ -106,6 +106,26 @@ +
+
+ +
+
+
@@ -186,6 +206,11 @@
{_admin.configuration.menu.skautis}
+ + +
+ + {if $guestRole} +
+
+
+ {_web.application_content.login_required_begin} + {_web.application_content.login_required_link}{_web.application_content.login_required_end} +
+
+
+ + {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} +
+ +
+
+
+
+
+ {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()} +
+ +
+
+
+ {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 1b4de5a09..f91cc7191 100644 --- a/app/lang/admin.cs_CZ.neon +++ b/app/lang/admin.cs_CZ.neon @@ -40,6 +40,7 @@ menu: payments: "Platby" acl: "Role" mailing: "Mailing" + groups: "Skupiny" configuration: "Nastavení" help: "Nápověda" @@ -631,6 +632,25 @@ mailing: recipient_emails: "Příjemci - e-maily" automatic: "Automatický" +groups: + menu: + statuses: "Statusy" + groups: "Skupiny" + + status: + heading: "Statusy" + + group: + heading: "Skupiny" + + 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: seminar: "Seminář" @@ -641,6 +661,7 @@ configuration: place: "Místo" mailing: "Mailing" skautis: "SkautIS" + group: "Skupinová registrace" web: "Web" system: "Systém" subevents: "Podakce"