diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index c7b7d3f3c..acb2121d5 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -12,7 +12,7 @@ jobs: deploy: name: "Deploy to test-srs.skauting.cz" environment: test-srs.skauting.cz - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 container: image: skaut/lebeda:8.1 env: @@ -39,6 +39,7 @@ jobs: 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 diff --git a/.github/workflows/deploy-manual.yml b/.github/workflows/deploy-manual.yml index 0af3e4f7c..956b6a3b5 100644 --- a/.github/workflows/deploy-manual.yml +++ b/.github/workflows/deploy-manual.yml @@ -15,7 +15,7 @@ jobs: deploy: name: "Manual deploy to ${{ github.event.inputs.environment }}" environment: ${{ github.event.inputs.environment }} - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 container: image: skaut/lebeda:8.1 env: @@ -42,6 +42,7 @@ jobs: 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 diff --git a/.github/workflows/deploy-nsj-dev.yml b/.github/workflows/deploy-nsj-dev.yml new file mode 100644 index 000000000..9a00798a8 --- /dev/null +++ b/.github/workflows/deploy-nsj-dev.yml @@ -0,0 +1,73 @@ +name: deploy-nsj-dev + +on: + push: + branches: [nsj/master] + +concurrency: + group: environment-nsj-dev + +jobs: + deploy: + name: "Deploy to srs-dev.skauting.cz" + environment: srs-dev.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_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/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 16087a885..637294871 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -11,7 +11,7 @@ jobs: deploy: name: "Deploy to srs.skauting.cz" environment: srs.skauting.cz - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 container: image: skaut/lebeda:8.1 env: @@ -38,6 +38,7 @@ jobs: 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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 00186b065..ad770853d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,7 +7,7 @@ on: jobs: package: name: "Create release package" - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 container: image: skaut/lebeda:8.1 steps: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 77a8ef8ed..7914ed50b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,9 +2,9 @@ name: test on: push: - branches: [master] + branches: [master, nsj/master] pull_request: - branches: [master] + branches: [master, nsj/master] concurrency: group: test-${{ github.ref }} @@ -13,7 +13,7 @@ concurrency: jobs: workdir: name: "Build" - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 container: image: skaut/lebeda:8.1 steps: diff --git a/app/ActionModule/Presenters/PaymentPresenter.php b/app/ActionModule/Presenters/PaymentPresenter.php new file mode 100644 index 000000000..29ea542b8 --- /dev/null +++ b/app/ActionModule/Presenters/PaymentPresenter.php @@ -0,0 +1,34 @@ +paymentRepository->findNotPairedVs(); + + foreach ($notPairedPayments as $payment) { + $this->applicationService->pairPayment($payment); + } + + $response = new TextResponse(null); + $this->sendResponse($response); + } +} diff --git a/app/AdminModule/ConfigurationModule/Forms/SeminarFormFactory.php b/app/AdminModule/ConfigurationModule/Forms/SeminarFormFactory.php index 28c8957b1..0ebf16d1a 100644 --- a/app/AdminModule/ConfigurationModule/Forms/SeminarFormFactory.php +++ b/app/AdminModule/ConfigurationModule/Forms/SeminarFormFactory.php @@ -5,8 +5,10 @@ namespace App\AdminModule\ConfigurationModule\Forms; use App\AdminModule\Forms\BaseFormFactory; +use App\Model\Settings\Commands\SetSettingBoolValue; use App\Model\Settings\Commands\SetSettingDateValue; use App\Model\Settings\Commands\SetSettingStringValue; +use App\Model\Settings\Queries\SettingBoolValueQuery; use App\Model\Settings\Queries\SettingDateValueQuery; use App\Model\Settings\Queries\SettingStringValueQuery; use App\Model\Settings\Settings; @@ -71,6 +73,8 @@ public function create(): Form $seminarToDate->addRule([$this, 'validateSeminarToDate'], 'admin.configuration.seminar_to_date_before_from', [$seminarToDate, $seminarFromDate]); $editRegistrationTo->addRule([$this, 'validateEditRegistrationTo'], 'admin.configuration.edit_registration_to_after_from', [$editRegistrationTo, $seminarFromDate]); + $form->addCheckbox('groupRegistrationAllowed', 'povolit registraci nových skupin'); + $form->addSubmit('submit', 'admin.common.save'); $form->setDefaults([ @@ -78,6 +82,7 @@ public function create(): Form 'seminarFromDate' => $this->queryBus->handle(new SettingDateValueQuery(Settings::SEMINAR_FROM_DATE)), 'seminarToDate' => $this->queryBus->handle(new SettingDateValueQuery(Settings::SEMINAR_TO_DATE)), 'editRegistrationTo' => $this->queryBus->handle(new SettingDateValueQuery(Settings::EDIT_REGISTRATION_TO)), + 'groupRegistrationAllowed' => $this->queryBus->handle(new SettingBoolValueQuery(Settings::GROUP_REGISTRATION_ALLOWED)), ]); $form->onSuccess[] = [$this, 'processForm']; @@ -100,6 +105,7 @@ public function processForm(Form $form, stdClass $values): void $this->commandBus->handle(new SetSettingDateValue(Settings::SEMINAR_FROM_DATE, $values->seminarFromDate)); $this->commandBus->handle(new SetSettingDateValue(Settings::SEMINAR_TO_DATE, $values->seminarToDate)); $this->commandBus->handle(new SetSettingDateValue(Settings::EDIT_REGISTRATION_TO, $values->editRegistrationTo)); + $this->commandBus->handle(new SetSettingBoolValue(Settings::GROUP_REGISTRATION_ALLOWED, $values->groupRegistrationAllowed)); } /** diff --git a/app/AdminModule/Forms/AddRoleFormFactory.php b/app/AdminModule/Forms/AddRoleFormFactory.php index fd41dd690..79573c1ea 100644 --- a/app/AdminModule/Forms/AddRoleFormFactory.php +++ b/app/AdminModule/Forms/AddRoleFormFactory.php @@ -91,6 +91,9 @@ public function processForm(Form $form, stdClass $values): void $role->setFee($parent->getFee()); $role->setCapacity($parent->getCapacity()); $role->setMinimumAge($parent->getMinimumAge()); + $role->setMaximumAge($parent->getMaximumAge()); + $role->setMinimumAgeWarning($parent->getMinimumAgeWarning()); + $role->setMaximumAgeWarning($parent->getMaximumAgeWarning()); $role->setApprovedAfterRegistration($parent->isApprovedAfterRegistration()); $role->setSyncedWithSkautIS($parent->isSyncedWithSkautIS()); $role->setRegisterable($parent->isRegisterable()); diff --git a/app/AdminModule/Forms/EditRoleFormFactory.php b/app/AdminModule/Forms/EditRoleFormFactory.php index 2bac062fa..e14b327a6 100644 --- a/app/AdminModule/Forms/EditRoleFormFactory.php +++ b/app/AdminModule/Forms/EditRoleFormFactory.php @@ -109,6 +109,23 @@ public function create(int $id): Form ->addRule(Form::INTEGER, 'admin.acl.roles.minimum_age.error_format') ->addRule(Form::MIN, 'admin.acl.roles.minimum_age.error_low', 0); + $form->addText('minimumAgeMsg', 'admin.acl.roles.minimum_age.custom_msg_label') + ->setHtmlAttribute('data-toggle', 'tooltip') + ->setHtmlAttribute('data-placement', 'bottom') + ->setHtmlAttribute('title', $form->getTranslator()->translate('admin.acl.roles.minimum_age.custom_msg_note')); + + $form->addText('maximumAge', 'admin.acl.roles.maximum_age.label') + ->setHtmlAttribute('data-toggle', 'tooltip') + ->setHtmlAttribute('data-placement', 'bottom') + ->setHtmlAttribute('title', $form->getTranslator()->translate('admin.acl.roles.maximum_age.note')) + ->addRule(Form::INTEGER, 'admin.acl.roles.maximum_age.error_format') + ->addRule(Form::MIN, 'admin.acl.roles.maximum_age.error_low', 0); + + $form->addText('maximumAgeMsg', 'admin.acl.roles.maximum_age.custom_msg_label') + ->setHtmlAttribute('data-toggle', 'tooltip') + ->setHtmlAttribute('data-placement', 'bottom') + ->setHtmlAttribute('title', $form->getTranslator()->translate('admin.acl.roles.maximum_age.custom_msg_note')); + $form->addMultiSelect('permissions', 'admin.acl.roles_permissions', $this->preparePermissionsOptions()); $pagesOptions = $this->pageRepository->getPagesOptions(); @@ -164,6 +181,9 @@ public function create(int $id): Form 'feeFromSubevents' => $this->role->getFee() === null, 'fee' => $this->role->getFee() ?? 0, 'minimumAge' => $this->role->getMinimumAge(), + 'maximumAge' => $this->role->getMaximumAge(), + 'minimumAgeMsg' => $this->role->getMinimumAgeWarning(), + 'maximumAgeMsg' => $this->role->getMaximumAgeWarning(), 'permissions' => $this->permissionRepository->findPermissionsIds($this->role->getPermissions()), 'pages' => $this->pageRepository->findPagesSlugs($this->role->getPages()), 'redirectAfterLogin' => array_key_exists($redirectAfterLoginValue, $pagesOptions) ? $redirectAfterLoginValue : null, @@ -197,6 +217,9 @@ public function processForm(Form $form, stdClass $values): void $this->role->setCapacity($capacity); $this->role->setApprovedAfterRegistration($values->approvedAfterRegistration); $this->role->setMinimumAge($values->minimumAge); + $this->role->setMaximumAge($values->maximumAge); + $this->role->setMinimumAgeWarning($values->minimumAgeMsg); + $this->role->setMaximumAgeWarning($values->maximumAgeMsg); $this->role->setPermissions($this->permissionRepository->findPermissionsByIds($values->permissions)); $this->role->setPages($this->pageRepository->findPagesBySlugs($values->pages)); $this->role->setRedirectAfterLogin($values->redirectAfterLogin); diff --git a/app/AdminModule/PaymentsModule/Components/PaymentsGridControl.php b/app/AdminModule/PaymentsModule/Components/PaymentsGridControl.php index 1a6023f56..b78ab0618 100644 --- a/app/AdminModule/PaymentsModule/Components/PaymentsGridControl.php +++ b/app/AdminModule/PaymentsModule/Components/PaymentsGridControl.php @@ -91,6 +91,8 @@ public function createComponentPaymentsGrid(string $name): void $grid->addColumnText('pairedApplications', 'admin.payments.payments.paired_applications', 'pairedValidApplicationsText'); + $grid->addColumnText('pairedTroops', 'admin.payments.payments.paired_troops', 'pairedTroopsText'); + $grid->addColumnText('state', 'admin.payments.payments.state') ->setRenderer(fn (Payment $payment) => $this->translator->translate('common.payment_state.' . $payment->getState())) ->setFilterMultiSelect($this->preparePaymentStatesOptions()) @@ -173,10 +175,14 @@ public function handleDelete(int $id): void */ public function handleGeneratePaymentProofBank(int $id): void { - $this->session->getSection('srs')->applicationIds = Helpers::getIds( - $this->paymentRepository->findById($id)->getPairedApplications() - ); - $this->presenter->redirect(':Export:IncomeProof:applications'); + $payment = $this->paymentRepository->findById($id); + + if (! $payment->getPairedApplications()->isEmpty()) { + $this->session->getSection('srs')->applicationIds = Helpers::getIds($payment->getPairedApplications()); + $this->presenter->redirect(':Export:IncomeProof:applications'); + } elseif (! $payment->getPairedTroops()->isEmpty()) { + $this->presenter->redirect(':Export:TroopIncomeProof:troop', ['id' => $payment->getPairedTroops()->get(0)->getId()]); + } } /** diff --git a/app/AdminModule/PaymentsModule/Forms/EditPaymentFormFactory.php b/app/AdminModule/PaymentsModule/Forms/EditPaymentFormFactory.php index ef66e3721..9f6ea7182 100644 --- a/app/AdminModule/PaymentsModule/Forms/EditPaymentFormFactory.php +++ b/app/AdminModule/PaymentsModule/Forms/EditPaymentFormFactory.php @@ -8,6 +8,7 @@ use App\Model\Application\Repositories\ApplicationRepository; use App\Model\Payment\Payment; use App\Model\Payment\Repositories\PaymentRepository; +use App\Model\User\Repositories\TroopRepository; use App\Model\User\Repositories\UserRepository; use App\Services\ApplicationService; use Nette; @@ -33,6 +34,7 @@ public function __construct( private PaymentRepository $paymentRepository, private ApplicationRepository $applicationRepository, private UserRepository $userRepository, + private TroopRepository $troopRepository, private ApplicationService $applicationService ) { } @@ -44,6 +46,9 @@ public function create(int $id): Form { $this->payment = $this->paymentRepository->findById($id); + $pairedValidApplications = $this->payment->getPairedValidApplications(); + $pairedTroops = $this->payment->getPairedTroops(); + $form = $this->baseFormFactory->create(); $form->addHidden('id'); @@ -55,7 +60,19 @@ public function create(int $id): Form $inputVariableSymbol = $form->addText('variableSymbol', 'admin.payments.payments.variable_symbol'); - $inputPairedApplication = $form->addMultiSelect('pairedApplications', 'admin.payments.payments.paired_applications', $this->applicationRepository->getApplicationsVariableSymbolsOptions()) + $form->addMultiSelect( + 'pairedApplications', + 'admin.payments.payments.paired_applications', + $this->applicationRepository->getWaitingForPaymentOrPairedApplicationsVariableSymbolsOptions($pairedValidApplications) + ) + ->setHtmlAttribute('class', 'datagrid-multiselect') + ->setHtmlAttribute('data-live-search', 'true'); + + $form->addMultiSelect( + 'pairedTroops', + 'admin.payments.payments.paired_troops', + $this->troopRepository->getWaitingForPaymentOrPairedTroopsVariableSymbolsOptions($pairedTroops) + ) ->setHtmlAttribute('class', 'datagrid-multiselect') ->setHtmlAttribute('data-live-search', 'true'); @@ -81,18 +98,13 @@ public function create(int $id): Form $inputVariableSymbol->setDisabled(); } - $pairedValidApplications = $this->payment->getPairedValidApplications(); - - $inputPairedApplication->setItems( - $this->applicationRepository->getWaitingForPaymentOrPairedApplicationsVariableSymbolsOptions($pairedValidApplications) - ); - $form->setDefaults([ 'id' => $id, 'date' => $this->payment->getDate(), 'amount' => $this->payment->getAmount(), 'variableSymbol' => $this->payment->getVariableSymbol(), 'pairedApplications' => $this->applicationRepository->findApplicationsIds($pairedValidApplications), + 'pairedTroops' => $this->troopRepository->findTroopsIds($pairedTroops), ]); $form->onSuccess[] = [$this, 'processForm']; @@ -111,8 +123,9 @@ public function processForm(Form $form, stdClass $values): void $loggedUser = $this->userRepository->findById($form->getPresenter()->user->id); $pairedApplications = $this->applicationRepository->findApplicationsByIds($values->pairedApplications); + $pairedTroops = $this->troopRepository->findTroopsByIds($values->pairedTroops); - $this->applicationService->updatePayment($this->payment, $values->date, $values->amount, $values->variableSymbol, $pairedApplications, $loggedUser); + $this->applicationService->updatePayment($this->payment, $values->date, $values->amount, $values->variableSymbol, $pairedApplications, $pairedTroops, $loggedUser); } } } diff --git a/app/AdminModule/Presenters/templates/Dashboard/default.latte b/app/AdminModule/Presenters/templates/Dashboard/default.latte index 3ed3bb43b..eb825a122 100644 --- a/app/AdminModule/Presenters/templates/Dashboard/default.latte +++ b/app/AdminModule/Presenters/templates/Dashboard/default.latte @@ -67,8 +67,18 @@
{_admin.menu.users}
- -
{_admin.menu.users} +
+
{_admin.users.menu.persons} +
+
+
+ +
{_admin.users.menu.patrols} +
+
+
+ +
{_admin.users.menu.troops}
@@ -202,4 +212,4 @@ -{/block} \ No newline at end of file +{/block} diff --git a/app/AdminModule/Presenters/templates/Users/groups.latte b/app/AdminModule/Presenters/templates/Users/groups.latte new file mode 100644 index 000000000..6ef9b4c92 --- /dev/null +++ b/app/AdminModule/Presenters/templates/Users/groups.latte @@ -0,0 +1,8 @@ +{block main} + +

Skupiny

+ + {control groupsGrid} +{/block} diff --git a/app/AdminModule/Presenters/templates/Users/patrols.latte b/app/AdminModule/Presenters/templates/Users/patrols.latte new file mode 100644 index 000000000..38663b633 --- /dev/null +++ b/app/AdminModule/Presenters/templates/Users/patrols.latte @@ -0,0 +1,7 @@ +{block main} +

Družiny

+ + {control patrolsGrid} +{/block} diff --git a/app/AdminModule/Presenters/templates/includes/main_menu.latte b/app/AdminModule/Presenters/templates/includes/main_menu.latte index 68f22ea40..08f700d77 100644 --- a/app/AdminModule/Presenters/templates/includes/main_menu.latte +++ b/app/AdminModule/Presenters/templates/includes/main_menu.latte @@ -23,8 +23,11 @@ {_admin.menu.program} -
  • - {_admin.menu.users} +
  • + {_admin.menu.users}
  • diff --git a/app/AdminModule/ProgramModule/Components/ProgramAttendeesGridControl.php b/app/AdminModule/ProgramModule/Components/ProgramAttendeesGridControl.php index 32912a2ee..9a4f338ff 100644 --- a/app/AdminModule/ProgramModule/Components/ProgramAttendeesGridControl.php +++ b/app/AdminModule/ProgramModule/Components/ProgramAttendeesGridControl.php @@ -149,7 +149,7 @@ public function createComponentProgramAttendeesGrid(string $name): void $grid->setDefaultFilter(['attends' => 'yes'], false); if ($user->isAllowed(SrsResource::USERS, Permission::MANAGE)) { - $grid->addAction('detail', 'admin.common.detail', ':Admin:Users:detail') + $grid->addAction('detail', 'admin.common.detail', ':Admin:Users:Users:detail') ->setClass('btn btn-xs btn-primary') ->addAttributes(['target' => '_blank']); } diff --git a/app/AdminModule/Components/ApplicationsGridControl.php b/app/AdminModule/UsersModule/Components/ApplicationsGridControl.php similarity index 99% rename from app/AdminModule/Components/ApplicationsGridControl.php rename to app/AdminModule/UsersModule/Components/ApplicationsGridControl.php index 30488b6bd..45bfbb790 100644 --- a/app/AdminModule/Components/ApplicationsGridControl.php +++ b/app/AdminModule/UsersModule/Components/ApplicationsGridControl.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\AdminModule\Components; +namespace App\AdminModule\UsersModule\Components; use App\Model\Application\Application; use App\Model\Application\Repositories\ApplicationRepository; diff --git a/app/AdminModule/Components/IApplicationsGridControlFactory.php b/app/AdminModule/UsersModule/Components/IApplicationsGridControlFactory.php similarity index 82% rename from app/AdminModule/Components/IApplicationsGridControlFactory.php rename to app/AdminModule/UsersModule/Components/IApplicationsGridControlFactory.php index 827874ad0..41deaf012 100644 --- a/app/AdminModule/Components/IApplicationsGridControlFactory.php +++ b/app/AdminModule/UsersModule/Components/IApplicationsGridControlFactory.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\AdminModule\Components; +namespace App\AdminModule\UsersModule\Components; /** * Factory komponenty pro správu přihlášek. diff --git a/app/AdminModule/UsersModule/Components/IPatrolsGridControlFactory.php b/app/AdminModule/UsersModule/Components/IPatrolsGridControlFactory.php new file mode 100644 index 000000000..9c77a6562 --- /dev/null +++ b/app/AdminModule/UsersModule/Components/IPatrolsGridControlFactory.php @@ -0,0 +1,16 @@ +sessionSection = $session->getSection('srs'); + } + + /** + * Vykreslí komponentu. + */ + public function render(): void + { + $this->template->setFile(__DIR__ . '/templates/patrols_grid.latte'); + $this->template->render(); + } + + /** + * Vytvoří komponentu. + * + * @throws Throwable + * @throws DataGridColumnStatusException + * @throws DataGridException + */ + public function createComponentPatrolsGrid(string $name): DataGrid + { + $grid = new DataGrid($this, $name); + $grid->setTranslator($this->translator); + $grid->setDataSource($this->patrolRepository->createQueryBuilder('p')->where('p.confirmed = true')); + $grid->setDefaultSort(['displayName' => 'ASC']); + $grid->setColumnsHideable(); + $grid->setItemsPerPageList([25, 50, 100, 250, 500]); + $grid->setStrictSessionFilterValues(false); + + $grid->addGroupAction('Export seznamu družin') + ->onSelect[] = [$this, 'groupExportPatrols']; + + $grid->addColumnText('name', 'Název') + ->setSortable() + ->setFilterText(); + + $grid->addColumnText('troop', 'Skupina') + ->setRenderer(function (Patrol $p) { + $troop = $p->getTroop(); + + return Html::el('a')->setAttribute('href', $this->getPresenter()->link('Troops:detail', $troop->getId()))->setText($troop->getName()); + }); + + $grid->addColumnDateTime('created', 'Datum založení') + ->setRenderer(static function (Patrol $p) { + $date = $p->getTroop()->getApplicationDate(); + + return $date ? $date->format(Helpers::DATETIME_FORMAT) : ''; + }); + + $grid->addColumnNumber('userRoles', 'Počet osob') + ->setRenderer(static fn (Patrol $p) => count($p->getUsersRoles())); // je to správné číslo? + +// $grid->addAction('detail', 'admin.common.detail', 'Patrols:detail') // destinace +// ->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('Opravdu chcete družinu odstranit?'), + ]); + + return $grid; + } + + /** + * Zpracuje odstranění družiny. + * + * @throws AbortException + */ + public function handleDelete(int $id): void + { + $patrol = $this->patrolRepository->findById($id); + $this->commandBus->handle(new RemovePatrol($patrol)); + $p = $this->getPresenter(); + $p->flashMessage('Družina byla úspěšně odstraněna.', 'success'); + $p->redirect('this'); + } + + /** + * Hromadně vyexportuje seznam družin. + * + * @param int[] $ids + * + * @throws AbortException + */ + public function groupExportPatrols(array $ids): void + { + $this->sessionSection->patrolIds = $ids; + $this->redirect('exportpatrols'); + } + + /** + * Zpracuje export seznamu družin. + * + * @throws AbortException + * @throws Exception + */ + public function handleExportPatrols(): void + { + $ids = $this->session->getSection('srs')->patrolIds; + + $patrols = $this->patrolRepository->findPatrolsByIds($ids); + + $response = $this->excelExportService->exportPatrolsList($patrols, 'seznam-druzin.xlsx'); + + $this->getPresenter()->sendResponse($response); + } +} diff --git a/app/AdminModule/UsersModule/Components/TroopsGridControl.php b/app/AdminModule/UsersModule/Components/TroopsGridControl.php new file mode 100644 index 000000000..bdc1bffb4 --- /dev/null +++ b/app/AdminModule/UsersModule/Components/TroopsGridControl.php @@ -0,0 +1,235 @@ +sessionSection = $session->getSection('srs'); + } + + /** + * Vykreslí komponentu. + */ + public function render(): void + { + $this->template->setFile(__DIR__ . '/templates/troops_grid.latte'); + $this->template->render(); + } + + /** + * Vytvoří komponentu. + * + * @throws Throwable + * @throws DataGridColumnStatusException + * @throws DataGridException + */ + public function createComponentPatrolsGrid(string $name): DataGrid + { + $grid = new DataGrid($this, $name); + $grid->setTranslator($this->translator); + $grid->setDataSource($this->troopRepository->createQueryBuilder('p')); + $grid->setDefaultSort(['displayName' => 'ASC']); + $grid->setColumnsHideable(); + $grid->setItemsPerPageList([25, 50, 100, 250, 500]); + $grid->setStrictSessionFilterValues(false); + + $grid->addGroupAction('Export seznamu skupin') + ->onSelect[] = [$this, 'groupExportTroops']; + + $grid->addToolbarButton('exportNsjTroops', 'Export NSJ - skupiny'); + + $grid->addColumnText('name', 'Název') + ->setSortable() + ->setFilterText(); + + $grid->addColumnText('state', 'Stav') + ->setSortable() + ->setRenderer(fn ($t) => $this->translator->translate('common.application_state.' . $t->getState())) + ->setFilterText(); + + $grid->addColumnText('variableSymbol', 'Variabilní symbol', 'variableSymbolText') + ->setSortable() + ->setSortableCallback(static function (QueryBuilder $qb, array $sort): void { + $sortRev = $sort['variableSymbolText'] === 'DESC' ? 'DESC' : 'ASC'; + $qb->join('p.variableSymbol', 'pVS') + ->orderBy('pVS.variableSymbol', $sortRev); + }) + ->setFilterText() + ->setCondition(static function (QueryBuilder $qb, string $value): void { + $qb->join('p.variableSymbol', 'pVS') + ->andWhere('pVS.variableSymbol LIKE :variableSymbol') + ->setParameter(':variableSymbol', '%' . $value . '%'); + }); + + $grid->addColumnText('leader', 'Vedoucí') + ->setRenderer(function (Troop $t) { + $leader = $t->getLeader(); + + return Html::el('a')->setAttribute('href', $this->getPresenter()->link('Users:detail', $leader->getId()))->setText($leader->getDisplayName()); + }); + + $grid->addColumnText('leaderEmail', 'E-mail vedoucího') + ->setRenderer(static function (Troop $t) { + $email = $t->getLeader()->getEmail(); + + return Html::el('a')->href('mailto:' . $email)->setText($email); + }); + + $grid->addColumnDateTime('applicationDate', 'Datum založení') + ->setRenderer(static function (Troop $t) { + $date = $t->getApplicationDate(); + + return $date ? $date->format(Helpers::DATETIME_FORMAT) : ''; + }) + ->setSortable(); + + $grid->addColumnNumber('fee', 'Cena')->setSortable()->setFilterText(); + + $grid->addColumnDateTime('maturityDate', 'Datum splatnosti') + ->setFormat(Helpers::DATE_FORMAT) + ->setSortable(); + + $grid->addColumnDateTime('paymentDate', 'Datum platby') + ->setFormat(Helpers::DATE_FORMAT) + ->setSortable(); + + $grid->addColumnText('pairingCode', 'Kód jamoddílu') + ->setFilterText(); + + $grid->addColumnNumber('numPersons', '# osob') +// ->setSortableCallback(static fn($qb,$vals) =>sort($vals)) + ->setRenderer(static fn (Troop $p) => $p->countUsersInRoles([Role::PATROL_LEADER, Role::LEADER, Role::ESCORT, Role::ATTENDEE])); + + $grid->addColumnNumber('numChilder', '# rádců') +// ->setSortableCallback(static fn($qb,$vals) =>sort($vals)) + ->setRenderer(static fn (Troop $p) => $p->countUsersInRoles([Role::PATROL_LEADER])); + + $grid->addColumnNumber('numAdults', '# dospělých') +// ->setSortableCallback(static fn($qb,$vals) =>sort($vals)) + ->setRenderer(static fn (Troop $p) => $p->countUsersInRoles([Role::LEADER, Role::ESCORT])); + + $grid->addColumnNumber('numPatrols', '# družin') +// ->setSortableCallback(static fn($qb,$vals) =>sort($vals)) + ->setRenderer(static fn (Troop $p) => count($p->getConfirmedPatrols())); + + $grid->addAction('generatePaymentProof', 'Stáhnout potvzrení o přijetí platby', 'generatePaymentProof'); + $grid->allowRowsAction('generatePaymentProof', static fn (Troop $troop) => $troop->getPaymentDate() !== null); + + $grid->addAction('detail', 'admin.common.detail', 'Troops: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('Opravdu chcete skupinu odstranit?'), + ]); + + return $grid; + } + + /** + * Zpracuje odstranění skupiny. + * + * @throws AbortException + */ + public function handleDelete(int $id): void + { + $troop = $this->troopRepository->findById($id); + $this->commandBus->handle(new RemoveTroop($troop)); + $p = $this->getPresenter(); + $p->flashMessage('Skupina byla úspěšně odstraněna.', 'success'); + $p->redirect('this'); + } + + /** + * Vygeneruje potvrzení o přijetí platby. + * + * @throws AbortException + */ + public function handleGeneratePaymentProof(int $id): void + { + $this->presenter->redirect(':Export:TroopIncomeProof:troop', ['id' => $id]); + } + + /** + * Hromadně vyexportuje seznam skupin. + * + * @param int[] $ids + * + * @throws AbortException + */ + public function groupExportTroops(array $ids): void + { + $this->sessionSection->troopIds = $ids; + $this->redirect('exporttroops'); + } + + /** + * Zpracuje export seznamu skupin. + * + * @throws AbortException + * @throws Exception + */ + public function handleExportTroops(): void + { + $ids = $this->session->getSection('srs')->troopIds; + + $troops = $this->troopRepository->findTroopsByIds($ids); + + $response = $this->excelExportService->exportTroopsList($troops, 'seznam-skupin.xlsx'); + + $this->getPresenter()->sendResponse($response); + } + + public function handleExportNsjTroops(): void + { + $troops = $this->queryBus->handle(new TroopsByStateQuery(TroopApplicationState::PAID)); + + $response = $this->excelExportService->exportNsjTroops($troops, 'nsj-skupiny.xlsx'); + + $this->getPresenter()->sendResponse($response); + } +} diff --git a/app/AdminModule/Components/UsersGridControl.php b/app/AdminModule/UsersModule/Components/UsersGridControl.php similarity index 91% rename from app/AdminModule/Components/UsersGridControl.php rename to app/AdminModule/UsersModule/Components/UsersGridControl.php index 1d8181ac8..1157aea43 100644 --- a/app/AdminModule/Components/UsersGridControl.php +++ b/app/AdminModule/UsersModule/Components/UsersGridControl.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\AdminModule\Components; +namespace App\AdminModule\UsersModule\Components; use App\Model\Acl\Repositories\RoleRepository; use App\Model\Acl\Role; @@ -19,9 +19,11 @@ use App\Model\Enums\ApplicationState; use App\Model\Enums\PaymentType; use App\Model\Enums\SkautIsEventType; +use App\Model\Enums\TroopApplicationState; use App\Model\Settings\Queries\SettingIntValueQuery; use App\Model\Settings\Queries\SettingStringValueQuery; use App\Model\Settings\Settings; +use App\Model\User\Queries\TroopsByStateQuery; use App\Model\User\Repositories\UserRepository; use App\Model\User\User; use App\Services\AclService; @@ -30,6 +32,7 @@ use App\Services\QueryBus; use App\Services\SkautIsEventEducationService; use App\Services\SkautIsEventGeneralService; +use App\Services\SkautIsService; use App\Services\SubeventService; use App\Services\UserService; use App\Utils\Helpers; @@ -46,6 +49,7 @@ use Nette\Localization\Translator; use Nette\Utils\ArrayHash; use Nette\Utils\Html; +use Skaut\Skautis\Wsdl\WsdlException; use Throwable; use Ublaboo\DataGrid\DataGrid; use Ublaboo\DataGrid\Exception\DataGridColumnStatusException; @@ -72,6 +76,7 @@ public function __construct( private AclService $aclService, private ApplicationService $applicationService, private UserService $userService, + private SkautIsService $skautIsService, private SkautIsEventEducationService $skautIsEventEducationService, private SkautIsEventGeneralService $skautIsEventGeneralService, private SubeventService $subeventService @@ -150,6 +155,13 @@ public function createComponentUsersGrid(string $name): DataGrid $grid->addGroupAction('admin.users.users_group_action_export_schedules') ->onSelect[] = [$this, 'groupExportSchedules']; + $grid->addGroupAction('Načíst členství ze skautIS (admin)') + ->onSelect[] = [$this, 'groupUpdateMembership']; + + $grid->addToolbarButton('exportNsjAttendees', 'Export NSJ - účastníci'); + + $grid->addToolbarButton('exportNsjOthers', 'Export NSJ - ostatní'); + $grid->addColumnText('displayName', 'admin.users.users_name') ->setSortable() ->setFilterText(); @@ -166,6 +178,20 @@ public function createComponentUsersGrid(string $name): DataGrid ->setParameter('rids', (array) $values); }); + $grid->addColumnText('groupRoles', 'Skupinové role', 'groupRolesText') + ->setFilterMultiSelect($this->aclService->getRolesWithoutRolesOptions([Role::GUEST, Role::UNAPPROVED])) + ->setCondition(static function (QueryBuilder $qb, ArrayHash $values): void { + $qb->join('u.groupRoles', 'uGR') + ->join('uGR.role', 'uGRR') + ->leftJoin('uGR.troop', 'uGRT') + ->leftJoin('uGR.patrol', 'uGRP') + ->leftJoin('uGRP.troop', 'uGRPT') + ->andWhere('uGRR.id IN (:grids)') + ->andWhere('(uGRT.state IS NULL OR uGRT.state != :tas) AND (uGRPT.state IS NULL OR uGRPT.state != :tas)') + ->setParameter('grids', (array) $values) + ->setParameter('tas', TroopApplicationState::DRAFT); + }); + $grid->addColumnText('subevents', 'admin.users.users_subevents', 'subeventsText') ->setFilterMultiSelect($this->subeventService->getSubeventsOptions()) ->setCondition(static function (QueryBuilder $qb, ArrayHash $values): void { @@ -712,6 +738,33 @@ public function groupExportUsers(array $ids): void $this->redirect('exportusers'); } + /** + * @param int[] $ids + */ + public function groupUpdateMembership(array $ids): void + { + $users = $this->userRepository->findUsersByIds($ids); + $errors = 0; + + foreach ($users as $user) { + try { + $membership = $this->skautIsService->getValidMembership($user->getSkautISPersonId()); + $user->setUnit($membership?->RegistrationNumber); + $this->userRepository->save($user); + } catch (WsdlException $e) { + $errors++; + } + } + + if ($errors > 0) { + $this->getPresenter()->flashMessage('Členství některých účastníků se nepodařilo načíst (oprávnění).', 'warning'); + } else { + $this->getPresenter()->flashMessage('Členství byla úspěšně načtena.', 'success'); + } + + $this->reload(); + } + /** * Zpracuje export seznamu uživatelů. * @@ -820,6 +873,27 @@ public function handleExportSchedules(): void $this->getPresenter()->sendResponse($response); } + public function handleExportNsjAttendees(): void + { + $troops = $this->queryBus->handle(new TroopsByStateQuery(TroopApplicationState::PAID)); + + $response = $this->excelExportService->exportNsjAttendees($troops, 'nsj-ucastnici.xlsx'); + + $this->getPresenter()->sendResponse($response); + } + + public function handleExportNsjOthers(): void + { + $nonRegisteredRole = $this->roleRepository->findBySystemName(Role::NONREGISTERED); + $users = $this->userRepository->findAll() + ->filter(static fn (User $u) => $u->getRoles()->count() > 0 && ! $u->isInRole($nonRegisteredRole)) + ->toArray(); + + $response = $this->excelExportService->exportNsjOthers($users, 'nsj-ostatni.xlsx'); + + $this->getPresenter()->sendResponse($response); + } + /** * Vrátí platební metody jako možnosti pro select. Bez prázdné možnosti. * diff --git a/app/AdminModule/Components/templates/applications_grid.latte b/app/AdminModule/UsersModule/Components/templates/applications_grid.latte similarity index 100% rename from app/AdminModule/Components/templates/applications_grid.latte rename to app/AdminModule/UsersModule/Components/templates/applications_grid.latte diff --git a/app/AdminModule/Components/templates/applications_grid_detail.latte b/app/AdminModule/UsersModule/Components/templates/applications_grid_detail.latte similarity index 100% rename from app/AdminModule/Components/templates/applications_grid_detail.latte rename to app/AdminModule/UsersModule/Components/templates/applications_grid_detail.latte diff --git a/app/AdminModule/UsersModule/Components/templates/patrols_grid.latte b/app/AdminModule/UsersModule/Components/templates/patrols_grid.latte new file mode 100644 index 000000000..3f5cf26a1 --- /dev/null +++ b/app/AdminModule/UsersModule/Components/templates/patrols_grid.latte @@ -0,0 +1 @@ +{control patrolsGrid} diff --git a/app/AdminModule/UsersModule/Components/templates/troops_grid.latte b/app/AdminModule/UsersModule/Components/templates/troops_grid.latte new file mode 100644 index 000000000..3f5cf26a1 --- /dev/null +++ b/app/AdminModule/UsersModule/Components/templates/troops_grid.latte @@ -0,0 +1 @@ +{control patrolsGrid} diff --git a/app/AdminModule/Components/templates/users_grid.latte b/app/AdminModule/UsersModule/Components/templates/users_grid.latte similarity index 100% rename from app/AdminModule/Components/templates/users_grid.latte rename to app/AdminModule/UsersModule/Components/templates/users_grid.latte diff --git a/app/AdminModule/Forms/AddLectorFormFactory.php b/app/AdminModule/UsersModule/Forms/AddLectorFormFactory.php similarity index 98% rename from app/AdminModule/Forms/AddLectorFormFactory.php rename to app/AdminModule/UsersModule/Forms/AddLectorFormFactory.php index 9bbcbd9c1..73bfd9d90 100644 --- a/app/AdminModule/Forms/AddLectorFormFactory.php +++ b/app/AdminModule/UsersModule/Forms/AddLectorFormFactory.php @@ -2,8 +2,9 @@ declare(strict_types=1); -namespace App\AdminModule\Forms; +namespace App\AdminModule\UsersModule\Forms; +use App\AdminModule\Forms\BaseFormFactory; use App\Model\Acl\Repositories\RoleRepository; use App\Model\Acl\Role; use App\Model\User\Repositories\UserRepository; diff --git a/app/AdminModule/Forms/EditUserPersonalDetailsFormFactory.php b/app/AdminModule/UsersModule/Forms/EditUserPersonalDetailsFormFactory.php similarity index 98% rename from app/AdminModule/Forms/EditUserPersonalDetailsFormFactory.php rename to app/AdminModule/UsersModule/Forms/EditUserPersonalDetailsFormFactory.php index 27de00856..594cca030 100644 --- a/app/AdminModule/Forms/EditUserPersonalDetailsFormFactory.php +++ b/app/AdminModule/UsersModule/Forms/EditUserPersonalDetailsFormFactory.php @@ -2,8 +2,9 @@ declare(strict_types=1); -namespace App\AdminModule\Forms; +namespace App\AdminModule\UsersModule\Forms; +use App\AdminModule\Forms\BaseFormFactory; use App\Model\User\Repositories\UserRepository; use App\Model\User\User; use App\Services\FilesService; diff --git a/app/AdminModule/Forms/EditUserSeminarFormFactory.php b/app/AdminModule/UsersModule/Forms/EditUserSeminarFormFactory.php similarity index 99% rename from app/AdminModule/Forms/EditUserSeminarFormFactory.php rename to app/AdminModule/UsersModule/Forms/EditUserSeminarFormFactory.php index ce0683ba2..4ca83eb67 100644 --- a/app/AdminModule/Forms/EditUserSeminarFormFactory.php +++ b/app/AdminModule/UsersModule/Forms/EditUserSeminarFormFactory.php @@ -2,8 +2,9 @@ declare(strict_types=1); -namespace App\AdminModule\Forms; +namespace App\AdminModule\UsersModule\Forms; +use App\AdminModule\Forms\BaseFormFactory; use App\Model\Acl\Repositories\RoleRepository; use App\Model\Acl\Role; use App\Model\CustomInput\CustomCheckbox; diff --git a/app/AdminModule/UsersModule/Presenters/PatrolsPresenter.php b/app/AdminModule/UsersModule/Presenters/PatrolsPresenter.php new file mode 100644 index 000000000..33a14ef43 --- /dev/null +++ b/app/AdminModule/UsersModule/Presenters/PatrolsPresenter.php @@ -0,0 +1,26 @@ +patrolsGridControlFactory->create(); + } +} diff --git a/app/AdminModule/UsersModule/Presenters/TroopsPresenter.php b/app/AdminModule/UsersModule/Presenters/TroopsPresenter.php new file mode 100644 index 000000000..7cc7912c8 --- /dev/null +++ b/app/AdminModule/UsersModule/Presenters/TroopsPresenter.php @@ -0,0 +1,36 @@ +troopsGridControlFactory->create(); + } + + public function renderDetail(int $id): void + { + $troop = $this->troopRepository->findById($id); + $this->template->troop = $troop; + } +} diff --git a/app/AdminModule/UsersModule/Presenters/UsersBasePresenter.php b/app/AdminModule/UsersModule/Presenters/UsersBasePresenter.php new file mode 100644 index 000000000..033cbb365 --- /dev/null +++ b/app/AdminModule/UsersModule/Presenters/UsersBasePresenter.php @@ -0,0 +1,28 @@ +checkPermission(Permission::MANAGE); + } +} diff --git a/app/AdminModule/Presenters/UsersPresenter.php b/app/AdminModule/UsersModule/Presenters/UsersPresenter.php similarity index 90% rename from app/AdminModule/Presenters/UsersPresenter.php rename to app/AdminModule/UsersModule/Presenters/UsersPresenter.php index 155c8080a..eba13dff6 100644 --- a/app/AdminModule/Presenters/UsersPresenter.php +++ b/app/AdminModule/UsersModule/Presenters/UsersPresenter.php @@ -2,16 +2,15 @@ declare(strict_types=1); -namespace App\AdminModule\Presenters; - -use App\AdminModule\Components\ApplicationsGridControl; -use App\AdminModule\Components\IApplicationsGridControlFactory; -use App\AdminModule\Components\IUsersGridControlFactory; -use App\AdminModule\Components\UsersGridControl; -use App\AdminModule\Forms\AddLectorFormFactory; -use App\AdminModule\Forms\EditUserPersonalDetailsFormFactory; -use App\AdminModule\Forms\EditUserSeminarFormFactory; -use App\Model\Acl\Permission; +namespace App\AdminModule\UsersModule\Presenters; + +use App\AdminModule\UsersModule\Components\ApplicationsGridControl; +use App\AdminModule\UsersModule\Components\IApplicationsGridControlFactory; +use App\AdminModule\UsersModule\Components\IUsersGridControlFactory; +use App\AdminModule\UsersModule\Components\UsersGridControl; +use App\AdminModule\UsersModule\Forms\AddLectorFormFactory; +use App\AdminModule\UsersModule\Forms\EditUserPersonalDetailsFormFactory; +use App\AdminModule\UsersModule\Forms\EditUserSeminarFormFactory; use App\Model\Acl\Role; use App\Model\Acl\SrsResource; use App\Model\CustomInput\CustomInput; @@ -20,7 +19,6 @@ use App\Model\Enums\PaymentType; use App\Model\User\Queries\UserAttendsProgramsQuery; use App\Services\ApplicationService; -use App\Services\ExcelExportService; use Nette\Application\AbortException; use Nette\Application\UI\Form; use Nette\DI\Attributes\Inject; @@ -30,7 +28,7 @@ /** * Presenter obsluhující správu uživatelů. */ -class UsersPresenter extends AdminBasePresenter +class UsersPresenter extends UsersBasePresenter { protected string $resource = SrsResource::USERS; @@ -49,9 +47,6 @@ class UsersPresenter extends AdminBasePresenter #[Inject] public IApplicationsGridControlFactory $applicationsGridControlFactory; - #[Inject] - public ExcelExportService $excelExportService; - #[Inject] public CustomInputRepository $customInputRepository; @@ -65,8 +60,6 @@ public function startup(): void { parent::startup(); - $this->checkPermission(Permission::MANAGE); - $this->template->results = []; $this->template->editPersonalDetails = false; $this->template->editSeminar = false; diff --git a/app/AdminModule/UsersModule/Presenters/templates/@layout.latte b/app/AdminModule/UsersModule/Presenters/templates/@layout.latte new file mode 100644 index 000000000..1cc7452c7 --- /dev/null +++ b/app/AdminModule/UsersModule/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/UsersModule/Presenters/templates/Patrols/default.latte b/app/AdminModule/UsersModule/Presenters/templates/Patrols/default.latte new file mode 100644 index 000000000..125f2de60 --- /dev/null +++ b/app/AdminModule/UsersModule/Presenters/templates/Patrols/default.latte @@ -0,0 +1,4 @@ +{block main} +

    {_admin.users.patrols.heading}

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

    {_admin.users.troops.heading}

    + {control troopsGrid} +{/block} \ No newline at end of file diff --git a/app/AdminModule/UsersModule/Presenters/templates/Troops/detail.latte b/app/AdminModule/UsersModule/Presenters/templates/Troops/detail.latte new file mode 100644 index 000000000..464e8f240 --- /dev/null +++ b/app/AdminModule/UsersModule/Presenters/templates/Troops/detail.latte @@ -0,0 +1,93 @@ +{block main} +

    Detail skupiny: {$troop->getName()}

    + +

    Základní údaje

    +
    +
    +
    Stav
    +
    {_'common.application_state.' . $troop->getState()}
    +
    +
    +
    Variabilní symbol
    +
    {$troop->getVariableSymbolText()}
    +
    + +
    +
    Datum založení
    +
    {$troop->getApplicationDate()|date:'j. n. Y H:i'}
    +
    +
    +
    Cena
    +
    {$troop->getFee()}
    +
    +
    +
    Datum splatnosti
    +
    {$troop->getMaturityDate()|date:'j. n. Y'}
    +
    +
    +
    Datum platby
    +
    {$troop->getPaymentDate()|date:'j. n. Y'}
    +
    +
    +
    Kód jamooodílu
    +
    {$troop->getPairingCode()}
    +
    +
    + +

    Seznam členů

    + + + + + + + + + + + + + + + + + +
    JménoRoleDatum narozeníZdravotní údaje
    + + {$userRole->getUser()->getDisplayName()} + + {$userRole->getRole()->getName()}{$userRole->getUser()->getBirthdate()|date:"j. n. Y"}{$userRole->getUser()->getHealthInfo()}
    + +
    +

    Družina: {$patrol->getName()}

    + + + + + + + + + + + + + + + + + +
    JménoRoleDatum narozeníZdravotní údaje
    + + {$userRole->getUser()->getDisplayName()} + + {$userRole->getRole()->getName()}{$userRole->getUser()->getBirthdate()|date:"j. n. Y"}{$userRole->getUser()->getHealthInfo()}
    +
    +{/block} diff --git a/app/AdminModule/Presenters/templates/Users/add.latte b/app/AdminModule/UsersModule/Presenters/templates/Users/add.latte similarity index 100% rename from app/AdminModule/Presenters/templates/Users/add.latte rename to app/AdminModule/UsersModule/Presenters/templates/Users/add.latte diff --git a/app/AdminModule/Presenters/templates/Users/default.latte b/app/AdminModule/UsersModule/Presenters/templates/Users/default.latte similarity index 100% rename from app/AdminModule/Presenters/templates/Users/default.latte rename to app/AdminModule/UsersModule/Presenters/templates/Users/default.latte diff --git a/app/AdminModule/Presenters/templates/Users/detail.latte b/app/AdminModule/UsersModule/Presenters/templates/Users/detail.latte similarity index 100% rename from app/AdminModule/Presenters/templates/Users/detail.latte rename to app/AdminModule/UsersModule/Presenters/templates/Users/detail.latte diff --git a/app/AdminModule/Presenters/templates/Users/sidebar.latte b/app/AdminModule/UsersModule/Presenters/templates/Users/sidebar.latte similarity index 100% rename from app/AdminModule/Presenters/templates/Users/sidebar.latte rename to app/AdminModule/UsersModule/Presenters/templates/Users/sidebar.latte diff --git a/app/AdminModule/UsersModule/Presenters/templates/sidebar.latte b/app/AdminModule/UsersModule/Presenters/templates/sidebar.latte new file mode 100644 index 000000000..be6cca607 --- /dev/null +++ b/app/AdminModule/UsersModule/Presenters/templates/sidebar.latte @@ -0,0 +1,15 @@ +{block sidebar} +
    + +
    +{/block} \ No newline at end of file diff --git a/app/Commands/BackupDatabaseCommand.php b/app/Commands/BackupDatabaseCommand.php index 5ed52b505..6d2f3d6fe 100644 --- a/app/Commands/BackupDatabaseCommand.php +++ b/app/Commands/BackupDatabaseCommand.php @@ -50,8 +50,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $output->writeln('Database dump created successfully.'); return 0; - } catch (Throwable) { - $output->writeln('Database dump creation failed.'); + } catch (Throwable $e) { + $output->writeln('Database dump creation failed: ' . $e); return 1; } diff --git a/app/ExportModule/Presenters/TroopIncomeProofPresenter.php b/app/ExportModule/Presenters/TroopIncomeProofPresenter.php new file mode 100644 index 000000000..939cc12e5 --- /dev/null +++ b/app/ExportModule/Presenters/TroopIncomeProofPresenter.php @@ -0,0 +1,111 @@ +user->isLoggedIn()) { + throw new ForbiddenRequestException(); + } + } + + public function actionTroop(int $id): void + { + $troops = new ArrayCollection(); + $troop = $this->troopRepository->findById($id); + + if ($troop->getState() === TroopApplicationState::PAID) { + $troops->add($troop); + } + + $this->generateTroopIncomeProofs($troops); + } + + /** + * @param Collection $troops + * + * @throws AbortException + * @throws SettingsItemNotFoundException + * @throws Throwable + * @throws NonUniqueResultException + */ + private function generateTroopIncomeProofs(Collection $troops): void + { + $template = $this->createTemplate(); + assert($template instanceof Template); + $template->setFile(__DIR__ . '/templates/TroopIncomeProof/pdf.latte'); + + $template->troops = $troops; + $template->logo = $this->queryBus->handle(new SettingStringValueQuery(Settings::LOGO)); + $template->seminarName = $this->queryBus->handle(new SettingStringValueQuery(Settings::SEMINAR_NAME)); + $template->company = $this->queryBus->handle(new SettingStringValueQuery(Settings::COMPANY)); + $template->ico = $this->queryBus->handle(new SettingStringValueQuery(Settings::ICO)); + $template->accountNumber = $this->queryBus->handle(new SettingStringValueQuery(Settings::ACCOUNT_NUMBER)); + $template->accountant = $this->queryBus->handle(new SettingStringValueQuery(Settings::ACCOUNTANT)); + $template->date = (new DateTimeImmutable())->format(Helpers::DATE_FORMAT); + + $this->pdfResponse->setTemplate($template); + + $this->pdfResponse->documentTitle = 'potvrzeni-platby'; + $this->pdfResponse->pageFormat = 'A4'; + $this->pdfResponse->getMPDF()->SetProtection(['copy', 'print', 'print-highres'], '', bin2hex(random_bytes(40))); + + $this->sendResponse($this->pdfResponse); + } +} diff --git a/app/ExportModule/Presenters/templates/TroopIncomeProof/pdf.latte b/app/ExportModule/Presenters/templates/TroopIncomeProof/pdf.latte new file mode 100644 index 000000000..40f79d66c --- /dev/null +++ b/app/ExportModule/Presenters/templates/TroopIncomeProof/pdf.latte @@ -0,0 +1,56 @@ +{block content} + {foreach $troops as $troop} +
    +
    +

    {_export.income_proof.bank.heading}

    +
    +
    + +
    +
    + +
    +
    +
    + {$company|breakLines} +
    + {_export.income_proof.ico} {$ico} +
    +
    +
    + +
    +
    + {_export.income_proof.bank.purpose} {$seminarName} +
    +
    + +
    +
    + {_export.income_proof.bank.fee} {$troop->getFee()},00 {_export.income_proof.bank.fee_unit} + ({_export.income_proof.bank.fee_words} {$troop->getFeeWords()} {_export.income_proof.bank.fee_unit}) +
    +
    + +
    +
    + {_export.income_proof.bank.name} {$troop->getLeader()->getFirstName()} {$troop->getLeader()->getLastName()} + ({_export.income_proof.bank.address} {$troop->getLeader()->getStreet()}, {$troop->getLeader()->getPostcode()} {$troop->getLeader()->getCity()}), + skupina: {$troop->getName()} +
    +
    + +
    +
    + Zaplaceno z účtu: {$troop->getPayment()->getAccountNumber()}, variabilní symbol: {$troop->getPayment()->getVariableSymbol()} +
    +
    + +
    +
    + {_export.income_proof.bank.accountant} {$accountant} {_export.income_proof.bank.date} {$date} +
    +
    + {sep}{/sep} + {/foreach} +{/block} \ No newline at end of file diff --git a/app/Model/Acl/Queries/Handlers/RolesByTypeQueryHandler.php b/app/Model/Acl/Queries/Handlers/RolesByTypeQueryHandler.php new file mode 100644 index 000000000..0baa5431d --- /dev/null +++ b/app/Model/Acl/Queries/Handlers/RolesByTypeQueryHandler.php @@ -0,0 +1,25 @@ +roleRepository->findByType($query->getType()); + } +} diff --git a/app/Model/Acl/Queries/RolesByTypeQuery.php b/app/Model/Acl/Queries/RolesByTypeQuery.php new file mode 100644 index 000000000..ffed4e8c5 --- /dev/null +++ b/app/Model/Acl/Queries/RolesByTypeQuery.php @@ -0,0 +1,17 @@ +type; + } +} diff --git a/app/Model/Acl/Repositories/RoleRepository.php b/app/Model/Acl/Repositories/RoleRepository.php index 5391ba225..99670a246 100644 --- a/app/Model/Acl/Repositories/RoleRepository.php +++ b/app/Model/Acl/Repositories/RoleRepository.php @@ -53,6 +53,16 @@ public function findBySystemName(string $name): Role return $this->getRepository()->findOneBy(['systemName' => $name]); } + /** + * Vrací role podle typu. + * + * @return Role[] + */ + public function findByType(string $type): array + { + return $this->getRepository()->findBy(['type' => $type]); + } + /** * Vrací id naposledy přidané role. * diff --git a/app/Model/Acl/Role.php b/app/Model/Acl/Role.php index 099aca455..d9b1a28a7 100644 --- a/app/Model/Acl/Role.php +++ b/app/Model/Acl/Role.php @@ -6,6 +6,7 @@ use App\Model\Cms\Page; use App\Model\Cms\Tag; +use App\Model\Enums\RoleType; use App\Model\Program\Category; use App\Model\User\User; use DateTimeImmutable; @@ -67,6 +68,12 @@ class Role */ public const TEST = 'test'; + public const PATROL_LEADER = 'patrol_leader'; + + public const LEADER = 'leader'; + + public const ESCORT = 'escort'; + /** @var string[] */ public static array $roles = [ self::GUEST, @@ -77,6 +84,9 @@ class Role self::LECTOR, self::ORGANIZER, self::ADMIN, + self::PATROL_LEADER, + self::LEADER, + self::ESCORT, ]; #[ORM\Id] #[ORM\GeneratedValue] @@ -125,6 +135,12 @@ class Role #[ORM\Column(type: 'boolean')] protected bool $systemRole = true; + /** + * Typ role - individuální/družinová/skupinová. + */ + #[ORM\Column(type: 'string')] + protected string $type = RoleType::INDIVIDUAL; + /** * Registrovatelná role. Lze vybrat v přihlášce. */ @@ -174,6 +190,24 @@ class Role #[ORM\Column(type: 'integer')] protected int $minimumAge = 0; + /** + * Varování při příliš nízkém věku. + */ + #[ORM\Column(type: 'string', nullable: true)] + protected ?string $minimumAgeWarning; + + /** + * Maximální věk. + */ + #[ORM\Column(type: 'integer')] + protected int $maximumAge = 150; + + /** + * Varování při příliš vysokém věku. + */ + #[ORM\Column(type: 'string', nullable: true)] + protected ?string $maximumAgeWarning; + /** * Synchronizovat účastníky v roli se skautIS. */ @@ -375,6 +409,16 @@ public function setSystemRole(bool $systemRole): void $this->systemRole = $systemRole; } + public function getType(): string + { + return $this->type; + } + + public function setType(string $type): void + { + $this->type = $type; + } + public function isRegisterable(): bool { return $this->registerable; @@ -467,6 +511,36 @@ public function setMinimumAge(int $age): void $this->minimumAge = $age; } + public function getMinimumAgeWarning(): ?string + { + return $this->minimumAgeWarning; + } + + public function setMinimumAgeWarning(?string $minimumAgeWarning): void + { + $this->minimumAgeWarning = $minimumAgeWarning; + } + + public function getMaximumAge(): int + { + return $this->maximumAge; + } + + public function setMaximumAge(int $maximumAge): void + { + $this->maximumAge = $maximumAge; + } + + public function getMaximumAgeWarning(): ?string + { + return $this->maximumAgeWarning; + } + + public function setMaximumAgeWarning(?string $maximumAgeWarning): void + { + $this->maximumAgeWarning = $maximumAgeWarning; + } + public function isSyncedWithSkautIS(): bool { return $this->syncedWithSkautIS; diff --git a/app/Model/Application/Repositories/ApplicationRepository.php b/app/Model/Application/Repositories/ApplicationRepository.php index 0ef2e8daa..7f57859c9 100644 --- a/app/Model/Application/Repositories/ApplicationRepository.php +++ b/app/Model/Application/Repositories/ApplicationRepository.php @@ -135,19 +135,6 @@ public function findWaitingForPaymentOrPairedApplications(Collection $pairedAppl return $this->getRepository()->matching($criteria); } - /** - * @return string[] - */ - public function getApplicationsVariableSymbolsOptions(): array - { - $options = []; - foreach ($this->findValid() as $application) { - $options[$application->getId()] = $application->getUser()->getLastName() . ' ' . $application->getUser()->getFirstName() . ' (' . $application->getVariableSymbolText() . ' - ' . $application->getFee() . ')'; - } - - return $options; - } - /** * @param Collection $pairedApplications * @@ -157,7 +144,7 @@ public function getWaitingForPaymentOrPairedApplicationsVariableSymbolsOptions(C { $options = []; foreach ($this->findWaitingForPaymentOrPairedApplications($pairedApplications) as $application) { - $options[$application->getId()] = $application->getUser()->getLastName() . ' ' . $application->getUser()->getFirstName() . ' (' . $application->getVariableSymbolText() . ' - ' . $application->getFee() . ')'; + $options[$application->getId()] = $application->getUser()->getLastName() . ' ' . $application->getUser()->getFirstName() . ' (' . $application->getVariableSymbolText() . ' - ' . $application->getFee() . ' Kč)'; } return $options; diff --git a/app/Model/Cms/Content.php b/app/Model/Cms/Content.php index 72e176624..e2b5b2d40 100644 --- a/app/Model/Cms/Content.php +++ b/app/Model/Cms/Content.php @@ -25,6 +25,7 @@ 'text_content' => TextContent::class, 'document_content' => DocumentContent::class, 'application_content' => ApplicationContent::class, + 'troop_application_content' => TroopApplicationContent::class, 'html_content' => HtmlContent::class, 'faq_content' => FaqContent::class, 'news_content' => NewsContent::class, @@ -61,6 +62,11 @@ abstract class Content implements IContent */ public const APPLICATION = 'application'; + /** + * TroopApplicationContent. + */ + public const TROOP_APPLICATION = 'troop_application'; + /** * HtmlContent. */ @@ -140,6 +146,7 @@ abstract class Content implements IContent self::NEWS, self::DOCUMENT, self::APPLICATION, + self::TROOP_APPLICATION, self::PROGRAMS, self::CONTACT_FORM, self::FAQ, diff --git a/app/Model/Cms/TroopApplicationContent.php b/app/Model/Cms/TroopApplicationContent.php new file mode 100644 index 000000000..554b5c091 --- /dev/null +++ b/app/Model/Cms/TroopApplicationContent.php @@ -0,0 +1,17 @@ + + */ + #[ORM\OneToMany(targetEntity: Troop::class, mappedBy: 'payment', cascade: ['persist'])] + protected Collection $pairedTroops; + /** * Stav platby. */ @@ -86,6 +95,7 @@ class Payment public function __construct() { $this->pairedApplications = new ArrayCollection(); + $this->pairedTroops = new ArrayCollection(); } public function getId(): ?int @@ -221,6 +231,21 @@ public function getPairedValidApplicationsText(): string return implode(', ', $usersTexts); } + /** + * @return Collection + */ + public function getPairedTroops(): Collection + { + return $this->pairedTroops; + } + + public function getPairedTroopsText(): string + { + $pairedNames = $this->getPairedTroops()->map(static fn (Troop $troop) => $troop->getName())->toArray(); + + return implode(', ', $pairedNames); + } + public function getState(): string { return $this->state; diff --git a/app/Model/Payment/Repositories/PaymentRepository.php b/app/Model/Payment/Repositories/PaymentRepository.php index f64df21ff..2aae51c75 100644 --- a/app/Model/Payment/Repositories/PaymentRepository.php +++ b/app/Model/Payment/Repositories/PaymentRepository.php @@ -4,8 +4,11 @@ namespace App\Model\Payment\Repositories; +use App\Model\Enums\PaymentState; use App\Model\Infrastructure\Repositories\AbstractRepository; use App\Model\Payment\Payment; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\EntityManagerInterface; /** @@ -34,6 +37,14 @@ public function findByTransactionId(string $transactionId): ?Payment return $this->getRepository()->findOneBy(['transactionId' => $transactionId]); } + /** + * @return Collection + */ + public function findNotPairedVs(): Collection + { + return new ArrayCollection($this->getRepository()->findBy(['state' => PaymentState::NOT_PAIRED_VS])); + } + /** * Uloží platbu. */ diff --git a/app/Model/Settings/Settings.php b/app/Model/Settings/Settings.php index 255cde18f..de5e9771d 100644 --- a/app/Model/Settings/Settings.php +++ b/app/Model/Settings/Settings.php @@ -234,6 +234,11 @@ class Settings */ public const CONTACT_FORM_GUESTS_ALLOWED = 'contact_form_guests_allowed'; + /** + * Povolit registraci skupiny. + */ + public const GROUP_REGISTRATION_ALLOWED = 'group_registration_allowed'; + /** * Název položky nastavení. */ diff --git a/app/Model/User/Commands/ConfirmPatrol.php b/app/Model/User/Commands/ConfirmPatrol.php new file mode 100644 index 000000000..386bd06f5 --- /dev/null +++ b/app/Model/User/Commands/ConfirmPatrol.php @@ -0,0 +1,18 @@ +patrolId; + } +} diff --git a/app/Model/User/Commands/ConfirmTroop.php b/app/Model/User/Commands/ConfirmTroop.php new file mode 100644 index 000000000..e1332a9d1 --- /dev/null +++ b/app/Model/User/Commands/ConfirmTroop.php @@ -0,0 +1,24 @@ +troop_id; + } + + public function getPairedTroopCode(): ?string + { + return $this->pairedTroopCode; + } +} diff --git a/app/Model/User/Commands/Handlers/ConfirmPatrolHandler.php b/app/Model/User/Commands/Handlers/ConfirmPatrolHandler.php new file mode 100644 index 000000000..54bf1564e --- /dev/null +++ b/app/Model/User/Commands/Handlers/ConfirmPatrolHandler.php @@ -0,0 +1,24 @@ +patrolRepository->findById($command->getPatrolId()); + $patrol->setConfirmed(true); + $this->patrolRepository->save($patrol); + } +} diff --git a/app/Model/User/Commands/Handlers/ConfirmTroopHandler.php b/app/Model/User/Commands/Handlers/ConfirmTroopHandler.php new file mode 100644 index 000000000..854d39b3c --- /dev/null +++ b/app/Model/User/Commands/Handlers/ConfirmTroopHandler.php @@ -0,0 +1,85 @@ +troopRepository->findById($command->getTroopId()); + $troop->setState(TroopApplicationState::WAITING_FOR_PAYMENT); + $troop->setPairedTroopCode($command->getPairedTroopCode()); + $troop->setFee($troop->countFee()); + $troop->setApplicationDate(new DateTimeImmutable()); + $troop->setMaturityDate($this->countMaturityDate()); + $this->troopRepository->save($troop); + + $this->mailService->sendMailFromTemplate(new ArrayCollection([$troop->getLeader()]), null, Template::TROOP_REGISTRATION, [ + TemplateVariable::SEMINAR_NAME => $this->queryBus->handle(new SettingStringValueQuery(Settings::SEMINAR_NAME)), + TemplateVariable::APPLICATION_FEE => (string) $troop->getFee(), + TemplateVariable::APPLICATION_VARIABLE_SYMBOL => $troop->getVariableSymbolText(), + TemplateVariable::APPLICATION_MATURITY => $troop->getMaturityDateText(), + TemplateVariable::BANK_ACCOUNT => $this->queryBus->handle( + new SettingStringValueQuery(Settings::ACCOUNT_NUMBER) + ), + ]); + } + + /** + * Vypočítá datum splatnosti podle zvolené metody. + */ + private function countMaturityDate(): ?DateTimeImmutable + { + 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; + } +} diff --git a/app/Model/User/Commands/Handlers/RegisterTroopHandler.php b/app/Model/User/Commands/Handlers/RegisterTroopHandler.php new file mode 100644 index 000000000..9f9263397 --- /dev/null +++ b/app/Model/User/Commands/Handlers/RegisterTroopHandler.php @@ -0,0 +1,50 @@ +em->wrapInTransaction(function () use ($command): void { + $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); + + $troop = new Troop($command->getLeader(), $variableSymbol); + $this->troopRepository->save($troop); + }); + } +} diff --git a/app/Model/User/Commands/Handlers/RemovePatrolHandler.php b/app/Model/User/Commands/Handlers/RemovePatrolHandler.php new file mode 100644 index 000000000..05767f7de --- /dev/null +++ b/app/Model/User/Commands/Handlers/RemovePatrolHandler.php @@ -0,0 +1,38 @@ +em->wrapInTransaction(function () use ($command): void { + foreach ($command->getPatrol()->getUsersRoles() as $userRole) { + $user = $userRole->getUser(); + $this->userGroupRoleRepository->remove($userRole); + if ($user->getRoles()->isEmpty() && $user->getGroupRoles()->isEmpty()) { + $this->userRepository->remove($user); + } + } + + $this->patrolRepository->remove($command->getPatrol()); + }); + } +} diff --git a/app/Model/User/Commands/Handlers/RemoveTroopHandler.php b/app/Model/User/Commands/Handlers/RemoveTroopHandler.php new file mode 100644 index 000000000..4afc11981 --- /dev/null +++ b/app/Model/User/Commands/Handlers/RemoveTroopHandler.php @@ -0,0 +1,47 @@ +em->wrapInTransaction(function () use ($command): void { + foreach ($command->getTroop()->getPatrols() as $patrol) { + $this->commandBus->handle(new RemovePatrol($patrol)); + } + + foreach ($command->getTroop()->getUsersRoles() as $userRole) { + $user = $userRole->getUser(); + $this->userGroupRoleRepository->remove($userRole); + if ($user->getRoles()->isEmpty() && $user->getGroupRoles()->isEmpty()) { + $this->userRepository->remove($user); + } + } + + $command->getTroop()->getLeader()->setTroop(null); + + $this->troopRepository->remove($command->getTroop()); + }); + } +} diff --git a/app/Model/User/Commands/Handlers/UpdateGroupMembersHandler.php b/app/Model/User/Commands/Handlers/UpdateGroupMembersHandler.php new file mode 100644 index 000000000..17d5ad9fb --- /dev/null +++ b/app/Model/User/Commands/Handlers/UpdateGroupMembersHandler.php @@ -0,0 +1,158 @@ +em->wrapInTransaction(function () use ($command): void { + $troop = $this->queryBus->handle(new TroopByIdQuery($command->getTroopId())); + + if ($command->getType() === 'patrol') { + if ($command->getPatrolId() !== null) { + $patrol = $this->queryBus->handle(new PatrolByIdQuery($command->getPatrolId())); + } else { + $patrolNumber = $troop->getPatrols()->count() + 1; + $patrol = new Patrol($troop, $troop->getName() . '-' . sprintf('%02d', $patrolNumber)); + $this->patrolRepository->save($patrol); + } + } else { + $patrol = null; + } + + foreach ($command->getPersons() as $person) { + $personId = $person['personId']; + $roleId = $person['roleId']; + + $personDetail = $this->skautIsService->getPersonDetail($personId); + + $user = $this->userRepository->findBySkautISPersonId($personId); + if ($user == null) { + $user = new User(); + $user->setSkautISPersonId($personId); + } + + $user->setFirstName($personDetail->FirstName); + $user->setLastName($personDetail->LastName); + $user->setNickName($personDetail->NickName); + + $user->setMember($personDetail->HasMembership); + + $birthdate = new DateTimeImmutable($personDetail->Birthday); + $user->setBirthdate($birthdate); + $user->setSex($personDetail->ID_Sex); + + if (property_exists($personDetail, 'Phone')) { + $user->setPhone($personDetail->Phone); + } + + if (property_exists($personDetail, 'Email')) { + $user->setEmail($personDetail->Email); + } + + $user->setStreet($personDetail->Street); + $user->setCity($personDetail->City); + $user->setPostcode($personDetail->Postcode); + $user->setState($personDetail->State); + + if ((new DateTimeImmutable())->diff($birthdate)->y < 18) { + $personContacts = $this->skautIsService->getPersonContactAllParent($personId); + + foreach ($personContacts as $contact) { + if ($contact->ID_ContactType === 'telefon_hlavni') { + switch ($contact->ID_ParentType) { + case 'mother': + $user->setMotherName($contact->PersonPersonParent); + $user->setMotherPhone($contact->Value); + break; + case 'father': + $user->setFatherName($contact->PersonPersonParent); + $user->setFatherPhone($contact->Value); + break; + } + } + } + } + + $this->userRepository->save($user); + + $role = $this->roleRepository->findById($roleId); + + if ($command->getType() === 'patrol') { + foreach ($patrol->getUsersRoles() as $usersRole) { + $this->userGroupRoleRepository->remove($usersRole); + } + + $userGroupRoles = $this->userGroupRoleRepository->findByUserAndPatrol($user->getId(), $patrol->getId()); + if ($userGroupRoles->isEmpty()) { + $userGroupRole = new UserGroupRole($user, $role, $patrol); + } else { + $userGroupRole = $userGroupRoles[0]; + $userGroupRole->setRole($role); + } + + $this->userGroupRoleRepository->save($userGroupRole); + } elseif ($command->getType() === 'troop') { + foreach ($troop->getUsersRoles() as $usersRole) { + $this->userGroupRoleRepository->remove($usersRole); + } + + $userGroupRoles = $this->userGroupRoleRepository->findByUserAndTroop($user->getId(), $troop->getId()); + if ($userGroupRoles->isEmpty()) { + $userGroupRole = new UserGroupRole($user, $role, null, $troop); + } else { + $userGroupRole = $userGroupRoles[0]; + $userGroupRole->setRole($role); + } + + $this->userGroupRoleRepository->save($userGroupRole); + } + } + + if (empty($command->getPersons())) { + if ($command->getType() === 'patrol') { + foreach ($patrol->getUsersRoles() as $usersRole) { + $this->userGroupRoleRepository->remove($usersRole); + } + } elseif ($command->getType() === 'troop') { + foreach ($troop->getUsersRoles() as $usersRole) { + $this->userGroupRoleRepository->remove($usersRole); + } + } + } + }); + } +} diff --git a/app/Model/User/Commands/RegisterTroop.php b/app/Model/User/Commands/RegisterTroop.php new file mode 100644 index 000000000..b27d45406 --- /dev/null +++ b/app/Model/User/Commands/RegisterTroop.php @@ -0,0 +1,19 @@ +leader; + } +} diff --git a/app/Model/User/Commands/RemovePatrol.php b/app/Model/User/Commands/RemovePatrol.php new file mode 100644 index 000000000..f59e78218 --- /dev/null +++ b/app/Model/User/Commands/RemovePatrol.php @@ -0,0 +1,19 @@ +patrol; + } +} diff --git a/app/Model/User/Commands/RemoveTroop.php b/app/Model/User/Commands/RemoveTroop.php new file mode 100644 index 000000000..40f2df601 --- /dev/null +++ b/app/Model/User/Commands/RemoveTroop.php @@ -0,0 +1,19 @@ +troop; + } +} diff --git a/app/Model/User/Commands/UpdateGroupMembers.php b/app/Model/User/Commands/UpdateGroupMembers.php new file mode 100644 index 000000000..f03c5aaf3 --- /dev/null +++ b/app/Model/User/Commands/UpdateGroupMembers.php @@ -0,0 +1,42 @@ +type; + } + + public function getTroopId(): int + { + return $this->troopId; + } + + public function getPatrolId(): ?int + { + return $this->patrolId; + } + + /** + * @return int[][] + */ + public function getPersons(): array + { + return $this->persons; + } +} diff --git a/app/Model/User/Patrol.php b/app/Model/User/Patrol.php new file mode 100644 index 000000000..16104ed36 --- /dev/null +++ b/app/Model/User/Patrol.php @@ -0,0 +1,106 @@ + + */ + #[ORM\OneToMany(mappedBy: 'patrol', targetEntity: UserGroupRole::class, cascade: ['persist'])] + protected Collection $usersRoles; + + /** + * Stav přihlášky - nepotvrzená přihláška slouží pro předávání údajů mezi kroky formuláře. + */ + #[ORM\Column(type: 'boolean')] + protected bool $confirmed = false; + + public function __construct(Troop $troop, string $name) + { + $this->usersRoles = new ArrayCollection(); + + $this->troop = $troop; + $this->name = $name; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getName(): string + { + return $this->name; + } + + public function getTroop(): Troop + { + return $this->troop; + } + + /** + * @return Collection + */ + public function getUsersRoles(): Collection + { + return $this->usersRoles; + } + + public function isConfirmed(): bool + { + return $this->confirmed; + } + + public function setConfirmed(bool $confirmed): void + { + $this->confirmed = $confirmed; + } + + /** + * @param string[] $roleNames + */ + public function countUsersInRoles(array $roleNames): int + { + $counter = 0; + foreach ($this->usersRoles as $userRole) { + if (in_array($userRole->getRole()->getSystemName(), $roleNames)) { + $counter++; + } + } + + return $counter; + } +} diff --git a/app/Model/User/Queries/Handlers/PatrolByIdQueryHandler.php b/app/Model/User/Queries/Handlers/PatrolByIdQueryHandler.php new file mode 100644 index 000000000..f5affe12f --- /dev/null +++ b/app/Model/User/Queries/Handlers/PatrolByIdQueryHandler.php @@ -0,0 +1,22 @@ +patrolRepository->findById($query->getPatrolId()); + } +} diff --git a/app/Model/User/Queries/Handlers/PatrolByTroopAndNotConfirmedQueryHandler.php b/app/Model/User/Queries/Handlers/PatrolByTroopAndNotConfirmedQueryHandler.php new file mode 100644 index 000000000..b14044cef --- /dev/null +++ b/app/Model/User/Queries/Handlers/PatrolByTroopAndNotConfirmedQueryHandler.php @@ -0,0 +1,22 @@ +patrolRepository->findByTroopAndNotConfirmed($query->getTroopId()); + } +} diff --git a/app/Model/User/Queries/Handlers/TroopByIdQueryHandler.php b/app/Model/User/Queries/Handlers/TroopByIdQueryHandler.php new file mode 100644 index 000000000..3d2042122 --- /dev/null +++ b/app/Model/User/Queries/Handlers/TroopByIdQueryHandler.php @@ -0,0 +1,22 @@ +troopRepository->findById($query->getTroopId()); + } +} diff --git a/app/Model/User/Queries/Handlers/TroopByLeaderQueryHandler.php b/app/Model/User/Queries/Handlers/TroopByLeaderQueryHandler.php new file mode 100644 index 000000000..d2757575e --- /dev/null +++ b/app/Model/User/Queries/Handlers/TroopByLeaderQueryHandler.php @@ -0,0 +1,22 @@ +troopRepository->findByLeaderId($query->getLeaderId()); + } +} diff --git a/app/Model/User/Queries/Handlers/TroopsByStateQueryHandler.php b/app/Model/User/Queries/Handlers/TroopsByStateQueryHandler.php new file mode 100644 index 000000000..e0fb212f7 --- /dev/null +++ b/app/Model/User/Queries/Handlers/TroopsByStateQueryHandler.php @@ -0,0 +1,26 @@ + + */ + public function __invoke(TroopsByStateQuery $query): Collection + { + return $this->troopRepository->findByState($query->getTroopState()); + } +} diff --git a/app/Model/User/Queries/PatrolByIdQuery.php b/app/Model/User/Queries/PatrolByIdQuery.php new file mode 100644 index 000000000..b0dee5c99 --- /dev/null +++ b/app/Model/User/Queries/PatrolByIdQuery.php @@ -0,0 +1,17 @@ +patrolId; + } +} diff --git a/app/Model/User/Queries/PatrolByTroopAndNotConfirmedQuery.php b/app/Model/User/Queries/PatrolByTroopAndNotConfirmedQuery.php new file mode 100644 index 000000000..6a26e8ebb --- /dev/null +++ b/app/Model/User/Queries/PatrolByTroopAndNotConfirmedQuery.php @@ -0,0 +1,17 @@ +troopId; + } +} diff --git a/app/Model/User/Queries/TroopByIdQuery.php b/app/Model/User/Queries/TroopByIdQuery.php new file mode 100644 index 000000000..6c62b4373 --- /dev/null +++ b/app/Model/User/Queries/TroopByIdQuery.php @@ -0,0 +1,17 @@ +troopId; + } +} diff --git a/app/Model/User/Queries/TroopByLeaderQuery.php b/app/Model/User/Queries/TroopByLeaderQuery.php new file mode 100644 index 000000000..59748efff --- /dev/null +++ b/app/Model/User/Queries/TroopByLeaderQuery.php @@ -0,0 +1,17 @@ +leaderId; + } +} diff --git a/app/Model/User/Queries/TroopsByStateQuery.php b/app/Model/User/Queries/TroopsByStateQuery.php new file mode 100644 index 000000000..8bc2e0d4b --- /dev/null +++ b/app/Model/User/Queries/TroopsByStateQuery.php @@ -0,0 +1,17 @@ +troopState; + } +} diff --git a/app/Model/User/Repositories/PatrolRepository.php b/app/Model/User/Repositories/PatrolRepository.php new file mode 100644 index 000000000..f7b8be640 --- /dev/null +++ b/app/Model/User/Repositories/PatrolRepository.php @@ -0,0 +1,57 @@ +getRepository()->findOneBy(['id' => $id]); + } + + public function findByTroopAndNotConfirmed(int $troopId): ?Patrol + { + return $this->getRepository()->findOneBy(['troop' => $troopId, 'confirmed' => false]); + } + + /** + * @param int[] $ids + * + * @return Collection + */ + public function findPatrolsByIds(array $ids): Collection + { + $criteria = Criteria::create() + ->where(Criteria::expr()->in('id', $ids)); + + return $this->getRepository()->matching($criteria); + } + + public function save(Patrol $patrol): void + { + $this->em->persist($patrol); + $this->em->flush(); + } + + public function remove(Patrol $patrol): void + { + $this->em->remove($patrol); + $this->em->flush(); + } +} diff --git a/app/Model/User/Repositories/TroopRepository.php b/app/Model/User/Repositories/TroopRepository.php new file mode 100644 index 000000000..a60e90dc3 --- /dev/null +++ b/app/Model/User/Repositories/TroopRepository.php @@ -0,0 +1,149 @@ +getRepository()->findOneBy(['id' => $id]); + } + + /** + * @return Collection + */ + public function findAll(): Collection + { + $result = $this->getRepository()->findAll(); + + return new ArrayCollection($result); + } + + /** + * @return Collection + */ + public function findByState(string $state): Collection + { + return $this->getRepository()->matching(Criteria::create()->where(Criteria::expr()->eq('state', $state))); + } + + /** + * @throws NonUniqueResultException + */ + public function findByLeaderId(int $leaderId): ?Troop + { + return $this->getRepository() + ->createQueryBuilder('t') + ->where('t.leader = :leader_id') + ->setParameter('leader_id', $leaderId) + ->getQuery() + ->getOneOrNullResult(); + } + + /** + * @throws NonUniqueResultException + */ + public function findByVariableSymbol(?string $variableSymbol): ?Troop + { + $variableSymbolRegex = '^0*' . $variableSymbol . '$'; + + return $this->createQueryBuilder('t') + ->select('t') + ->join('t.variableSymbol', 'v') + ->where('REGEXP(v.variableSymbol, :variableSymbol) = 1')->setParameter('variableSymbol', $variableSymbolRegex) + ->getQuery() + ->getOneOrNullResult(); + } + + /** + * Vrací skupiny podle id. + * + * @param int[] $ids + * + * @return Collection + */ + public function findTroopsByIds(array $ids): Collection + { + $criteria = Criteria::create() + ->where(Criteria::expr()->in('id', $ids)); + + return $this->getRepository()->matching($criteria); + } + + /** + * Vrací id skupin. + * + * @param Collection $troops + * + * @return int[] + */ + public function findTroopsIds(Collection $troops): array + { + return array_map(static fn (Troop $t) => $t->getId(), $troops->toArray()); + } + + /** + * @param Collection $pairedTroops + * + * @return Collection + */ + public function findWaitingForPaymentOrPairedTroops(Collection $pairedTroops): Collection + { + $criteria = Criteria::create() + ->where(Criteria::expr()->orX( + Criteria::expr()->eq('state', TroopApplicationState::WAITING_FOR_PAYMENT), + Criteria::expr()->in('id', $pairedTroops->map(static fn (Troop $troop) => $troop->getId()) + ->toArray()) + )); + + return $this->getRepository()->matching($criteria); + } + + /** + * @param Collection $pairedTroops + * + * @return string[] + */ + public function getWaitingForPaymentOrPairedTroopsVariableSymbolsOptions(Collection $pairedTroops): array + { + $options = []; + foreach ($this->findWaitingForPaymentOrPairedTroops($pairedTroops) as $troop) { + $options[$troop->getId()] = $troop->getName() . ' (' . $troop->getVariableSymbolText() . ' - ' . $troop->getFee() . ' Kč)'; + } + + return $options; + } + + public function save(Troop $troop): void + { + $this->em->persist($troop); + $this->em->flush(); + } + + public function remove(Troop $troop): void + { + $this->em->remove($troop); + $this->em->flush(); + } +} diff --git a/app/Model/User/Repositories/UserGroupRoleRepository.php b/app/Model/User/Repositories/UserGroupRoleRepository.php new file mode 100644 index 000000000..e884f428c --- /dev/null +++ b/app/Model/User/Repositories/UserGroupRoleRepository.php @@ -0,0 +1,54 @@ + + */ + public function findByUserAndPatrol(int $user_id, int $patrol_id): Collection + { + $result = $this->getRepository()->findBy(['user' => $user_id, 'patrol' => $patrol_id]); + + return new ArrayCollection($result); + } + + /** + * @return Collection + */ + public function findByUserAndTroop(int $user_id, int $troop_id): Collection + { + $result = $this->getRepository()->findBy(['user' => $user_id, 'troop' => $troop_id]); + + return new ArrayCollection($result); + } + + public function save(UserGroupRole $userGroupRole): void + { + $this->em->persist($userGroupRole); + $this->em->flush(); + } + + public function remove(UserGroupRole $userGroupRole): void + { + $this->em->remove($userGroupRole); + $this->em->flush(); + } +} diff --git a/app/Model/User/Repositories/UserRepository.php b/app/Model/User/Repositories/UserRepository.php index c913f7b17..b8d23399c 100644 --- a/app/Model/User/Repositories/UserRepository.php +++ b/app/Model/User/Repositories/UserRepository.php @@ -55,6 +55,14 @@ public function findBySkautISUserId(int $skautISUserId): ?User return $this->getRepository()->findOneBy(['skautISUserId' => $skautISUserId]); } + /** + * Vrací uživatele podle skautISPersonId. + */ + public function findBySkautISPersonId(int $skautISUserId): ?User + { + return $this->getRepository()->findOneBy(['skautISPersonId' => $skautISUserId]); + } + /** * Vrací uživatele podle id. * diff --git a/app/Model/User/Troop.php b/app/Model/User/Troop.php new file mode 100644 index 000000000..43e2faa37 --- /dev/null +++ b/app/Model/User/Troop.php @@ -0,0 +1,389 @@ + + */ + #[ORM\OneToMany(mappedBy: 'troop', targetEntity: Patrol::class, cascade: ['persist'])] + protected Collection $patrols; + + /** + * Uživatelé. + * + * @var Collection + */ + #[ORM\OneToMany(mappedBy: 'troop', targetEntity: UserGroupRole::class, cascade: ['persist'])] + protected Collection $usersRoles; + + /** + * Poplatek za oddíl. + */ + #[ORM\Column(type: 'integer')] + protected int $fee; + + /** + * Variabilní symbol. + */ + #[ORM\ManyToOne(targetEntity: VariableSymbol::class, cascade: ['persist'])] + protected VariableSymbol $variableSymbol; + + /** + * Datum podání přihlášky. + */ + #[ORM\Column(type: 'datetime_immutable', nullable: true)] + protected ?DateTimeImmutable $applicationDate; + + /** + * Datum splatnosti. + */ + #[ORM\Column(type: 'date_immutable', nullable: true)] + protected ?DateTimeImmutable $maturityDate = null; + + /** + * Platební metoda. + */ + #[ORM\Column(type: 'string', nullable: true)] + protected ?string $paymentMethod = null; + + /** + * Datum zaplacení. + */ + #[ORM\Column(type: 'date_immutable', nullable: true)] + protected ?DateTimeImmutable $paymentDate = null; + + /** + * Spárovaná platba. + */ + #[ORM\ManyToOne(targetEntity: Payment::class, inversedBy: 'pairedTroops', cascade: ['persist'])] + protected ?Payment $payment = null; + + /** + * Příjmový doklad. Používá se pro generování id. + */ + #[ORM\ManyToOne(targetEntity: IncomeProof::class, cascade: ['persist'])] + protected ?IncomeProof $incomeProof = null; + + /** + * Stav přihlášky. + */ + #[ORM\Column(type: 'string')] + protected string $state; + + /** + * Vedoucí oddílu. + */ + #[ORM\OneToOne(targetEntity: User::class, inversedBy: 'troop', cascade: ['persist'])] + protected User $leader; + + /** + * Kód pro párování oddílů. + */ + #[ORM\Column(type: 'string')] + protected string $pairingCode; + + /** + * Spárovaný oddíl. + */ + #[ORM\Column(type: 'string', nullable: true)] + protected ?string $pairedTroopCode = null; + + public function __construct(User $leader, VariableSymbol $variableSymbol) + { + $this->patrols = new ArrayCollection(); + $this->usersRoles = new ArrayCollection(); + + $this->leader = $leader; + $this->variableSymbol = $variableSymbol; + + $this->name = 'S-' . $variableSymbol->getVariableSymbol(); + $this->pairingCode = substr(md5(uniqid((string) mt_rand(), true)), 0, 20); + $this->state = TroopApplicationState::DRAFT; + $this->fee = 0; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): void + { + $this->name = $name; + } + + /** + * @return Collection + */ + public function getPatrols(): Collection + { + return $this->patrols; + } + + /** + * @return Collection + */ + public function getUsersRoles(): Collection + { + return $this->usersRoles; + } + + public function getFee(): int + { + return $this->fee; + } + + /** + * Vrací poplatek slovy. + */ + public function getFeeWords(): string + { + $numbersWords = new Numbers_Words(); + $feeWord = $numbersWords->toWords($this->getFee(), 'cs'); + + return str_replace(' ', '', $feeWord); + } + + public function setFee(int $fee): void + { + $this->fee = $fee; + } + + public function getVariableSymbol(): VariableSymbol + { + return $this->variableSymbol; + } + + /** + * Vrací text variabilního symbolu. + */ + public function getVariableSymbolText(): string + { + return $this->variableSymbol->getVariableSymbol(); + } + + public function getApplicationDate(): ?DateTimeImmutable + { + return $this->applicationDate; + } + + public function setApplicationDate(?DateTimeImmutable $applicationDate): void + { + $this->applicationDate = $applicationDate; + } + + public function getMaturityDate(): ?DateTimeImmutable + { + return $this->maturityDate; + } + + public function getMaturityDateText(): ?string + { + return $this->maturityDate?->format(Helpers::DATE_FORMAT); + } + + public function setMaturityDate(?DateTimeImmutable $maturityDate): void + { + $this->maturityDate = $maturityDate; + } + + public function getPaymentMethod(): ?string + { + return $this->paymentMethod; + } + + public function setPaymentMethod(?string $paymentMethod): void + { + $this->paymentMethod = $paymentMethod; + } + + public function getPaymentDate(): ?DateTimeImmutable + { + return $this->paymentDate; + } + + public function setPaymentDate(?DateTimeImmutable $paymentDate): void + { + $this->paymentDate = $paymentDate; + } + + public function getPayment(): ?Payment + { + return $this->payment; + } + + public function setPayment(?Payment $payment): void + { + $this->payment = $payment; + } + + public function getIncomeProof(): ?IncomeProof + { + return $this->incomeProof; + } + + public function setIncomeProof(?IncomeProof $incomeProof): void + { + $this->incomeProof = $incomeProof; + } + + public function getState(): string + { + return $this->state; + } + + public function setState(string $state): void + { + $this->state = $state; + } + + public function getLeader(): User + { + return $this->leader; + } + + public function getPairingCode(): string + { + return $this->pairingCode; + } + + public function getPairedTroopCode(): ?string + { + return $this->pairedTroopCode; + } + + public function setPairedTroopCode(?string $pairedTroopCode): void + { + $this->pairedTroopCode = $pairedTroopCode; + } + + /** + * @return Collection + */ + public function getConfirmedPatrols(): Collection + { + return $this->patrols->filter(static fn ($p) => $p->isConfirmed()); + } + + public function getMaxEscortsCount(): int + { + return $this->getMaxAdultsCount() - $this->countUsersInRoles(['leader']); + } + + public function getMaxAdultsCount(): int + { + return $this->getConfirmedPatrols()->count() * 2; + } + + /** + * @param string[] $roleNames + */ + public function countUsersInRoles(array $roleNames): int + { + $users = []; + + // uzivatele v oddile + foreach ($this->usersRoles as $userRole) { + if (in_array($userRole->getRole()->getSystemName(), $roleNames)) { + $users[$userRole->getUser()->getId()] = true; + } + } + + // uzivatele v druzinach + foreach ($this->getConfirmedPatrols() as $patrol) { + foreach ($patrol->getUsersRoles() as $userRole) { + if (in_array($userRole->getRole()->getSystemName(), $roleNames)) { + $users[$userRole->getUser()->getId()] = true; + } + } + } + + return count($users); + } + + public function countFee(): int + { + $rolesFees = []; + $rolesUsers = []; + + // uzivatele v oddile + foreach ($this->usersRoles as $userRole) { + $role = $userRole->getRole(); + $user = $userRole->getUser(); + if (! array_key_exists($role->getId(), $rolesFees)) { + $rolesFees[$role->getId()] = $role->getFee(); + $rolesUsers[$role->getId()] = []; + } + + $rolesUsers[$role->getId()][$user->getId()] = true; + } + + // uzivatele v druzinach + foreach ($this->getConfirmedPatrols() as $patrol) { + foreach ($patrol->getUsersRoles() as $userRole) { + $role = $userRole->getRole(); + $user = $userRole->getUser(); + if (! array_key_exists($role->getId(), $rolesFees)) { + $rolesFees[$role->getId()] = $role->getFee(); + $rolesUsers[$role->getId()] = []; + } + + $rolesUsers[$role->getId()][$user->getId()] = true; + } + } + + $totalFee = 0; + foreach ($rolesFees as $key => $value) { + $totalFee += $value * count($rolesUsers[$key]); + } + + return $totalFee; + } +} diff --git a/app/Model/User/User.php b/app/Model/User/User.php index 6c6db3a89..45fe0e512 100644 --- a/app/Model/User/User.php +++ b/app/Model/User/User.php @@ -13,6 +13,7 @@ use App\Model\CustomInput\CustomInput; use App\Model\CustomInput\CustomInputValue; use App\Model\Enums\ApplicationState; +use App\Model\Enums\TroopApplicationState; use App\Model\Program\Block; use App\Model\Program\Program; use App\Model\Program\ProgramApplication; @@ -294,6 +295,52 @@ class User #[ORM\Column(type: 'datetime_immutable', nullable: true)] protected ?DateTimeImmutable $photoUpdate = null; + /** + * Přihláška oddílu. + */ + #[ORM\OneToOne(targetEntity: Troop::class, mappedBy: 'leader', cascade: ['persist'])] + protected ?Troop $troop; + + /** + * Telefon. + */ + #[ORM\Column(type: 'string', nullable: true)] + protected ?string $phone = null; + + /** + * Jméno matky. + */ + #[ORM\Column(type: 'string', nullable: true)] + protected ?string $motherName = null; + + /** + * Telefon matky. + */ + #[ORM\Column(type: 'string', nullable: true)] + protected ?string $motherPhone = null; + + /** + * Jméno otce. + */ + #[ORM\Column(type: 'string', nullable: true)] + protected ?string $fatherName = null; + + /** + * Telefon otce. + */ + #[ORM\Column(type: 'string', nullable: true)] + protected ?string $fatherPhone = null; + + /** + * Informace o zdravotním stavu - omezení, alergie, léky. + */ + #[ORM\Column(type: 'text', nullable: true)] + protected ?string $healthInfo = null; + + /** @var Collection */ + #[ORM\OneToMany(targetEntity: UserGroupRole::class, cascade: ['persist'], mappedBy: 'user')] + protected Collection $groupRoles; + public function __construct() { $this->applications = new ArrayCollection(); @@ -302,6 +349,7 @@ public function __construct() $this->lecturersBlocks = new ArrayCollection(); $this->notRegisteredMandatoryBlocks = new ArrayCollection(); $this->customInputValues = new ArrayCollection(); + $this->groupRoles = new ArrayCollection(); } public function getId(): ?int @@ -1121,4 +1169,98 @@ public function isAlternate(Program $program): bool ) )->isEmpty(); } + + public function getTroop(): ?Troop + { + return $this->troop; + } + + public function setTroop(?Troop $troop): void + { + $this->troop = $troop; + } + + public function getPhone(): ?string + { + return $this->phone; + } + + public function setPhone(?string $phone): void + { + $this->phone = $phone; + } + + public function getMotherName(): ?string + { + return $this->motherName; + } + + public function setMotherName(?string $motherName): void + { + $this->motherName = $motherName; + } + + public function getMotherPhone(): ?string + { + return $this->motherPhone; + } + + public function setMotherPhone(?string $motherPhone): void + { + $this->motherPhone = $motherPhone; + } + + public function getFatherName(): ?string + { + return $this->fatherName; + } + + public function setFatherName(?string $fatherName): void + { + $this->fatherName = $fatherName; + } + + public function getFatherPhone(): ?string + { + return $this->fatherPhone; + } + + public function setFatherPhone(?string $fatherPhone): void + { + $this->fatherPhone = $fatherPhone; + } + + public function getHealthInfo(): ?string + { + return $this->healthInfo; + } + + public function setHealthInfo(?string $healthInfo): void + { + $this->healthInfo = $healthInfo; + } + + /** + * @return Collection + */ + public function getGroupRoles(): Collection + { + return $this->groupRoles; + } + + /** + * Vrátí skupinové role uživatele oddělené čárkou. + */ + public function getGroupRolesText(): string + { + $rolesNames = []; + foreach ($this->groupRoles as $groupRole) { + $troop = $groupRole->getTroop() ?? $groupRole->getPatrol()->getTroop(); + if ($troop->getState() !== TroopApplicationState::DRAFT) { + $rolesNames[] = $groupRole->getRole()->getName(); + } + } + + return implode(', ', $rolesNames); + } } diff --git a/app/Model/User/UserGroupRole.php b/app/Model/User/UserGroupRole.php new file mode 100644 index 000000000..fc3444b3f --- /dev/null +++ b/app/Model/User/UserGroupRole.php @@ -0,0 +1,98 @@ +user = $user; + $this->role = $role; + $this->patrol = $patrol; + $this->troop = $troop; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getUser(): User + { + return $this->user; + } + + public function setUser(User $user): void + { + $this->user = $user; + } + + public function getTroop(): ?Troop + { + return $this->troop; + } + + public function setTroop(?Troop $troop): void + { + $this->troop = $troop; + } + + public function getPatrol(): ?Patrol + { + return $this->patrol; + } + + public function setPatrol(?Patrol $patrol): void + { + $this->patrol = $patrol; + } + + public function getRole(): Role + { + return $this->role; + } + + public function setRole(Role $role): void + { + $this->role = $role; + } +} diff --git a/app/Router/RouterFactory.php b/app/Router/RouterFactory.php index 3997ce19a..7e02de8e0 100644 --- a/app/Router/RouterFactory.php +++ b/app/Router/RouterFactory.php @@ -58,6 +58,13 @@ public function createRouter(): RouteList 'id' => null, ]); + $router->addRoute('admin/users//[/]', [ + 'module' => 'Admin:Users', + 'presenter' => 'Users', + 'action' => 'default', + 'id' => null, + ]); + $router->addRoute('admin/payments//[/]', [ 'module' => 'Admin:Payments', 'presenter' => 'Payments', diff --git a/app/Services/ApplicationService.php b/app/Services/ApplicationService.php index ae6d41f56..39140af44 100644 --- a/app/Services/ApplicationService.php +++ b/app/Services/ApplicationService.php @@ -19,6 +19,7 @@ use App\Model\Enums\MaturityType; use App\Model\Enums\PaymentState; use App\Model\Enums\PaymentType; +use App\Model\Enums\TroopApplicationState; use App\Model\Mailing\Template; use App\Model\Mailing\TemplateVariable; use App\Model\Payment\Payment; @@ -32,7 +33,9 @@ use App\Model\Settings\Settings; use App\Model\Structure\Repositories\SubeventRepository; use App\Model\Structure\Subevent; +use App\Model\User\Repositories\TroopRepository; use App\Model\User\Repositories\UserRepository; +use App\Model\User\Troop; use App\Model\User\User; use App\Utils\Helpers; use DateTimeImmutable; @@ -78,7 +81,8 @@ public function __construct( private Translator $translator, private PaymentRepository $paymentRepository, private IncomeProofRepository $incomeProofRepository, - private EventBus $eventBus + private EventBus $eventBus, + private TroopRepository $troopRepository, ) { } @@ -536,6 +540,52 @@ public function updateApplicationPayment( } } + /** + * Aktualizuje stav platby přihlášky oddílu. + * + * @throws Throwable + */ + public function updateTroopApplicationPayment( + Troop $troop, + ?string $paymentMethod, + ?DateTimeImmutable $paymentDate, + ?DateTimeImmutable $maturityDate + ): void { + $oldPaymentMethod = $troop->getPaymentMethod(); + $oldPaymentDate = $troop->getPaymentDate(); + $oldMaturityDate = $troop->getMaturityDate(); + + // pokud neni zmena, nic se neprovede + if ($paymentMethod === $oldPaymentMethod && $paymentDate == $oldPaymentDate && $maturityDate == $oldMaturityDate) { + return; + } + + $this->em->wrapInTransaction(function () use ($troop, $paymentMethod, $paymentDate, $maturityDate): void { + $troop->setPaymentMethod($paymentMethod); + $troop->setPaymentDate($paymentDate); + $troop->setMaturityDate($maturityDate); + $troop->setState($this->getTroopApplicationState($troop)); + $this->troopRepository->save($troop); + }); + + if ($paymentDate !== null && $oldPaymentDate === null) { + $this->mailService->sendMailFromTemplate( + new ArrayCollection( + [ + $troop->getLeader(), + ] + ), + null, + Template::TROOP_PAYMENT_CONFIRMED, + [ + TemplateVariable::SEMINAR_NAME => $this->queryBus->handle( + new SettingStringValueQuery(Settings::SEMINAR_NAME) + ), + ] + ); + } + } + /** * Vytvoří platbu a označí spárované přihlášky jako zaplacené. * @@ -562,32 +612,57 @@ public function createPayment( $payment->setAccountName($accountName); $payment->setMessage($message); - $pairedApplication = $this->applicationRepository->findValidByVariableSymbol($variableSymbol); + $this->pairPayment($payment, $createdBy); + }); + } + + public function pairPayment(Payment $payment, ?User $createdBy = null): void + { + $pairedApplication = $this->applicationRepository->findValidByVariableSymbol($payment->getVariableSymbol()); + + 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() - $payment->getAmount()) >= 0.01) { + $payment->setState(PaymentState::NOT_PAIRED_FEE); + } else { + $payment->setState(PaymentState::PAIRED_AUTO); + $pairedApplication->setPayment($payment); + $this->updateApplicationPayment($pairedApplication, PaymentType::BANK, $payment->getDate(), $pairedApplication->getMaturityDate(), $createdBy); + } + } else { + $pairedTroopApplication = $this->troopRepository->findByVariableSymbol($payment->getVariableSymbol()); - if ($pairedApplication) { + if ($pairedTroopApplication) { if ( - $pairedApplication->getState() === ApplicationState::PAID || - $pairedApplication->getState() === ApplicationState::PAID_FREE + $pairedTroopApplication->getState() === TroopApplicationState::PAID ) { $payment->setState(PaymentState::NOT_PAIRED_PAID); } elseif ( - $pairedApplication->getState() === ApplicationState::CANCELED || - $pairedApplication->getState() === ApplicationState::CANCELED_NOT_PAID + $pairedTroopApplication->getState() === TroopApplicationState::CANCELED_NOT_PAID ) { $payment->setState(PaymentState::NOT_PAIRED_CANCELED); - } elseif (abs($pairedApplication->getFee() - $amount) >= 0.01) { + } elseif (abs($pairedTroopApplication->getFee() - $payment->getAmount()) >= 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); + $pairedTroopApplication->setPayment($payment); + $this->updateTroopApplicationPayment($pairedTroopApplication, PaymentType::BANK, $payment->getDate(), $pairedTroopApplication->getMaturityDate()); } } else { $payment->setState(PaymentState::NOT_PAIRED_VS); } + } - $this->paymentRepository->save($payment); - }); + $this->paymentRepository->save($payment); } /** @@ -608,6 +683,7 @@ public function createPaymentManual( * Aktualizuje platbu a stav spárovaných přihlášek. * * @param Collection $pairedApplications + * @param Collection $pairedTroops * * @throws Throwable */ @@ -617,9 +693,10 @@ public function updatePayment( ?float $amount, ?string $variableSymbol, Collection $pairedApplications, + Collection $pairedTroops, User $createdBy ): void { - $this->em->wrapInTransaction(function () use ($payment, $date, $amount, $variableSymbol, $pairedApplications, $createdBy): void { + $this->em->wrapInTransaction(function () use ($payment, $date, $amount, $variableSymbol, $pairedApplications, $pairedTroops, $createdBy): void { if ($date !== null) { $payment->setDate($date); } @@ -653,8 +730,27 @@ public function updatePayment( } } + $oldPairedTroops = clone $payment->getPairedTroops(); + $newPairedTroops = clone $pairedTroops; + + foreach ($oldPairedTroops as $pairedTroop) { + if (! $newPairedTroops->contains($pairedTroop)) { + $pairedTroop->setPayment(null); + $this->updateTroopApplicationPayment($pairedTroop, null, null, $pairedTroop->getMaturityDate()); + $pairedApplicationsModified = true; + } + } + + foreach ($newPairedTroops as $pairedTroop) { + if (! $oldPairedTroops->contains($pairedTroop)) { + $pairedTroop->setPayment($payment); + $this->updateTroopApplicationPayment($pairedTroop, PaymentType::BANK, $payment->getDate(), $pairedTroop->getMaturityDate()); + $pairedApplicationsModified = true; + } + } + if ($pairedApplicationsModified) { - if ($pairedApplications->isEmpty()) { + if ($pairedApplications->isEmpty() && $pairedTroops->isEmpty()) { $payment->setState(PaymentState::NOT_PAIRED); } else { $payment->setState(PaymentState::PAIRED_MANUAL); @@ -677,6 +773,12 @@ public function removePayment(Payment $payment, User $createdBy): void $this->updateApplicationPayment($pairedApplication, null, null, $pairedApplication->getMaturityDate(), $createdBy); } + foreach ($payment->getPairedTroops() as $pairedTroop) { + $this->updateTroopApplicationPayment($pairedTroop, null, null, $pairedTroop->getMaturityDate()); + $pairedTroop->setPayment(null); + $this->troopRepository->save($pairedTroop); + } + $this->paymentRepository->remove($payment); }); } @@ -999,6 +1101,26 @@ private function getApplicationState(Application $application): string return ApplicationState::WAITING_FOR_PAYMENT; } + /** + * Určí stav přihlášky skupiny. + */ + private function getTroopApplicationState(Troop $troop): string + { + if ($troop->getState() === TroopApplicationState::DRAFT) { + return TroopApplicationState::DRAFT; + } + + if ($troop->getState() === TroopApplicationState::CANCELED_NOT_PAID) { + return TroopApplicationState::CANCELED_NOT_PAID; + } + + if ($troop->getPaymentDate()) { + return TroopApplicationState::PAID; + } + + return TroopApplicationState::WAITING_FOR_PAYMENT; + } + /** * Zvýší obsazenost rolí. * diff --git a/app/Services/Authenticator.php b/app/Services/Authenticator.php index cfcba4643..862e46fe6 100644 --- a/app/Services/Authenticator.php +++ b/app/Services/Authenticator.php @@ -53,7 +53,8 @@ public function authenticate(string $user, string $password): SimpleIdentity $firstLogin = false; if ($user === null) { - $user = new User(); + // nacten ze skautIS pres skupinu + $user = $this->userRepository->findBySkautISPersonId($skautISUser->ID_Person) ?? new User(); $roleNonregistered = $this->roleRepository->findBySystemName(Role::NONREGISTERED); $user->addRole($roleNonregistered); $firstLogin = true; @@ -107,11 +108,7 @@ private function updateUserFromSkautIS(User $user, stdClass $skautISUser): void $user->setMember($skautISUser->HasMembership); $validMembership = $this->skautIsService->getValidMembership($user->getSkautISPersonId()); - if ($validMembership === null) { - $user->setUnit(null); - } else { - $user->setUnit($validMembership->RegistrationNumber); - } + $user->setUnit($validMembership?->RegistrationNumber); $photoUpdate = new DateTimeImmutable($skautISPerson->PhotoUpdate); if ($user->getPhotoUpdate() === null || $photoUpdate->diff($user->getPhotoUpdate())->s > 0) { diff --git a/app/Services/ExcelExportService.php b/app/Services/ExcelExportService.php index 47006f208..6557dfd98 100644 --- a/app/Services/ExcelExportService.php +++ b/app/Services/ExcelExportService.php @@ -14,7 +14,9 @@ use App\Model\Program\Repositories\ProgramRepository; use App\Model\Program\Room; use App\Model\Structure\Repositories\SubeventRepository; +use App\Model\User\Patrol; use App\Model\User\Queries\UserAttendsProgramsQuery; +use App\Model\User\Troop; use App\Model\User\User; use App\Utils\Helpers; use Doctrine\Common\Collections\ArrayCollection; @@ -30,6 +32,10 @@ use function implode; use function preg_replace; +use function str_pad; +use function substr; + +use const STR_PAD_LEFT; /** * Služba pro export do formátu XLSX. @@ -244,83 +250,88 @@ public function exportUsersList(Collection $users, string $filename): ExcelRespo $row = 1; $column = 1; - $sheet->setCellValueByColumnAndRow($column, $row, $this->translator->translate('common.export.user.display_name')); - $sheet->getStyleByColumnAndRow($column, $row)->getFont()->setBold(true); + $sheet->setCellValue([$column, $row], $this->translator->translate('common.export.user.display_name')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); $sheet->getColumnDimensionByColumn($column++)->setWidth(30); - $sheet->setCellValueByColumnAndRow($column, $row, $this->translator->translate('common.export.user.username')); - $sheet->getStyleByColumnAndRow($column, $row)->getFont()->setBold(true); + $sheet->setCellValue([$column, $row], $this->translator->translate('common.export.user.username')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); $sheet->getColumnDimensionByColumn($column++)->setWidth(20); - $sheet->setCellValueByColumnAndRow($column, $row, $this->translator->translate('common.export.user.roles')); - $sheet->getStyleByColumnAndRow($column, $row)->getFont()->setBold(true); + $sheet->setCellValue([$column, $row], $this->translator->translate('common.export.user.roles')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); $sheet->getColumnDimensionByColumn($column++)->setWidth(30); - $sheet->setCellValueByColumnAndRow($column, $row, $this->translator->translate('common.export.user.subevents')); - $sheet->getStyleByColumnAndRow($column, $row)->getFont()->setBold(true); + $sheet->setCellValue([$column, $row], $this->translator->translate('Skupinové role')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); $sheet->getColumnDimensionByColumn($column++)->setWidth(30); - $sheet->setCellValueByColumnAndRow($column, $row, $this->translator->translate('common.export.user.approved')); - $sheet->getStyleByColumnAndRow($column, $row)->getFont()->setBold(true); + $sheet->setCellValue([$column, $row], $this->translator->translate('common.export.user.subevents')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(30); + + $sheet->setCellValue([$column, $row], $this->translator->translate('common.export.user.approved')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); $sheet->getColumnDimensionByColumn($column++)->setWidth(10); - $sheet->setCellValueByColumnAndRow($column, $row, $this->translator->translate('common.export.user.membership')); - $sheet->getStyleByColumnAndRow($column, $row)->getFont()->setBold(true); + $sheet->setCellValue([$column, $row], $this->translator->translate('common.export.user.membership')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); $sheet->getColumnDimensionByColumn($column++)->setWidth(20); - $sheet->setCellValueByColumnAndRow($column, $row, $this->translator->translate('common.export.user.age')); - $sheet->getStyleByColumnAndRow($column, $row)->getFont()->setBold(true); + $sheet->setCellValue([$column, $row], $this->translator->translate('common.export.user.age')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); $sheet->getColumnDimensionByColumn($column++)->setWidth(10); - $sheet->setCellValueByColumnAndRow($column, $row, $this->translator->translate('common.export.user.email')); - $sheet->getStyleByColumnAndRow($column, $row)->getFont()->setBold(true); + $sheet->setCellValue([$column, $row], $this->translator->translate('common.export.user.email')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); $sheet->getColumnDimensionByColumn($column++)->setWidth(30); - $sheet->setCellValueByColumnAndRow($column, $row, $this->translator->translate('common.export.user.city')); - $sheet->getStyleByColumnAndRow($column, $row)->getFont()->setBold(true); + $sheet->setCellValue([$column, $row], $this->translator->translate('common.export.user.city')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); $sheet->getColumnDimensionByColumn($column++)->setWidth(20); - $sheet->setCellValueByColumnAndRow($column, $row, $this->translator->translate('common.export.user.fee')); - $sheet->getStyleByColumnAndRow($column, $row)->getFont()->setBold(true); + $sheet->setCellValue([$column, $row], $this->translator->translate('common.export.user.fee')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); $sheet->getColumnDimensionByColumn($column++)->setWidth(15); - $sheet->setCellValueByColumnAndRow($column, $row, $this->translator->translate('common.export.user.fee_remaining')); - $sheet->getStyleByColumnAndRow($column, $row)->getFont()->setBold(true); + $sheet->setCellValue([$column, $row], $this->translator->translate('common.export.user.fee_remaining')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); $sheet->getColumnDimensionByColumn($column++)->setWidth(15); - $sheet->setCellValueByColumnAndRow($column, $row, $this->translator->translate('common.export.user.variable_symbol')); - $sheet->getStyleByColumnAndRow($column, $row)->getFont()->setBold(true); + $sheet->setCellValue([$column, $row], $this->translator->translate('common.export.user.variable_symbol')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); $sheet->getColumnDimensionByColumn($column++)->setWidth(25); - $sheet->setCellValueByColumnAndRow($column, $row, $this->translator->translate('common.export.user.payment_method')); - $sheet->getStyleByColumnAndRow($column, $row)->getFont()->setBold(true); + $sheet->setCellValue([$column, $row], $this->translator->translate('common.export.user.payment_method')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); $sheet->getColumnDimensionByColumn($column++)->setWidth(15); - $sheet->setCellValueByColumnAndRow($column, $row, $this->translator->translate('common.export.user.payment_date')); - $sheet->getStyleByColumnAndRow($column, $row)->getFont()->setBold(true); + $sheet->setCellValue([$column, $row], $this->translator->translate('common.export.user.payment_date')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); $sheet->getColumnDimensionByColumn($column++)->setWidth(20); - $sheet->setCellValueByColumnAndRow($column, $row, $this->translator->translate('common.export.user.first_application_date')); - $sheet->getStyleByColumnAndRow($column, $row)->getFont()->setBold(true); + $sheet->setCellValue([$column, $row], $this->translator->translate('common.export.user.first_application_date')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); $sheet->getColumnDimensionByColumn($column++)->setWidth(20); - $sheet->setCellValueByColumnAndRow($column, $row, $this->translator->translate('common.export.user.attended')); - $sheet->getStyleByColumnAndRow($column, $row)->getFont()->setBold(true); + $sheet->setCellValue([$column, $row], $this->translator->translate('common.export.user.attended')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); $sheet->getColumnDimensionByColumn($column++)->setWidth(10); @@ -348,14 +359,14 @@ public function exportUsersList(Collection $users, string $filename): ExcelRespo throw new InvalidArgumentException(); } - $sheet->setCellValueByColumnAndRow($column, $row, $this->translator->translate($customInput->getName())); - $sheet->getStyleByColumnAndRow($column, $row)->getFont()->setBold(true); + $sheet->setCellValue([$column, $row], $this->translator->translate($customInput->getName())); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); $sheet->getColumnDimensionByColumn($column++)->setWidth($width); } - $sheet->setCellValueByColumnAndRow($column, $row, $this->translator->translate('common.export.user.private_note')); - $sheet->getStyleByColumnAndRow($column, $row)->getFont()->setBold(true); + $sheet->setCellValue([$column, $row], $this->translator->translate('common.export.user.private_note')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); $sheet->getColumnDimensionByColumn($column++)->setWidth(60); @@ -363,41 +374,41 @@ public function exportUsersList(Collection $users, string $filename): ExcelRespo $row++; $column = 1; - $sheet->setCellValueByColumnAndRow($column++, $row, $user->getDisplayName()); + $sheet->setCellValue([$column++, $row], $user->getDisplayName()); + + $sheet->setCellValue([$column++, $row], $user->getUsername()); - $sheet->setCellValueByColumnAndRow($column++, $row, $user->getUsername()); + $sheet->setCellValue([$column++, $row], $user->getRolesText()); - $sheet->setCellValueByColumnAndRow($column++, $row, $user->getRolesText()); + $sheet->setCellValue([$column++, $row], $user->getGroupRolesText()); - $sheet->setCellValueByColumnAndRow($column++, $row, $user->getSubeventsText()); + $sheet->setCellValue([$column++, $row], $user->getSubeventsText()); - $sheet->setCellValueByColumnAndRow($column++, $row, $user->isApproved() + $sheet->setCellValue([$column++, $row], $user->isApproved() ? $this->translator->translate('common.export.common.yes') : $this->translator->translate('common.export.common.no')); - $sheet->getCellByColumnAndRow($column++, $row) - ->setValueExplicit($this->userService->getMembershipText($user), DataType::TYPE_STRING); + $sheet->getCell([$column++, $row])->setValueExplicit($this->userService->getMembershipText($user)); - $sheet->setCellValueByColumnAndRow($column++, $row, $user->getAge()); + $sheet->setCellValue([$column++, $row], $user->getAge()); - $sheet->setCellValueByColumnAndRow($column++, $row, $user->getEmail()); + $sheet->setCellValue([$column++, $row], $user->getEmail()); - $sheet->setCellValueByColumnAndRow($column++, $row, $user->getCity()); + $sheet->setCellValue([$column++, $row], $user->getCity()); - $sheet->setCellValueByColumnAndRow($column++, $row, $user->getFee()); + $sheet->setCellValue([$column++, $row], $user->getFee()); - $sheet->setCellValueByColumnAndRow($column++, $row, $user->getFeeRemaining()); + $sheet->setCellValue([$column++, $row], $user->getFeeRemaining()); - $sheet->getCellByColumnAndRow($column++, $row) - ->setValueExplicit($user->getVariableSymbolsText(), DataType::TYPE_STRING); + $sheet->getCell([$column++, $row])->setValueExplicit($user->getVariableSymbolsText()); - $sheet->setCellValueByColumnAndRow($column++, $row, $user->getPaymentMethod() ? $this->translator->translate('common.payment.' . $user->getPaymentMethod()) : ''); + $sheet->setCellValue([$column++, $row], $user->getPaymentMethod() ? $this->translator->translate('common.payment.' . $user->getPaymentMethod()) : ''); - $sheet->setCellValueByColumnAndRow($column++, $row, $user->getLastPaymentDate() !== null ? $user->getLastPaymentDate()->format(Helpers::DATE_FORMAT) : ''); + $sheet->setCellValue([$column++, $row], $user->getLastPaymentDate() !== null ? $user->getLastPaymentDate()->format(Helpers::DATE_FORMAT) : ''); - $sheet->setCellValueByColumnAndRow($column++, $row, $user->getRolesApplicationDate() !== null ? $user->getRolesApplicationDate()->format(Helpers::DATE_FORMAT) : ''); + $sheet->setCellValue([$column++, $row], $user->getRolesApplicationDate() !== null ? $user->getRolesApplicationDate()->format(Helpers::DATE_FORMAT) : ''); - $sheet->setCellValueByColumnAndRow($column++, $row, $user->isAttended() + $sheet->setCellValue([$column++, $row], $user->isAttended() ? $this->translator->translate('common.export.common.yes') : $this->translator->translate('common.export.common.no')); @@ -417,11 +428,11 @@ public function exportUsersList(Collection $users, string $filename): ExcelRespo $value = $customInputValue->getValueText(); } - $sheet->setCellValueByColumnAndRow($column++, $row, $value); + $sheet->setCellValue([$column++, $row], $value); } - $sheet->setCellValueByColumnAndRow($column, $row, $user->getNote()); - $sheet->getStyleByColumnAndRow($column++, $row)->getAlignment()->setWrapText(true); + $sheet->setCellValue([$column, $row], $user->getNote()); + $sheet->getStyle([$column++, $row])->getAlignment()->setWrapText(true); } return new ExcelResponse($this->spreadsheet, $filename); @@ -513,6 +524,533 @@ public function exportUsersSubeventsAndCategories(Collection $users, string $fil return new ExcelResponse($this->spreadsheet, $filename); } + /** + * @param Collection $patrols + * + * @throws Exception + */ + public function exportPatrolsList(Collection $patrols, string $filename): ExcelResponse + { + $sheet = $this->spreadsheet->getSheet(0); + + $row = 1; + $column = 1; + + $sheet->setCellValue([$column, $row], $this->translator->translate('Název')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(20); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Skupina')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(15); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Datum založení')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(20); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Počet osob')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(15); + + foreach ($patrols as $patrol) { + $row++; + $column = 1; + + $sheet->setCellValue([$column++, $row], $patrol->getName()); + + $sheet->setCellValue([$column++, $row], $patrol->getTroop()->getName()); + + $sheet->setCellValue([$column++, $row], $patrol->getTroop()->getApplicationDate() !== null ? $patrol->getTroop()->getApplicationDate()->format(Helpers::DATETIME_FORMAT) : ''); + + $sheet->setCellValue([$column++, $row], $patrol->getUsersRoles()->count()); + } + + return new ExcelResponse($this->spreadsheet, $filename); + } + + /** + * @param Collection $troops + * + * @throws Exception + */ + public function exportTroopsList(Collection $troops, string $filename): ExcelResponse + { + $sheet = $this->spreadsheet->getSheet(0); + + $row = 1; + $column = 1; + + $sheet->setCellValue([$column, $row], $this->translator->translate('Název')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(20); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Stav')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(20); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Variabilní symbol')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(20); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Vedoucí')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(30); + + $sheet->setCellValue([$column, $row], $this->translator->translate('E-mail vedoucího')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(30); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Datum založení')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(20); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Cena')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(10); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Datum splatnosti')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(20); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Datum platby')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(20); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Kód jamoddílu')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(30); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Počet osob')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(15); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Počet rádců')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(15); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Počet dospělých')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(15); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Počet družin')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(15); + + foreach ($troops as $troop) { + $row++; + $column = 1; + + $sheet->setCellValue([$column++, $row], $troop->getName()); + + $sheet->setCellValue([$column++, $row], $this->translator->translate('common.application_state.' . $troop->getState())); + + $sheet->setCellValue([$column++, $row], $troop->getVariableSymbolText()); + + $sheet->setCellValue([$column++, $row], $troop->getLeader()->getDisplayName()); + + $sheet->setCellValue([$column++, $row], $troop->getLeader()->getEmail()); + + $sheet->setCellValue([$column++, $row], $troop->getApplicationDate() !== null ? $troop->getApplicationDate()->format(Helpers::DATETIME_FORMAT) : ''); + + $sheet->setCellValue([$column++, $row], $troop->getFee()); + + $sheet->setCellValue([$column++, $row], $troop->getMaturityDate() !== null ? $troop->getMaturityDate()->format(Helpers::DATE_FORMAT) : ''); + + $sheet->setCellValue([$column++, $row], $troop->getPaymentDate() !== null ? $troop->getPaymentDate()->format(Helpers::DATE_FORMAT) : ''); + + $sheet->setCellValue([$column++, $row], $troop->getPairingCode()); + + $sheet->setCellValue([$column++, $row], $troop->countUsersInRoles([Role::PATROL_LEADER, Role::LEADER, Role::ESCORT, Role::ATTENDEE])); + + $sheet->setCellValue([$column++, $row], $troop->countUsersInRoles([Role::PATROL_LEADER])); + + $sheet->setCellValue([$column++, $row], $troop->countUsersInRoles([Role::LEADER, Role::ESCORT])); + + $sheet->setCellValue([$column++, $row], $troop->getConfirmedPatrols()->count()); + } + + return new ExcelResponse($this->spreadsheet, $filename); + } + + /** + * @param Collection $troops + * + * @throws Exception + */ + public function exportNsjTroops(Collection $troops, string $filename): ExcelResponse + { + $sheet = $this->spreadsheet->getSheet(0); + + $row = 1; + $column = 1; + + $sheet->setCellValue([$column, $row], $this->translator->translate('Název')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(20); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Vedoucí - jméno')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(30); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Vedoucí - userId')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(20); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Vedoucí - personId')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(20); + + foreach ($troops as $troop) { + $row++; + $column = 1; + + $sheet->setCellValue([$column++, $row], $troop->getName()); + $sheet->setCellValue([$column++, $row], $troop->getLeader()->getDisplayName()); + $sheet->setCellValue([$column++, $row], $troop->getLeader()->getSkautISUserId()); + $sheet->setCellValue([$column++, $row], $troop->getLeader()->getSkautISPersonId()); + } + + return new ExcelResponse($this->spreadsheet, $filename); + } + + /** + * @param Troop[] $troops + */ + public function exportNsjAttendees($troops, string $filename): ExcelResponse + { + $sheet = $this->spreadsheet->getSheet(0); + + $row = 1; + $column = 1; + + $sheet->setCellValue([$column, $row], $this->translator->translate('Jméno')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(20); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Příjmení')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(20); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Přezdívka')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(20); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Datum narození')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(20); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Adresa')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(50); + + $sheet->setCellValue([$column, $row], $this->translator->translate('E-mail')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(30); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Telefon')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(15); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Telefon matky')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(15); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Telefon otce')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(15); + + $sheet->setCellValue([$column, $row], $this->translator->translate('O mně')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(20); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Poznámka')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(20); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Zdravotní informace')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(50); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Role')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(15); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Skupina')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(15); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Družina')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(15); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Kód')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(15); + + foreach ($troops as $troop) { + $i = 0; + + foreach ($troop->getUsersRoles() as $usersRole) { + $user = $usersRole->getUser(); + $i++; + + $row++; + $column = 1; + + $sheet->setCellValue([$column++, $row], $user->getFirstName()); + $sheet->setCellValue([$column++, $row], $user->getLastName()); + $sheet->setCellValue([$column++, $row], $user->getNickName()); + $sheet->setCellValue([$column++, $row], $user->getBirthdate()->format(Helpers::DATE_FORMAT)); + $sheet->setCellValue([$column++, $row], $user->getAddress()); + $sheet->setCellValue([$column++, $row], $user->getEmail()); + $sheet->setCellValue([$column++, $row], $user->getPhone()); + $sheet->setCellValue([$column++, $row], $user->getMotherPhone()); + $sheet->setCellValue([$column++, $row], $user->getFatherPhone()); + $sheet->setCellValue([$column++, $row], $user->getAbout()); + $sheet->setCellValue([$column++, $row], $user->getNote()); + $sheet->setCellValue([$column++, $row], $user->getHealthInfo()); + $sheet->setCellValue([$column++, $row], $usersRole->getRole()->getName()); + $sheet->setCellValue([$column++, $row], $troop->getName()); + $sheet->setCellValue([$column++, $row], ''); + + $code = substr($troop->getVariableSymbolText(), -4) . '-00-' + . str_pad((string) $i, 2, '0', STR_PAD_LEFT); + $sheet->setCellValue([$column++, $row], $code); + } + + $allUsers = new ArrayCollection(); + $duplicitUsers = new ArrayCollection(); + $exportedUsers = new ArrayCollection(); + + foreach ($troop->getConfirmedPatrols() as $patrol) { + foreach ($patrol->getUsersRoles() as $usersRole) { + $userId = $usersRole->getUser()->getId(); + + if ($allUsers->contains($userId)) { + $duplicitUsers->add($userId); + } else { + $allUsers->add($userId); + } + } + } + + foreach ($troop->getConfirmedPatrols() as $patrol) { + $i = 0; + + foreach ($patrol->getUsersRoles() as $usersRole) { + $user = $usersRole->getUser(); + $i++; + + if ($exportedUsers->contains($user->getId())) { + continue; + } + + $exportedUsers->add($user->getId()); + + $row++; + $column = 1; + + $sheet->setCellValue([$column++, $row], $user->getFirstName()); + $sheet->setCellValue([$column++, $row], $user->getLastName()); + $sheet->setCellValue([$column++, $row], $user->getNickName()); + $sheet->setCellValue([$column++, $row], $user->getBirthdate()->format(Helpers::DATE_FORMAT)); + $sheet->setCellValue([$column++, $row], $user->getAddress()); + $sheet->setCellValue([$column++, $row], $user->getEmail()); + $sheet->setCellValue([$column++, $row], $user->getPhone()); + $sheet->setCellValue([$column++, $row], $user->getMotherPhone()); + $sheet->setCellValue([$column++, $row], $user->getFatherPhone()); + $sheet->setCellValue([$column++, $row], $user->getAbout()); + $sheet->setCellValue([$column++, $row], $user->getNote()); + $sheet->setCellValue([$column++, $row], $user->getHealthInfo()); + $sheet->setCellValue([$column++, $row], $usersRole->getRole()->getName()); + $sheet->setCellValue([$column++, $row], $troop->getName()); + + if ($duplicitUsers->contains($user->getId())) { + $sheet->setCellValue([$column++, $row], ''); + + $code = substr($troop->getVariableSymbolText(), -4) . '-00-00'; + $sheet->setCellValue([$column++, $row], $code); + } else { + $sheet->setCellValue([$column++, $row], $patrol->getName()); + + $code = substr($troop->getVariableSymbolText(), -4) . '-' + . substr($patrol->getName(), -2) . '-' + . str_pad((string) $i, 2, '0', STR_PAD_LEFT); + $sheet->setCellValue([$column++, $row], $code); + } + } + } + } + + return new ExcelResponse($this->spreadsheet, $filename); + } + + /** + * @param User[] $users + */ + public function exportNsjOthers($users, string $filename): ExcelResponse + { + $sheet = $this->spreadsheet->getSheet(0); + + $row = 1; + $column = 1; + + $sheet->setCellValue([$column, $row], $this->translator->translate('Jméno')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(20); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Příjmení')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(20); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Přezdívka')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(20); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Datum narození')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(20); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Adresa')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(50); + + $sheet->setCellValue([$column, $row], $this->translator->translate('E-mail')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(30); + + $sheet->setCellValue([$column, $row], $this->translator->translate('O mně')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(20); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Poznámka')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(20); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Role')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(15); + + $sheet->setCellValue([$column, $row], $this->translator->translate('Kód')); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth(15); + + foreach ($this->customInputRepository->findAllOrderedByPosition() as $customInput) { + switch ($customInput->getType()) { + case CustomInput::TEXT: + case CustomInput::SELECT: + case CustomInput::MULTISELECT: + case CustomInput::DATETIME: + $width = 30; + break; + + case CustomInput::DATE: + $width = 20; + break; + + case CustomInput::CHECKBOX: + $width = 15; + break; + + case CustomInput::FILE: + continue 2; + + default: + throw new InvalidArgumentException(); + } + + $sheet->setCellValue([$column, $row], $this->translator->translate($customInput->getName())); + $sheet->getStyle([$column, $row])->getFont()->setBold(true); + $sheet->getColumnDimensionByColumn($column)->setAutoSize(false); + $sheet->getColumnDimensionByColumn($column++)->setWidth($width); + } + + foreach ($users as $user) { + $row++; + $column = 1; + + $sheet->setCellValue([$column++, $row], $user->getFirstName()); + $sheet->setCellValue([$column++, $row], $user->getLastName()); + $sheet->setCellValue([$column++, $row], $user->getNickName()); + $sheet->setCellValue([$column++, $row], $user->getBirthdate()->format(Helpers::DATE_FORMAT)); + $sheet->setCellValue([$column++, $row], $user->getAddress()); + $sheet->setCellValue([$column++, $row], $user->getEmail()); + $sheet->setCellValue([$column++, $row], $user->getAbout()); + $sheet->setCellValue([$column++, $row], $user->getNote()); + $sheet->setCellValue([$column++, $row], $user->getRolesText()); + $sheet->setCellValue([$column++, $row], substr($user->getRolesApplication()?->getVariableSymbolText() ?: '', -4)); + + foreach ($this->customInputRepository->findAllOrderedByPosition() as $customInput) { + $customInputValue = $user->getCustomInputValue($customInput); + + if ($customInputValue === null) { + $column++; + continue; + } + + if ($customInputValue instanceof CustomCheckboxValue) { + $value = $customInputValue->getValue() + ? $this->translator->translate('common.export.common.yes') + : $this->translator->translate('common.export.common.no'); + } else { + $value = $customInputValue->getValueText(); + } + + $sheet->setCellValue([$column++, $row], $value); + } + } + + return new ExcelResponse($this->spreadsheet, $filename); + } + /** * @param Collection $blocks * diff --git a/app/Services/SkautIsService.php b/app/Services/SkautIsService.php index 1993acd36..f2b75c873 100644 --- a/app/Services/SkautIsService.php +++ b/app/Services/SkautIsService.php @@ -12,6 +12,9 @@ use stdClass; use Throwable; +use function array_filter; +use function in_array; + /** * Služba pro komunikaci se skautIS. */ @@ -66,11 +69,13 @@ public function setLoginData(array $data): void /** * Vrátí skautIS role uživatele. * + * @param ?string[] $allowedRoleTypes + * * @return stdClass[] * * @throws Throwable */ - public function getUserRoles(int $userId): array + public function getUserRoles(int $userId, ?array $allowedRoleTypes = null): array { $roles = $this->userRolesCache->load($userId); @@ -82,7 +87,13 @@ public function getUserRoles(int $userId): array $this->userRolesCache->save($userId, $roles); } - return $roles instanceof stdClass ? [] : $roles; + $rolesArray = $roles instanceof stdClass ? [] : $roles; + + if ($allowedRoleTypes !== null) { + return array_filter($rolesArray, static fn (stdClass $r) => isset($r->Key) && in_array($r->Key, $allowedRoleTypes)); + } + + return $rolesArray; } /** @@ -221,4 +232,57 @@ public function getUnitDetail(int $unitId): stdClass 'ID' => $unitId, ]); } + + /** + * @param ?string[] $allowedUnitTypes + * + * @return stdClass[] + */ + public function getUnitAllUnit(?array $allowedUnitTypes = null): array + { + $units = $this->skautIs->org->UnitAllUnit([ + 'ID_Login' => $this->skautIs->getUser()->getLoginId(), + 'ID_Unit' => $this->skautIs->getUser()->getUnitId(), + ], 'unitAllUnitInput'); + + $unitsArray = $units instanceof stdClass ? [] : $units; + + if ($allowedUnitTypes !== null) { + return array_filter($unitsArray, static fn (stdClass $r) => in_array($r->ID_UnitType, $allowedUnitTypes)); + } + + return $unitsArray; + } + + /** + * @return stdClass[] + */ + public function getMembershipAll(int $unitId, ?int $minimalAge = null, ?DateTimeImmutable $date = null): array + { + $memberships = $this->skautIs->org->MembershipAll([ + 'ID_Login' => $this->skautIs->getUser()->getLoginId(), + 'ID_Unit' => $unitId, + 'ID_MembershipType' => 'radne', + 'OnlyDirectMember' => false, + ], 'membershipAllInput'); + + if ($minimalAge !== null) { + return array_filter($memberships, static fn (stdClass $m) => $date->diff(new DateTimeImmutable($m->Birthday))->y >= $minimalAge); + } + + return $memberships instanceof stdClass ? [] : $memberships; + } + + /** + * @return stdClass[] + */ + public function getPersonContactAllParent(int $personId): array + { + $contacts = $this->skautIs->org->PersonContactAllParent([ + 'ID_Login' => $this->skautIs->getUser()->getLoginId(), + 'ID_Person' => $personId, + ], 'personContactAllParentInput'); + + return $contacts instanceof stdClass ? [] : $contacts; + } } diff --git a/app/Utils/Validators.php b/app/Utils/Validators.php index be753f87b..420d235e0 100644 --- a/app/Utils/Validators.php +++ b/app/Utils/Validators.php @@ -20,6 +20,7 @@ use function array_map; use function explode; +use function sprintf; use function trim; /** @@ -50,6 +51,54 @@ public function validateRolesNonregistered(Collection $selectedRoles, User $user return true; } + /** + * Ověří požadovaný minimální věk. + * + * @param Collection $selectedRoles + * @param string[] $warnings + * + * @throws SettingsItemNotFoundException + * @throws Throwable + */ + public function validateRolesMinimumAge(Collection $selectedRoles, User $user, array &$warnings = []): bool + { + $age = $this->queryBus->handle(new SettingDateValueQuery(Settings::SEMINAR_FROM_DATE))->diff($user->getBirthdate())->y; + $canary = true; + foreach ($selectedRoles as $role) { + $min = $role->getMinimumAge(); + if ($min > $age) { + $warnings[] = sprintf($role->getMinimumAgeWarning(), $min, $age); + $canary = false; + } + } + + return $canary; + } + + /** + * Ověří požadovaný maximální věk. + * + * @param Collection $selectedRoles + * @param string[] $warnings + * + * @throws SettingsItemNotFoundException + * @throws Throwable + */ + public function validateRolesMaximumAge(Collection $selectedRoles, User $user, array &$warnings = []): bool + { + $age = $this->queryBus->handle(new SettingDateValueQuery(Settings::SEMINAR_TO_DATE))->diff($user->getBirthdate())->y; + $canary = true; + foreach ($selectedRoles as $role) { + $max = $role->getMaximumAge(); + if ($max > 0 && $max < $age) { // Hodnota 0 je bez omezení + $warnings[] = sprintf($role->getMaximumAgeWarning(), $max, $age); + $canary = false; + } + } + + return $canary; + } + /** * Ověří kapacitu rolí. * @@ -122,27 +171,6 @@ public function validateRolesRegisterable(Collection $selectedRoles, User $user) return true; } - /** - * Ověří požadovaný minimální věk. - * - * @param Collection $selectedRoles - * - * @throws SettingsItemNotFoundException - * @throws Throwable - */ - public function validateRolesMinimumAge(Collection $selectedRoles, User $user): bool - { - $age = $this->queryBus->handle(new SettingDateValueQuery(Settings::SEMINAR_FROM_DATE))->diff($user->getBirthdate())->y; - - foreach ($selectedRoles as $role) { - if ($role->getMinimumAge() > $age) { - return false; - } - } - - return true; - } - /** * Ověří kapacitu podakcí. * diff --git a/app/WebModule/Components/ITroopApplicationContentControlFactory.php b/app/WebModule/Components/ITroopApplicationContentControlFactory.php new file mode 100644 index 000000000..0cc9ac2af --- /dev/null +++ b/app/WebModule/Components/ITroopApplicationContentControlFactory.php @@ -0,0 +1,13 @@ +template; + $template->setFile(__DIR__ . '/templates/troop_application_content.latte'); + + if ($content) { + $template->heading = $content->getHeading(); + } + + $template->backlink = $this->getPresenter()->getHttpRequest()->getUrl()->getPath(); + + $user = $this->getPresenter()->user; + $template->guestRole = $user->isInRole($this->roleRepository->findBySystemName(Role::GUEST)->getName()); + $template->testRole = Role::TEST; + + $this->template->accountNumber = $this->queryBus->handle(new SettingStringValueQuery(Settings::ACCOUNT_NUMBER)); + + $step = $this->getPresenter()->getParameter('step'); + $template->step = $step; + + if ($user->isLoggedIn()) { + $dbuser = $this->userRepository->findById($user->id); + $template->dbuser = $dbuser; + + $template->registrationAllowed = $this->queryBus->handle(new SettingBoolValueQuery(Settings::GROUP_REGISTRATION_ALLOWED)); + + $skautIsUserId = $dbuser->getSkautISUserId(); + $skautIsRoles = $this->skautIsService->getUserRoles($skautIsUserId, self::$ALLOWED_ROLE_TYPES); + $template->skautIsRoles = $skautIsRoles; + + $troop = $this->queryBus->handle(new TroopByLeaderQuery($dbuser->getId())); + if ($troop == null) { + $this->commandBus->handle(new RegisterTroop($dbuser)); + $troop = $this->queryBus->handle(new TroopByLeaderQuery($dbuser->getId())); + } + + $template->troop = $troop; + + if ($step === null) { + $skautIsRoleSelectedId = $this->skautIsService->getUserRoleId(); + $skautIsRoleSelected = array_filter($skautIsRoles, static fn (stdClass $r) => $r->ID === $skautIsRoleSelectedId); + if (empty($skautIsRoleSelected)) { + $template->skautIsRoleSelected = null; + } else { + $template->skautIsRoleSelected = $skautIsRoleSelected[array_keys($skautIsRoleSelected)[0]]; + } + } + } + + $template->render(); + } + + public function renderScripts(): void + { + } + + protected function createComponentGroupMembersForm(): GroupMembersForm + { + $type = $this->getPresenter()->getParameter('type'); + $patrolId = $this->getPresenter()->getParameter('patrol_id'); + if ($patrolId !== null) { + $patrolId = (int) $patrolId; + } + + $form = $this->groupMembersFormFactory->create($type, $patrolId); + + $form->onSave[] = function () use ($type, $patrolId): void { + $this->getPresenter()->redirect('this', ['step' => 'additional_info', 'type' => $type, 'patrol_id' => $patrolId]); + }; + + $form->onError[] = function () use ($type, $patrolId): void { + $p = $this->getPresenter(); + $p->flashMessage('Po potvrzení přihlášky nelze měnit počet účastníků.', 'danger'); + $p->redirect('this', ['step' => 'members', 'type' => $type, 'patrol_id' => $patrolId]); + }; + + $form->onRemoveAll[] = function (): void { + $p = $this->getPresenter(); + $p->redirect('this'); + }; + + return $form; + } + + protected function createComponentGroupAdditionalInfoForm(): GroupAdditionalInfoForm + { + $type = $this->getPresenter()->getParameter('type'); + $patrolId = $this->getPresenter()->getParameter('patrol_id'); + if ($patrolId !== null) { + $patrolId = (int) $patrolId; + } + + $form = $this->groupAdditionalInfoFormFactory->create($type, $patrolId); + + $form->onSave[] = function () use ($type, $patrolId): void { + $this->getPresenter()->redirect('this', ['step' => 'confirm', 'type' => $type, 'patrol_id' => $patrolId]); + }; + + return $form; + } + + protected function createComponentGroupConfirmForm(): GroupConfirmForm + { + $type = $this->getPresenter()->getParameter('type'); + $patrolId = $this->getPresenter()->getParameter('patrol_id'); + if ($patrolId !== null) { + $patrolId = (int) $patrolId; + } + + $form = $this->groupConfirmFormFactory->create($type, $patrolId); + + $form->onSave[] = function (): void { + $this->getPresenter()->redirect('this'); + }; + + return $form; + } + + protected function createComponentTroopConfirmForm(): TroopConfirmForm + { + $form = $this->troopConfirmFormFactory->create(); + + $form->onSave[] = function (): void { + $p = $this->getPresenter(); + $p->flashMessage('Přihláška skupiny byla úspěšně odeslána.', 'success'); + $p->redirect('this'); + }; + + return $form; + } + + public function handleChangeRole(int $roleId): void + { + $this->skautIsService->updateUserRole($roleId); + $this->redirect('this'); + } + + /** + * Vygeneruje potvrzení o přijetí platby. + */ + public function handleGeneratePaymentProof(int $id): void + { + $this->getPresenter()->redirect(':Export:TroopIncomeProof:troop', ['id' => $id]); + } +} diff --git a/app/WebModule/Components/templates/troop_application_content.latte b/app/WebModule/Components/templates/troop_application_content.latte new file mode 100644 index 000000000..7b0a7d94d --- /dev/null +++ b/app/WebModule/Components/templates/troop_application_content.latte @@ -0,0 +1,234 @@ +
    + +
    +
    +
    +

    {$heading}

    +
    +
    + + {if $guestRole} +
    +
    +
    + {_web.application_content.login_required_begin} + {_web.application_content.login_required_link}{_web.application_content.login_required_end} +
    +
    +
    + {elseif empty($skautIsRoles)} + Ve skautISu nemáš dostatečné oprávnění. Registrace vyžaduje přístup k informacím + o členech (typicky admin oddílu/střediska). + {elseif $step === 'members'} + {control groupMembersForm} + {elseif $step === 'additional_info'} + {control groupAdditionalInfoForm} + {elseif $step === 'confirm'} + {control groupConfirmForm} + {else} +
    +
    +
    +

    + Přihláška tvé skupiny byla úspěšně odeslána. Nyní můžeš pouze měnit osobu za osobu. +

    +

    + Abychom s vámi mohli počítat je nutné nejpozději do 30 kalendářních dnů, nejpozději však do 15. + února 2023 (pokud by nastalo dříve) uhradit {$troop->getFee()} Kč účastnického + poplatku za celou skupinu na účet: {$accountNumber} s variabilním symbolem: + {$troop->getVariableSymbolText()}. +

    +

    + Platba za vaši přihlášku byla přijata. + Stáhnout potvrzení platby. +

    +
    +
    +
    + + {if $registrationAllowed || $troop->getState() !== 'draft'} +
    +
    + Pracuješ v roli +
    +
    +
    + + +
    +
    + + + Účastníci do registrace se ti načítají z členů tvé jednotky. + +
    +
    + +

    Skupina

    +
    +
    + Jméno +
    +
    + {$troop->getName()} +
    +
    + +
    +
    + Zodpovědný vedoucí +
    +
    + {$troop->getLeader()->getDisplayName()} +
    + + + Registrující je vždy zodpovědnou osobou. + +
    +
    + +
    +
    + Kód pro spojení do Jamooddílu +
    +
    + {$troop->getPairingCode()} +
    + + + Kód můžeš nasdílet kamarádům z jiných skupin a my se budeme snažit vás spojit do jamoddílu. Pozor, abyste dohromady nepřekročili maximální počet lidí v jamoddílu (42). + +
    +
    + +
    +
    +

    Družiny

    +
    + +
    + + {foreach $troop->getConfirmedPatrols() as $patrol} +
    +
    +
    {$patrol->getName()}
    +
    + +
    +
    +
    + + + + + + {foreach $patrol->getUsersRoles() as $userRole} + + + + + {/foreach} +
    JménoRole
    {$userRole->getUser()->getDisplayName()}{$userRole->getRole()->getName()}
    + +

    + Družina má {$patrol->countUsersInRoles(['attendee', 'patrol_leader'])}/12 účastníků + (z toho {$patrol->countUsersInRoles(['patrol_leader'])}/1 rádců) + a {$patrol->countUsersInRoles(['leader'])}/1 vedoucích. +

    +
    +
    + {else} +
    +
    + + + Začni tím, že přidáš družinu. + +
    +
    + {/foreach} + +
    +
    +

    Dospělí průvodci

    +
    + +
    + + {if !$troop->getConfirmedPatrols()->isEmpty()} +
    +
    + + + + + + {foreach $troop->getUsersRoles() as $userRole} + + + + + {/foreach} +
    JménoRole
    {$userRole->getUser()->getDisplayName()}{$userRole->getRole()->getName()}
    + +

    + Doprovod má {$troop->countUsersInRoles(['escort'])}/{$troop->getMaxEscortsCount()} členů. + Můžeš mít 2× počet družin dospělých (tedy {$troop->getMaxAdultsCount()}, + z toho máš {$troop->countUsersInRoles(['leader'])} vedoucí + a {$troop->countUsersInRoles(['escort'])} doprovody). +

    +
    +
    + {else} +
    +
    + + + Zatím nemáš družinu, bez ní to nepůjde. + +
    +
    + {/if} + +
    +
    +

    Shrnutí

    + {control troopConfirmForm} +
    +
    + {else} +
    +
    +
    + Registrace nové skupiny není v tuto chvíli povolena. +
    +
    +
    + {/if} + {/if} +
    diff --git a/app/WebModule/Forms/ApplicationFormFactory.php b/app/WebModule/Forms/ApplicationFormFactory.php index 2e4a1f122..f058d8cc2 100644 --- a/app/WebModule/Forms/ApplicationFormFactory.php +++ b/app/WebModule/Forms/ApplicationFormFactory.php @@ -5,7 +5,6 @@ namespace App\WebModule\Forms; use App\Model\Acl\Repositories\RoleRepository; -use App\Model\Acl\Role; use App\Model\CustomInput\CustomCheckbox; use App\Model\CustomInput\CustomCheckboxValue; use App\Model\CustomInput\CustomDate; @@ -23,7 +22,6 @@ use App\Model\CustomInput\Repositories\CustomInputRepository; use App\Model\CustomInput\Repositories\CustomInputValueRepository; use App\Model\Enums\Sex; -use App\Model\Settings\Exceptions\SettingsItemNotFoundException; use App\Model\Settings\Queries\SettingStringValueQuery; use App\Model\Settings\Settings; use App\Model\Structure\Repositories\SubeventRepository; @@ -70,6 +68,7 @@ class ApplicationFormFactory { use Nette\SmartObject; + use RolesFormFunctions; /** * Přihlášený uživatel. @@ -435,7 +434,8 @@ private function addRolesSelect(Form $form): void $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'); + ->addRule([$this, 'validateRolesMinimumAge'], 'web.application_content.roles_require_minimum_age') + ->addRule([$this, 'validateRolesMaximumAge'], 'web.application_content.roles_require_maximum_age'); // generovani chybovych hlasek pro vsechny kombinace roli foreach ($this->roleRepository->findFilteredRoles(true, false, true, $this->user) as $role) { @@ -481,16 +481,6 @@ public function validateSubeventsCapacities(MultiSelectBox $field): bool 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í. * @@ -517,55 +507,6 @@ public function validateSubeventsRequired(MultiSelectBox $field, array $args): b 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í. diff --git a/app/WebModule/Forms/GroupAdditionalInfoForm.php b/app/WebModule/Forms/GroupAdditionalInfoForm.php new file mode 100644 index 000000000..5bcb7e062 --- /dev/null +++ b/app/WebModule/Forms/GroupAdditionalInfoForm.php @@ -0,0 +1,147 @@ +template->setFile(__DIR__ . '/templates/group_additional_info_form.latte'); + + $this->resolveUsersRoles(); + + $this->template->type = $this->type; + $this->template->patrolId = $this->patrolId; + $this->template->patrolName = $this->patrolName; + $this->template->usersRoles = $this->usersRoles; + + $this->template->attendeesCountError = $this->attendeesCountError; + $this->template->groupLeadersCountError = $this->groupLeadersCountError; + $this->template->leadersCountError = $this->leadersCountError; + $this->template->escortsCountError = $this->escortsCountError; + + $this->template->render(); + } + + /** + * Vytvoří formulář. + */ + public function createComponentForm(): Form + { + $this->resolveUsersRoles(); + + $form = $this->baseFormFactory->create(); + + foreach ($this->usersRoles as $userRole) { + $form->addTextArea('health_info_' . $userRole->getUser()->getId(), null, null, 3) + ->setDefaultValue($userRole->getUser()->getHealthInfo()); + } + + $form->addSubmit('submit', 'Pokračovat')->setDisabled($this->attendeesCountError || $this->groupLeadersCountError || $this->leadersCountError || $this->escortsCountError); + + $form->setAction($this->getPresenter()->link('this', ['step' => 'additional_info', 'type' => $this->type, 'patrol_id' => $this->patrolId])); + + $form->onSuccess[] = [$this, 'processForm']; + + return $form; + } + + /** + * Zpracuje formulář. + * + * @throws SettingsItemNotFoundException + * @throws Throwable + * @throws MailingMailCreationException + */ + public function processForm(Form $form, stdClass $values): void + { + foreach ($this->usersRoles as $userRole) { + $user = $userRole->getUser(); + $healthInfoInputName = 'health_info_' . $user->getId(); + $user->setHealthInfo($values->$healthInfoInputName); + $this->userRepository->save($user); + } + + $this->onSave(); + } + + private function resolveUsersRoles(): void + { + $user = $this->queryBus->handle(new UserByIdQuery($this->presenter->user->getId())); + $troop = $this->queryBus->handle(new TroopByLeaderQuery($user->getId())); + + if ($this->type == 'patrol') { + if ($this->patrolId !== null) { + $patrol = $this->queryBus->handle(new PatrolByIdQuery($this->patrolId)); + $this->usersRoles = $patrol->getUsersRoles()->toArray(); + } else { + $patrol = $this->queryBus->handle(new PatrolByTroopAndNotConfirmedQuery($troop->getId())); + $this->patrolId = $patrol->getId(); + $this->usersRoles = $patrol->getUsersRoles()->toArray(); + } + + $attendeesCount = $patrol->countUsersInRoles([Role::ATTENDEE, Role::PATROL_LEADER]); + $this->attendeesCountError = $attendeesCount < 4 || $attendeesCount > 12; + $this->groupLeadersCountError = $patrol->countUsersInRoles([Role::PATROL_LEADER]) > 1; + $this->leadersCountError = $patrol->countUsersInRoles([Role::LEADER]) != 1; + + $this->patrolName = $patrol->getName(); + } elseif ($this->type === 'troop') { + $this->usersRoles = $troop->getUsersRoles()->toArray(); + $this->escortsCountError = $troop->countUsersInRoles([Role::ESCORT]) > $troop->getMaxEscortsCount(); + } + + $collator = new Collator('cs_CZ'); + usort($this->usersRoles, static fn ($a, $b) => $collator->compare($a->getUser()->getDisplayName(), $b->getUser()->getDisplayName())); + } +} diff --git a/app/WebModule/Forms/GroupConfirmForm.php b/app/WebModule/Forms/GroupConfirmForm.php new file mode 100644 index 000000000..10aab7ce1 --- /dev/null +++ b/app/WebModule/Forms/GroupConfirmForm.php @@ -0,0 +1,125 @@ +template->setFile(__DIR__ . '/templates/group_confirm_form.latte'); + + $this->resolveUsersRoles(); + + $this->template->type = $this->type; + $this->template->patrolName = $this->patrolName; + $this->template->patrolId = $this->patrolId; + $this->template->usersRoles = $this->usersRoles; + + $this->template->render(); + } + + /** + * Vytvoří formulář. + */ + public function createComponentForm(): Form + { + $this->resolveUsersRoles(); + + $form = $this->baseFormFactory->create(); + + $form->addSubmit('submit', 'Pokračovat'); + + $form->setAction($this->getPresenter()->link('this', ['step' => 'confirm', 'type' => $this->type, 'patrol_id' => $this->patrolId])); + + $form->onSuccess[] = [$this, 'processForm']; + + return $form; + } + + /** + * Zpracuje formulář. + * + * @throws SettingsItemNotFoundException + * @throws Throwable + * @throws MailingMailCreationException + */ + public function processForm(Form $form, stdClass $values): void + { + if ($this->type == 'patrol') { + $this->commandBus->handle(new ConfirmPatrol($this->patrolId)); + } + + $this->onSave(); + } + + private function resolveUsersRoles(): void + { + $user = $this->queryBus->handle(new UserByIdQuery($this->presenter->user->getId())); + $troop = $this->queryBus->handle(new TroopByLeaderQuery($user->getId())); + + if ($this->type == 'patrol') { + if ($this->patrolId !== null) { + $patrol = $this->queryBus->handle(new PatrolByIdQuery($this->patrolId)); + $this->usersRoles = $patrol->getUsersRoles()->toArray(); + } else { + $patrol = $this->queryBus->handle(new PatrolByTroopAndNotConfirmedQuery($troop->getId())); + $this->patrolId = $patrol->getId(); + $this->usersRoles = $patrol->getUsersRoles()->toArray(); + } + + $this->patrolName = $patrol->getName(); + } elseif ($this->type === 'troop') { + $this->usersRoles = $troop->getUsersRoles()->toArray(); + } + + $collator = new Collator('cs_CZ'); + usort($this->usersRoles, static fn ($a, $b) => $collator->compare($a->getUser()->getDisplayName(), $b->getUser()->getDisplayName())); + } +} diff --git a/app/WebModule/Forms/GroupMembersForm.php b/app/WebModule/Forms/GroupMembersForm.php new file mode 100644 index 000000000..18d0c2261 --- /dev/null +++ b/app/WebModule/Forms/GroupMembersForm.php @@ -0,0 +1,278 @@ + */ + private Collection $usersRoles; + + public function __construct( + private string $type, + private ?int $patrolId, + private BaseFormFactory $baseFormFactory, + private QueryBus $queryBus, + private CommandBus $commandBus, + private SkautIsService $skautIsService + ) { + $this->seminarStart = $this->queryBus->handle(new SettingDateValueQuery(Settings::SEMINAR_FROM_DATE)); + + $this->units = $this->skautIsService->getUnitAllUnit(self::$ALLOWED_UNIT_TYPES); + $this->members = []; + + $collator = new Collator('cs_CZ'); + foreach ($this->units as $unit) { + $unitMembers = $this->skautIsService->getMembershipAll($unit->ID, $this->type === 'troop' ? 18 : null, $this->seminarStart); + usort($unitMembers, static fn ($a, $b) => $collator->compare($a->Person, $b->Person)); + $this->members[$unit->ID] = $unitMembers; + } + } + + /** + * Vykreslí komponentu. + */ + public function render(): void + { + $this->template->setFile(__DIR__ . '/templates/group_members_form.latte'); + + $this->resolveUsersRoles(); + + $this->template->type = $this->type; + $this->template->patrolName = $this->patrolName; + $this->template->units = $this->units; + $this->template->members = $this->members; + + $this->template->render(); + } + + /** + * Vytvoří formulář. + */ + public function createComponentForm(): Form + { + $roles = $this->queryBus->handle(new RolesByTypeQuery($this->type)); + $roleSelectOptions = $this->getRoleSelectOptions($roles); + + $this->resolveUsersRoles(); + + $form = $this->baseFormFactory->create(); + + foreach ($this->units as $unit) { + foreach ($this->members[$unit->ID] as $member) { + $register = false; + $role = null; + + if ($this->usersRoles !== null) { + foreach ($this->usersRoles as $usersRole) { + if ($usersRole->getUser()->getSkautISPersonId() === $member->ID_Person) { + $register = true; + $role = $usersRole->getRole(); + break; + } + } + } + + $memberId = $member->ID; + $registerCheckbox = $form->addCheckbox('register_' . $memberId) + ->setDefaultValue($register); + $registerCheckbox + ->addCondition(Form::EQUAL, true) + ->toggle('roleselect-' . $memberId); + + $roleSelect = $form->addSelect('role_' . $memberId, null, $roleSelectOptions) + ->setHtmlId('roleselect-' . $memberId) + ->setHtmlAttribute('class', 'form-control-sm ignore-bs-select'); + if ($role != null) { + $roleSelect->setDefaultValue($role->getId()); + } + + $birthdate = new DateTimeImmutable($member->Birthday); + $age = $this->countAgeAt($birthdate, $this->seminarStart); + foreach ($roles as $r) { + if ($age < $r->getMinimumAge()) { + $roleSelect + ->addConditionOn($registerCheckbox, Form::FILLED) + ->addCondition(Form::EQUAL, $r->getId()) + ->addRule(Form::NOT_EQUAL, sprintf($r->getMinimumAgeWarning() ?: 'Osobě je %2$d, ale musí být min. %1$d let.', $r->getMinimumAge(), $age), $r->getId()); + } elseif ($age > $r->getMaximumAge()) { + $roleSelect + ->addConditionOn($registerCheckbox, Form::FILLED) + ->addCondition(Form::EQUAL, $r->getId()) + ->addRule(Form::NOT_EQUAL, sprintf($r->getMaximumAgeWarning() ?: 'Osobě je %2$d, ale musí být max. %1$d let.', $r->getMaximumAge(), $age), $r->getId()); + } + } + } + } + + $form->addSubmit('submit', 'Pokračovat'); + + $form->setAction($this->getPresenter()->link('this', ['step' => 'members', 'type' => $this->type, 'patrol_id' => $this->patrolId])); + + $form->onSuccess[] = [$this, 'processForm']; + + return $form; + } + + /** + * Zpracuje formulář. + * + * @throws SettingsItemNotFoundException + * @throws Throwable + * @throws MailingMailCreationException + */ + public function processForm(Form $form, stdClass $values): void + { + $selectedPersons = []; + + foreach ($this->units as $unit) { + foreach ($this->members[$unit->ID] as $member) { + $memberId = $member->ID; + $registerInputName = 'register_' . $memberId; + $roleInputName = 'role_' . $memberId; + if ($values->$registerInputName) { + $selectedPersons[] = ['roleId' => $values->$roleInputName, 'personId' => $member->ID_Person]; + } + } + } + + if ($this->troop->getState() !== TroopApplicationState::DRAFT) { + if ($this->type === 'patrol') { + $patrol = $this->queryBus->handle(new PatrolByIdQuery($this->patrolId)); + $usersCount = $patrol->getUsersRoles()->count(); + } else { + $usersCount = $this->troop->getUsersRoles()->count(); + } + + if ($usersCount !== count($selectedPersons)) { + $this->onError(); + + return; + } + } + + $this->commandBus->handle(new UpdateGroupMembers($this->type, $this->troop->getId(), $this->patrolId, $selectedPersons)); + + if (empty($selectedPersons)) { + $this->onRemoveAll(); + } + + $this->onSave(); + } + + /** + * @param Role[] $roles + * + * @return string[] + */ + private function getRoleSelectOptions($roles): array + { + $options = []; + + foreach ($roles as $role) { + $options[$role->getId()] = $role->getName(); + } + + return $options; + } + + private function countAgeAt(DateTimeImmutable $birthdate, DateTimeImmutable $seminarStart): int + { + return $seminarStart->diff($birthdate)->y; + } + + private function resolveUsersRoles(): void + { + $user = $this->queryBus->handle(new UserByIdQuery($this->presenter->user->getId())); + $troop = $this->queryBus->handle(new TroopByLeaderQuery($user->getId())); + $this->troop = $troop; + + if ($this->type === 'patrol') { + if ($this->patrolId !== null) { + $patrol = $this->queryBus->handle(new PatrolByIdQuery($this->patrolId)); + } else { + $patrol = $this->queryBus->handle(new PatrolByTroopAndNotConfirmedQuery($troop->getId())); + } + + if ($patrol != null) { + $this->usersRoles = $patrol->getUsersRoles(); + $this->patrolId = $patrol->getId(); + $this->patrolName = $patrol->getName(); + } else { + $this->usersRoles = new ArrayCollection(); + } + } elseif ($this->type === 'troop') { + $this->usersRoles = $troop->getUsersRoles(); + } + } +} diff --git a/app/WebModule/Forms/IGroupAdditionalInfoFormFactory.php b/app/WebModule/Forms/IGroupAdditionalInfoFormFactory.php new file mode 100644 index 000000000..3bcc8fdcc --- /dev/null +++ b/app/WebModule/Forms/IGroupAdditionalInfoFormFactory.php @@ -0,0 +1,16 @@ +addRule([$this, 'validateRolesCapacities'], 'web.profile.roles_capacity_occupied') ->addRule([$this, 'validateRolesRegisterable'], 'web.profile.roles_not_registerable') ->addRule([$this, 'validateRolesMinimumAge'], 'web.application_content.roles_require_minimum_age') + ->addRule([$this, 'validateRolesMaximumAge'], 'web.application_content.roles_require_maximum_age') ->setDisabled(! $this->applicationService->isAllowedEditRegistration($this->user)); foreach ($this->roleRepository->findFilteredRoles(true, false, true, $this->user) as $role) { @@ -144,7 +145,8 @@ public function create(int $id): Form 'id' => $id, 'roles' => $this->roleRepository->findRolesIds($this->user->getRoles()), ]); - $form->onSuccess[] = [$this, 'processForm']; + $form->onSuccess[] = [$this, 'processForm']; + $form->onValidate[] = [$this, 'validateRolesAgeLimits']; return $form; } @@ -163,63 +165,4 @@ public function processForm(Form $form, stdClass $values): void $this->applicationService->cancelRegistration($this->user, ApplicationState::CANCELED, $this->user); } } - - /** - * Ověří kapacitu rolí. - */ - public function validateRolesCapacities(MultiSelectBox $field): bool - { - $selectedRoles = $this->roleRepository->findRolesByIds($field->getValue()); - - return $this->validators->validateRolesCapacities($selectedRoles, $this->user); - } - - /** - * 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 vyž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); - } } diff --git a/app/WebModule/Forms/RolesFormFunctions.php b/app/WebModule/Forms/RolesFormFunctions.php new file mode 100644 index 000000000..3c0c873b1 --- /dev/null +++ b/app/WebModule/Forms/RolesFormFunctions.php @@ -0,0 +1,109 @@ +roleRepository->findRolesByIds($values->roles); + $minWarnings = []; + $this->validators->validateRolesMinimumAge($selectedRoles, $this->user, $minWarnings); + foreach ($minWarnings as $error) { + $form->addError($error); + } + + // Max a Min se kontroluje zvlášť, protože rozdíl může být jednou vůči FROM_DATE a pak TO_DATE + $maxWarnings = []; + $this->validators->validateRolesMaximumAge($selectedRoles, $this->user, $maxWarnings); + foreach ($maxWarnings as $error) { + $form->addError($error); + } + } + + /** + * 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); + } + + /** + * Ověří požadovaný maximální věk. + * + * @throws SettingsItemNotFoundException + * @throws Throwable + */ + public function validateRolesMaximumAge(MultiSelectBox $field): bool + { + $selectedRoles = $this->roleRepository->findRolesByIds($field->getValue()); + + return $this->validators->validateRolesMaximumAge($selectedRoles, $this->user); + } + + /** + * Ověří kapacitu rolí. + */ + public function validateRolesCapacities(MultiSelectBox $field): bool + { + $selectedRoles = $this->roleRepository->findRolesByIds($field->getValue()); + + return $this->validators->validateRolesCapacities($selectedRoles, $this->user); + } + + /** + * 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ěří registrovatelnost rolí. + */ + public function validateRolesRegisterable(MultiSelectBox $field): bool + { + $selectedRoles = $this->roleRepository->findRolesByIds($field->getValue()); + + return $this->validators->validateRolesRegisterable($selectedRoles, $this->user); + } + + /** + * Ověří výběr vyž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); + } +} diff --git a/app/WebModule/Forms/TroopApplicationForm.php b/app/WebModule/Forms/TroopApplicationForm.php new file mode 100644 index 000000000..0bc3a5e75 --- /dev/null +++ b/app/WebModule/Forms/TroopApplicationForm.php @@ -0,0 +1,153 @@ +template->setFile(__DIR__ . '/templates/contact_form.latte'); + $this->template->render(); + } + + /** + * Vytvoří formulář. + */ + public function createComponentForm(): Form + { + $this->user = $this->userRepository->findById($this->presenter->user->getId()); + + $form = $this->baseFormFactory->create(); + + $nameText = $form->addText('name', 'web.contact_form_content.name') + ->addRule(Form::FILLED, 'web.contact_form_content.name_empty'); + + $emailText = $form->addText('email', 'web.contact_form_content.email') + ->addRule(Form::FILLED, 'web.contact_form_content.email_empty') + ->addRule(Form::EMAIL, 'web.contact_form_content.email_format'); + + $form->addTextArea('message', 'web.contact_form_content.message') + ->addRule(Form::FILLED, 'web.contact_form_content.message_empty'); + + $form->addCheckbox('sendCopy', 'web.contact_form_content.send_copy'); + + if ($this->user === null) { + $field = new ReCaptchaField($this->recaptchaProvider); + $field->addRule(Form::FILLED, 'web.contact_form_content.recaptcha_empty'); + $form->addComponent($field, 'recaptcha'); + } + + $form->addSubmit('submit', 'web.contact_form_content.send_message'); + + if ($this->user !== null) { + $nameText->setDisabled(); + $emailText->setDisabled(); + + $form->setDefaults([ + 'name' => $this->user->getDisplayName(), + 'email' => $this->user->getEmail(), + ]); + } + + $form->onSuccess[] = [$this, 'processForm']; + + return $form; + } + + /** + * Zpracuje formulář. + * + * @throws SettingsItemNotFoundException + * @throws Throwable + * @throws MailingMailCreationException + */ + public function processForm(Form $form, stdClass $values): void + { + $recipientsUsers = new ArrayCollection(); + $recipientsEmails = new ArrayCollection(); + + if ($this->user) { + $senderName = $this->user->getDisplayName(); + $senderEmail = $this->user->getEmail(); + if ($values->sendCopy) { + $recipientsUsers->add($this->user); + } + } else { + $senderName = $values->name; + $senderEmail = $values->email; + if ($values->sendCopy) { + $recipientsEmails->add($senderEmail); + } + } + + $recipients = $this->queryBus->handle(new SettingArrayValueQuery(Settings::CONTACT_FORM_RECIPIENTS)); + foreach ($recipients as $recipient) { + $recipientsEmails->add($recipient); + } + + $this->mailService->sendMailFromTemplate( + $recipientsUsers, + $recipientsEmails, + Template::CONTACT_FORM, + [ + TemplateVariable::SEMINAR_NAME => $this->queryBus->handle(new SettingStringValueQuery(Settings::SEMINAR_NAME)), + TemplateVariable::SENDER_NAME => $senderName, + TemplateVariable::SENDER_EMAIL => $senderEmail, + TemplateVariable::MESSAGE => str_replace(["\n", "\r"], '', nl2br($values->message, false)), + ] + ); + + $this->onSave(); + } +} diff --git a/app/WebModule/Forms/TroopConfirmForm.php b/app/WebModule/Forms/TroopConfirmForm.php new file mode 100644 index 000000000..585a69a2d --- /dev/null +++ b/app/WebModule/Forms/TroopConfirmForm.php @@ -0,0 +1,141 @@ +template->setFile(__DIR__ . '/templates/troop_confirm_form.latte'); + + $this->resolveTroop(); + + $this->template->troop = $this->troop; + $this->template->agreement = $this->queryBus->handle(new SettingStringValueQuery(Settings::APPLICATION_AGREEMENT)); + + $this->template->allCountError = $this->allCountError; + $this->template->duplicitUsersError = $this->duplicitUsersError; + $this->template->userNotLeaderError = $this->userNotLeaderError; + $this->template->userLeaderAndEscortError = $this->userLeaderAndEscortError; + + $this->template->render(); + } + + /** + * Vytvoří formulář. + */ + public function createComponentForm(): Form + { + $this->resolveTroop(); + + $form = $this->baseFormFactory->create(); + + $pairedTroopCodeText = $form->addText('pairedTroopCode') + ->setDefaultValue($this->troop->getPairedTroopCode()); + + $agreementCheckbox = $form->addCheckbox('agreement', 'Potvrzuji, že jsem si přečetl(a) a souhlasím s podmínkami akce Národní skautské jamboree 2023 a s tím, že v případě porušení těchto podmínek mohu být z akce vyloučen(a) bez náhrady.') + ->addRule(Form::FILLED, 'Musíš souhlasit s podmínkami akce.'); + + $submit = $form->addSubmit('submit', 'Závazně registrovat') + ->setDisabled($this->allCountError || $this->duplicitUsersError || $this->userNotLeaderError || $this->userLeaderAndEscortError); + + if ($this->troop->getState() !== TroopApplicationState::DRAFT) { + $pairedTroopCodeText->setHtmlAttribute('readonly'); + $agreementCheckbox->setDisabled(); + $submit->setDisabled(); + } + + $form->setAction($this->getPresenter()->link('this')); + + $form->onSuccess[] = [$this, 'processForm']; + + return $form; + } + + /** + * Zpracuje formulář. + * + * @throws SettingsItemNotFoundException + * @throws Throwable + * @throws MailingMailCreationException + */ + public function processForm(Form $form, stdClass $values): void + { + $this->commandBus->handle(new ConfirmTroop($this->troop->getId(), $values->pairedTroopCode)); + + $this->onSave(); + } + + private function resolveTroop(): void + { + $user = $this->queryBus->handle(new UserByIdQuery($this->presenter->user->getId())); + $this->troop = $this->queryBus->handle(new TroopByLeaderQuery($user->getId())); + + $allCount = $this->troop->countUsersInRoles([Role::ATTENDEE, Role::PATROL_LEADER]); + $this->allCountError = $allCount > 42; + + $countFromPatrols = 0; + foreach ($this->troop->getConfirmedPatrols() as $patrol) { + $countFromPatrols += $patrol->countUsersInRoles([Role::ATTENDEE, Role::PATROL_LEADER]); + } + + $countFromTroops = $this->troop->countUsersInRoles([Role::ATTENDEE, Role::PATROL_LEADER]); + $this->duplicitUsersError = $countFromPatrols !== $countFromTroops; + + $this->userNotLeaderError = $user->getGroupRoles() + ->filter(static fn (UserGroupRole $groupRole) => $groupRole->getRole()->getSystemName() === Role::LEADER && $groupRole->getPatrol()->isConfirmed()) + ->count() === 0; + + $leadersCount = $this->troop->countUsersInRoles([Role::LEADER]); + $escortsCount = $this->troop->countUsersInRoles([Role::ESCORT]); + $leadersOrEscortsCount = $this->troop->countUsersInRoles([Role::LEADER, Role::ESCORT]); + $this->userLeaderAndEscortError = $leadersCount + $escortsCount !== $leadersOrEscortsCount; + } +} diff --git a/app/WebModule/Forms/templates/group_additional_info_form.latte b/app/WebModule/Forms/templates/group_additional_info_form.latte new file mode 100644 index 000000000..59e0263df --- /dev/null +++ b/app/WebModule/Forms/templates/group_additional_info_form.latte @@ -0,0 +1,78 @@ +{form form class => form-horizontal} +
    +
    + {if $type === 'patrol'} +

    Úprava družiny {$patrolName}

    + {else} +

    Úprava dospělých průvodců

    + {/if} +

    2. Doplň údaje

    +
    +
    + +
    +
    + + + Počet účastníků musí být 4-12. + +
    +
    + + + Počet rádců musí být 0-1. + +
    +
    + + + Vedoucí musí být právě 1. + +
    +
    + + + Počet doprovodů je příliš vysoký. + +
    +
    + + {foreach $usersRoles as $userRole} + {var $user = $userRole->getUser()} +
    +
    +
    {$user->getDisplayName()}
    +
    {$userRole->getRole()->getName()}
    +
    +
    + {$user->getStreet()},
    + {$user->getPostcode()} {$user->getCity()}
    +
    + Telefon: {$user->getPhone()}
    + E-mail: {$user->getEmail()}
    + Datum narození: {$user->getBirthdate()|date:'j. n. Y'}
    + {if $user->getMotherPhone() !== null} +
    + Telefon matky: {$user->getMotherPhone()} ({$user->getMotherName()}) + {/if} + {if $user->getFatherPhone() !== null} +
    + Telefon otce: {$user->getFatherPhone()} ({$user->getFatherName()}) + {/if} +
    +
    + Zdravotní omezení, alergie, pravidelně užívané léky
    + {input 'health_info_' . $user->getId()} +
    +
    +
    +
    + {/foreach} + +
    +
    + Zpět + {input submit class => 'btn-primary button'} +
    +
    +{/form} diff --git a/app/WebModule/Forms/templates/group_confirm_form.latte b/app/WebModule/Forms/templates/group_confirm_form.latte new file mode 100644 index 000000000..fad217d0f --- /dev/null +++ b/app/WebModule/Forms/templates/group_confirm_form.latte @@ -0,0 +1,52 @@ +{form form class => form-horizontal} +
    +
    + {if $type === 'patrol'} +

    Úprava družiny {$patrolName}

    + {else} +

    Úprava dospělých průvodců

    + {/if} +

    3. Potvrď

    +
    +
    + +
    + {foreach $usersRoles as $userRole} + {var $user = $userRole->getUser()} +
    +
    +
    +
    {$user->getDisplayName()}
    +
    {$userRole->getRole()->getName()}
    + {$user->getStreet()},
    + {$user->getPostcode()} {$user->getCity()}
    +
    + Telefon: {$user->getPhone()}
    + E-mail: {$user->getEmail()}
    + Datum narození: {$user->getBirthdate()|date:'j. n. Y'}
    + {if $user->getMotherPhone() !== null} +
    + Telefon matky: {$user->getMotherPhone()} ({$user->getMotherName()})
    + {/if} + {if $user->getFatherPhone() !== null} +
    + Telefon otce: {$user->getFatherPhone()} ({$user->getFatherName()})
    + {/if} + {if $user->getHealthInfo()} +
    + Zdravotní omezení, alergie, pravidelně užívané léky:
    +

    {$user->getHealthInfo()|breakLines}

    + {/if} +
    +
    +
    + {/foreach} +
    + +
    +
    + Zpět + {input submit class => 'btn-primary button'} +
    +
    + {/form} diff --git a/app/WebModule/Forms/templates/group_members_form.latte b/app/WebModule/Forms/templates/group_members_form.latte new file mode 100644 index 000000000..77774daa6 --- /dev/null +++ b/app/WebModule/Forms/templates/group_members_form.latte @@ -0,0 +1,64 @@ +{form form class => form-horizontal} + + +
    +
    + {if $type === 'patrol'} +

    Úprava družiny {$patrolName}

    + {else} +

    Úprava dospělých průvodců

    + {/if} +

    1. Vyber účastníky

    +
    +
    + +
    +
    + Oddíl +
    +
    + +
    +
    + + {foreach $units as $unit} + + + + + + + + {foreach $members[$unit->ID] as $member} + + + + + + + {else} + + + + {/foreach} +
    RegistrovatDružinaJménoRole
    {input 'register_' . $member->ID}{if $unit->ID !== $member->ID_Unit}{$member->Unit}{/if}{$member->Person}{input 'role_' . $member->ID}
    Oddíl nemá žádné členy, které je možné vybrat.
    + {/foreach} + +
    +
    + Zpět + {input submit class => 'btn-primary button'} +
    +
    + {/form} diff --git a/app/WebModule/Forms/templates/troop_confirm_form.latte b/app/WebModule/Forms/templates/troop_confirm_form.latte new file mode 100644 index 000000000..7667a6a8c --- /dev/null +++ b/app/WebModule/Forms/templates/troop_confirm_form.latte @@ -0,0 +1,480 @@ +{form form class => form-horizontal} +
    +
    + Kód pro spojení do jamooddílu +
    +
    + {input pairedTroopCode} + + + Můžeš vložit kód jiné skupiny, se kterou chceš být v jamoddílu společně. + +
    +
    + +
    +
    + Počet účastníků +
    +
    + {$troop->countUsersInRoles(['attendee', 'patrol_leader', 'leader', 'escort'])}/42 + ({$troop->countUsersInRoles(['attendee'])} účastníků, + {$troop->countUsersInRoles(['patrol_leader'])} rádci, + {$troop->countUsersInRoles(['leader']) + $troop->countUsersInRoles(['escort'])} vedoucí a dospělí) + +
    + + + Počet účastníků ve skupině musí být maximálně 42. + +
    + +
    + + + Účastníci a rádci nesmí být zároveň ve více družinách. + +
    + +
    + + + Registrující uživatel musí být vedoucím některé družiny. + +
    + +
    + + + Vedoucí družiny nesmí být zároveň doprovod. + +
    +
    +
    + +
    +
    + Cena +
    +
    + {$troop->getFee() == null ? $troop->countFee() : $troop->getFee()} Kč +
    + + + Cena je splatná do 30 dnů od odeslání registrace. + +
    +
    + +
    +
    +
    +
    Obecná ustanovení
    +
      +
    • + Pořadatelem se rozumí Junák – český skaut, z. s., sídlem Senovážné nám. 24, Praha 1, dále jen + Junák – český skaut nebo Pořadatel. +
    • +
    • + Národní skautské jamboree 2023 organizované Pořadatelem v areálu Park 360 v Hradci Králové se + koná: +
        +
      1. pro mladší účastníky a vedoucí mladších účastníků v termínu 3. - 8. 5. 2023 a
      2. +
      3. pro členy servis týmu a pořadatelského týmu v prodlouženém termínu 29. 4. - + 9. 5. 2023 (prodlouženo o dny přípravy a balení akce v místě konání), +
      4. +
      + dále jen NSJ 2023 či Akce. +
    • +
    • + Není-li konkrétně uvedeno jinak, za účastníky akce se považují všichni mladší + účastníci i jejich vedoucí, stejně tak členové servis týmu a pořadatelského týmu akce, + dále jen Účastník. +
    • +
    • + Účastníci jsou povinni během akce dodržovat Provozní podmínky areálu Park 360, + které budou Pořadatelem zveřejněny nejpozději v den začátku akce na místě jejího + konání. +
    • +
    • + Pořadatel si vyhrazuje právo na úpravu těchto podmínek a pravidel, včetně + podmínek registrace, dojde-li k podstatným změnám okolností organizace akce. O + změnách bude Pořadatel účastníky informovat elektronickou cestou. +
    • +
    + +
    Základní pravidla akce
    +
      +
    • + Budu se řídit skautským zákonem. +
    • +
    • + Budu respektovat zákony České republiky i vnitřní právo Junáka - českého skauta. +
    • +
    • + Budu se řídit pravidly akce i pokyny, které vydá Pořadatel. +
    • +
    • + Po dobu trvání NSJ 2023 nebudu konzumovat alkohol ani jiné omamné látky. +
    • +
    • + Během konání NSJ 2023 nebudu kouřit. Pokud to však nepůjde jinak, tak budu kouřit + jen na vyhrazených místech a jen pokud je kouření v mém věku legální. +
    • +
    • + Budu ohleduplný a tolerantní k ostatním. +
    • +
    • + Budu dle nejlepšího svědomí a vědomí reprezentovat Junák – český skaut, svůj oddíl + a středisko i celé skautské hnutí a to nejen na akci, ale i mimo ni (například při + cestování). +
    • +
    • + Budu respektovat případné aktuálně platné podmínky a omezení vzhledem + k epidemiologické situaci nemoci covid-19. +
    • +
    • + Souhlasím s platebními i se storno podmínkami (viz níže). +
    • +
    • + Beru na vědomí a souhlasím, že v případě porušení pravidel akce můžu být jako + účastník z NSJ 2023 vyloučen bez nároku na vrácení účastnického poplatku. + Vedoucí mladších členů je pak povinen zajistit dopravu vyloučeného účastníka + neprodleně a na vlastní náklad. +
    • +
    + +
    Podmínky účasti
    +
      +
    • + NSJ 2023 si klade za jeden z cílů podporu družinového systému, a tak se mladší + účastníci na akci registrují a akce účastní v rámci družin (patrol), ideálně složených z + členů existující družiny, případně členů jednoho oddílu, méně často pak v družině + složené ze členů různých oddílů. +
    • +
    • + Družiny (patroly) se registrují do jamoddílů (samy, nebo jsou později pořadatelem + přiřazeny), ve kterých jsou i dospělí účastníci (dospělí průvodci družin nebo dospělí + vedoucí mladších členů, tj. dospělí členové vedení oddílů). Každá družina musí mít + přiřazenu jednu dospělou osobu, přitom v rámci jamoddílu může být tato jedna + dospělá osoba případně přiřazena i k více družinám současně. Na NSJ 2023 je tedy + snadno možné vyslat i více družin z jednoho oddílu či střediska, které bude + doprovázet jeden či více dospělých členů daného oddílu či střediska. +
    • +
    • + Jamoddíl je tvořen typicky 38 až 42 mladšími účastníky a k nim dalšími dospělými + účastníky (dospělí průvodci družin či dospělí vedoucí mladších členů). Počet + dospělých u jamoddílu je omezen maximálně na “počet družin krát dva”, kdy do + tohoto celkového počtu se započítává i každá dospělá osoba, která je už v rámci + registrace povinně k družině přiřazena. +
    • +
    • + Členové servis týmu a pořadatelského týmu akce se na akci vždy registrují + samostatně, tedy jako jednotlivé osoby. +
    • +
    • + Každý účastník akce musí být v době konání akce řádným členem Junáka - českého + skauta (tedy musí být evidován ve skautISu jako řádný, příp. čestný, člen v některé z + jednotek). +
    • +
    • + Přihlašování všech účastníků na akci probíhá elektronickou formou v rámci systému + na webové stránce akce www.nsj2023.cz ve zveřejněných termínech určených k + registraci na akci pro jednotlivé typy účastníků. Každý účastník musí být řádně na + akci přihlášen a v rámci přihlášky musí mít uvedeny všechny povinné údaje úplně a + pravdivě. +
    • +
    • + Pro účast na akci musí mít každý účastník uhrazen celý účastnický poplatek + odpovídající typu daného účastníka (viz účastnický poplatek a platba). +
    • +
    • + Nutná komunikace s účastníky před akcí probíhá čistě elektronicky prostřednictvím + e-mailové adresy zadané v přihlášce. +
    • +
    • + Výjimku z podmínek účasti či pravidel registrace na akci může ve zvláštních + případech udělit Pořadatel, typicky přímo vedoucí NSJ 2023. +
    • +
    +
    Mladší účastník (členové a rádci družin)
    +
      +
    • + Mladí lidé ve věku od 10 do 16 let (tj. ti, kteří se narodili 8. května 2007 a později, + nejpozději však 2. května 2013) se mohou zúčastnit NSJ 2023 jako členové družin + (patrol). +
    • +
    • V rámci každé družiny (patroly) se může účastnit jeden mladší účastník ve věku od + 10 do 16 let, případně do 18 let (tj. ti, kteří se narodili 8. května 2005 a později, + nejpozději však 2. května 2013), jako rádce družiny. +
    • +
    • Mladší účastníci se registrují a akce účastní ve družinách o 4 až 12 mladších členech + (vč. případného rádce družiny). +
    • +
    • S účastí mladšího člena na akci musí souhlasit jeho zákonný zástupce, tento souhlas + musí být následně vyznačen v přihlášce na akci. Za vyplnění a pravdivost všech + údajů mladšího člena odpovídá vedoucí mladších účastníků přiřazený k dané družině + (patrole). +
    • +
    • Mladší účastník se účastní NSJ 2023 po celou dobu akce v termínu pro mladší + účastníky a vedoucí mladších účastníků. +
    • +
    • V případě nutné komunikace s členy družiny (či jamoddílu) probíhá komunikace před + akcí prostřednictvím přiřazeného vedoucího mladších účastníků a to čistě + elektronicky prostřednictvím e-mailové adresy zadané v přihlášce. +
    • +
    + +
    Vedoucí mladších účastníků (průvodci družin, dospělí členové jamoddílů)
    +
      +
    • Dospělí lidé starší 18 let (tj. narození před 3. květnem 2005), kteří se chtějí zúčastnit + NSJ 2023 spolu s mladšími účastníky, se mohou registrovat jako vedoucí mladších + účastníků (průvodci družin, dospělí členové jamoddílů) a musí být vždy přiřazeni ke + konkrétní družině (v rámci jamoddílu). +
    • +
    • Jestliže v běžné skautské činnosti nějaký dospělý člen působí jako tzv. “rádce + družiny”, pak se NSJ 2023 může účastnit jako dospělý průvodce družiny (v rámci + jamoddílu), tedy vedoucí mladších účastníků. Na NSJ 2023 nebude označován jako + rádce, neboť rádci jsou na akci pouze mladší členové. +
    • +
    • Vedoucí mladších účastníků, kteří družiny doprovázejí (v rámci jamoddílu), jsou + povinni o mladší účastníky pečovat a podporovat je, jsou odpovědní za jejich + chování, bezpečnost a pohodu na akci i během cest. +
    • +
    • Vedoucí mladších účastníků jsou povinni zajistit pro sebe a pro mladší členy v + družině (či družinách), ke které jsou v rámci jamoddílu přiřazeni, dopravu na a z akce + a také stravování v průběhu celé akce. Pořadatel zajistí na akci vhodné podmínky + pro přípravu a konzumaci jídla. Zajištění potravin ani platba za ně, stejně tak doprava + na a z akce, není součástí účastnického poplatku. +
    • +
    • Za úplnost a správnost údajů v registraci na akci uvedených v přihlášce své osoby i + mladších členů družiny, odpovídá vedoucí mladších členů, který je k ní přiřazen. + Stejně tak odpovídá za včasné a úplné uhrazení příslušných účastnických poplatků. +
    • +
    • Vedoucí mladších účastníků se účastní NSJ 2023 po celou dobu akce v termínu pro + mladší účastníky a vedoucí mladších účastníků. +
    • +
    + +
    Člen servis týmu a pořadatelského týmu
    +
      +
    • Jednotlivci starší 18 let, respektive starší 15 let (tj. narození před 28. dubnem 2008), + kteří se chtějí účastnit NSJ 2023 a pomoci s pořádáním akce, se mohou registrovat + jako členové servis týmu (dále ST) nebo členové pořadatelského týmu. +
    • +
    • Člen ST vykonává činnosti potřebné pro úspěšné zajištění akce dle svých + schopností, dovedností i znalostí a dle domluvy s odpovědnými členy pořadatelského + týmu. Zvláště členům ST mladším 18 let nemusí být umožněno zapojit se do + zajišťování všech typů příprav či zajišťování všech typů programů (některé činnosti + mohou vyžadovat plnoletost či speciální způsobilost). +
    • +
    • Člen ST pomáhá zajišťovat program pro mladší účastníky a účastní se na zajištění + provozu NSJ 2023 v době konání akce. Navíc se, v souladu se svou přihláškou člena + ST, účastní přípravy akce před akcí samotnou a balení akce po jejím konci. +
    • +
    • Člen pořadatelského týmu se přímo podílí na organizaci akce samotné, rozděluje + úkoly a odpovídá za ně, řídí práci ST v oblasti, která je mu k zajištění akce svěřena + Pořadatelem. +
    • +
    • Člen ST i pořadatelského týmu má v příslušném termínu celého konání akce v + účastnickém poplatku zahrnuto a zajištěno Pořadatelem stravování. +
    • +
    • V rámci registrace na akci člen ST či pořadatelského týmu musí v přihlášce řádně + vyznačit v jakém konkrétním termínu před a po akci se bude účastnit příprav a balení + akce. U členů ST mladších 18 let musí být také pravdivě vyznačeno, že zákonný + zástupce osoby souhlasí s účastí na akci. Za vyplnění a pravdivost údajů, stejně tak + za včasnou a úplnou úhradu účastnického poplatku, odpovídá přímo osoba, která se + na akci registruje. +
    • +
    • Člen ST či pořadatelského týmu se účastní NSJ 2023 v termínu akce pro členy + servistýmu a pořadatelského týmu v souladu s údaji uvedenými v přihlášce. +
    • +
    + +
    Účastnický poplatek, termíny registrace a platby
    +
      +
    • Vyplněním a odesláním elektronické přihlášky se účastník zavazuje, že v řádných + termínech uhradí příslušný účastnický poplatek a souhlasí s těmito podmínkami. +
    • +
    • Přihláška účastníka se stává závaznou až uhrazením a připsáním celého + účastnického poplatku na bankovní účet Pořadatele. Dokud není celá částka + uhrazena a tím přihláška závazná, může ji Pořadatel dle svého uvážení kdykoli + zrušit. +
    • +
    • V případě, že nebude uhrazen účastnický poplatek do doby splatnosti, bude + registrace účastníka (či účastníků v rámci přihlášené družiny / jamoddílu) + automaticky zrušena bez nároku na vratku již uhrazené části účastnického poplatku, + nebude-li domluveno jinak. +
    • +
    + +
    Mladší účastník a vedoucí mladších účastníků (průvodci družin, dospělí členové jamoddílů)
    +
      +
    • Účastnický poplatek mladšího člena či vedoucího mladších členů je při závazném + odeslání přihlášky nejpozději do 4. prosince 2022 ve výši 1.300,-Kč za osobu a celé + trvání akce, při pozdějším odeslání přihlášky pak ve výši 1.600,-Kč za osobu za celé + trvání akce. +
    • +
    • Po závazném odeslání přihlášky družiny (mladších účastníků) včetně k družině + přiřazených dospělých účastníků je nutné nejpozději do 30 kalendářních dnů, + nejpozději však do 15. února 2023 (pokud by nastalo dříve), dle e-mailem + obdržených platebních informací uhradit celý účastnický poplatek za všechny + přihlášené členy družiny i přiřazené dospělé účastníky na bankovní účet Pořadatele. +
    • +
    • Podrobné platební informace k úhradě účastnických poplatků obdrží vedoucí + mladších účastníků na e-mailovou adresu uvedenou v přihlášce na akci. +
    • +
    • Pokud se některý z přihlášených účastníků nemůže akce zúčastnit, může nejpozději + do 15. dubna 2023 přiřazený dospělý vedoucí mladších účastníků v registračním + systému takovou osobu zaměnit za jinou. Pozdější změny už nemusí být + Pořadatelem umožněny. +
    • +
    • Upozornění: Účastnický poplatek mladších účastníků a vedoucích mladších + účastníků nezahrnuje jídlo během akce ani dopravu na akci a z ní zpět. +
    • +
    + +
    Člen servis týmu a pořadatelského týmu
    +
      +
    • Účastnický poplatek pro členy servistýmu a pořadatelského týmu je při závazném + odeslání přihlášky nejpozději do 4. prosince 2022 ve výši 650,-Kč za osobu, při + pozdějším odeslání přihlášky pak ve výši 800,-Kč za osobu za celé trvání akce i dny + před akcí a po akci určené k přípravě a ukončení akce na místě konání. +
    • +
    • Po závazném odeslání přihlášky člena servis týmu nebo pořadatelského týmu je + nutné do 14 kalendářních dnů, nejpozději však do 15. února 2023 (pokud by nastalo + dříve), dle e-mailem obdržených platebních informací uhradit celý účastnický + poplatek za osobu na bankovní účet Pořadatele. +
    • +
    • Podrobné platební informace k úhradě účastnického poplatku obdrží přihlášená + osoba na e-mailovou adresu uvedenou v přihlášce na akci. +
    • +
    • Účastnický poplatek členů ST a pořadatelského týmu již zahrnuje stravování po dobu + akce a to včetně dnů před a po akci určených k přípravě a ukončení akce na místě + konání. Dopravu na místo akce a zpět si hradí každý účastník samostatně. +
    • +
    + +
    Storno podmínky
    +
      +
    • Po zaplacení účastnického poplatku se přihláška stává závaznou a případné změny + podléhají těmto storno podmínkám. +
    • +
    • Pořadatel má právo zrušit NSJ 2023 v plném rozsahu, pokud nastane závažná + nepředvídaná okolnost. Za případy závažných nepředvídaných okolností se považují + zejména případy, kdy realizace NSJ 2023: +
        +
      1. + není možná, +
      2. +
      3. + je možná pouze za cenu výrazně zvýšených bezpečnostních, zdravotních, + finančních nebo právních rizik spojených s realizací akce nebo za cenu neúměrného + zvýšení finančních nebo organizačních nákladů na realizaci akce. + Jedná se zejména o případy přírodní katastrofy, občanských nepokojů, výrazného + zvýšení rizik nebo opatření příslušných orgánů, která výrazně omezují nebo ztěžují + plánovaný průběh NSJ 2023. Posouzení, zda takové okolnosti nastaly, provede + Pořadatel a následně bude informovat přihlášené účastníky. +
      4. +
      +
    • +
    • Pořadatel má také právo zrušit registraci či účast i pouze některým účastníkům, + pokud nastane závažná nepředvídatelná okolnost, která se týká pouze těchto + účastníků (např. zákaz vlády platící pouze pro účastníky z určitého kraje). +
    • +
    • Jestliže Pořadatel zruší NSJ 2023 nebo zruší registraci či účast pouze některým + účastníkům, bude dotčeným účastníkům vrácena částka ve výši poměrné části + zůstatku rozpočtu po úhradě všech nezbytných nákladů spojených s přípravou + a/nebo zrušením akce. Tato částka bude vrácena do tří měsíců od zrušení a to + bezhotovostním převodem na účet, ze kterého byl účastnický poplatek původně + uhrazen, nebude-li domluveno jinak. +
    • +
    • V případě porušení podmínek ze strany účastníka ztrácí tento účastník jakýkoliv + nárok na vrácení účastnického poplatku. Pokud toto porušení způsobí, že skupina, + ve které je účastník přihlášen, přestane splňovat podmínky registrace, může být tato + skupina z akce rovněž vyloučena bez nároku na vrácení peněz. +
    • +
    • Pokud neúčast (zrušení účasti apod.) jednoho člena znamená, že skupina přestane + splňovat tyto podmínky, může být její registrace považována za neplatnou a může + být ukončena. Ukončení bude posuzováno případ od případu na základě dopadu této + změny na družinu, jamoddíl a celou akci. V případě ukončení nemá skupina nárok na + vrácení účastnického poplatku. +
    • +
    • Jestliže účastník z jakéhokoliv důvodu zruší registraci po zaplacení celého + účastnického poplatku, ztrácí jakýkoliv nárok na vrácení účastnického poplatku. Po + schválení Pořadatelem však může být povolena změna účastníka ve skupině a + účastnický poplatek může být převeden na nového účastníka. I po této změně musí + skupina splňovat podmínky účasti. +
    • +
    • V případě, že se účastník nezúčastní z vážných zdravotních důvodů a informuje o + tom Pořadatele nejpozději v den začátku akce, může Pořadatel rozhodnout o vrácení + části účastnického poplatku. +
    • +
    + +
    Zpracování a ochrana osobních údajů a souhlasy účastníků (GDPR)
    +
      +
    • Osobní a citlivé údaje všech účastníků jsou zpracovávány Pořadatelem v souladu s + Nařízením Evropského parlamentu a Rady 2016/679 (tzv. GDPR, dále jen nařízení) + a se Zákonem č. 110/2019 Sb., o zpracování osobních údajů, v účinném znění (dále + jen zákon) a dalšími souvisejícími předpisy, v rozsahu nezbytném pro zajištění akce. +
    • +
    • Výslovně souhlasím se zpracováním mých předaných osobních a citlivých údajů + Pořadateli za účelem možnosti účasti na NSJ 2023 a zdárného průběhu akce. +
    • +
    • Beru na vědomí a souhlasím s tím, že Pořadatel v souvislosti s akcí zpracuje + především tyto osobní údaje: identifikační údaje – jméno, příjmení, datum narození, + rodné číslo, přezdívka, případné číslo osobního dokladu; kontaktní údaje na + účastníka a kontaktní osobu (v případě nezletilosti účastníka kontaktní údaje jeho + zákonného zástupce) – kontaktní adresa, telefonická spojení, e-mail. +
    • +
    • Výslovně souhlasím s tím, že Pořadatel v souvislosti s akcí zpracuje i údaje o + zdravotním stavu a dietních omezení účastníka, které zákon označuje jako citlivé + údaje. Jedná se o údaje nezbytné pro posouzení zdravotního stavu při účasti na NSJ + 2023 a též jako informace potřebné v případě nutnosti ošetření. Pro zpracování + těchto citlivých údajů uděluji svůj výslovný souhlas, s vědomím, že tento souhlas + může být kdykoliv odvolán. +
    • +
    • Beru dále na vědomí, že zpracování osobních údajů je v Junák – český skaut dále + upraveno vnitřními předpisy, aby tak byla zajištěna jejich ochrana před zneužitím. + Údaje mohou být v nutném rozsahu zpřístupněny činovníkům Junáka - českého + skauta. +
    • +
    • Výše uvedené osobní údaje budou zpracovávány po dobu přípravy a trvání akce. Na + základě právních povinností jsou některé osobní údaje zpracovávány i po skončení + akce a to po dobu, která je stanovena právními předpisy (např. v souvislosti s + vedením účetnictví). +
    • +
    • Prohlašuji, že veškeré své osobní a citlivé údaje poskytuji dobrovolně. Beru na + vědomí, že na základě písemné žádosti je Junák – český skaut povinen poskytnout + mi informace o osobních a citlivých údajích zpracovávaných o mé osobě, a to jednou + za kalendářní rok bezplatně, jinak kdykoli za přiměřenou úhradu. +
    • +
    • Výslovně dále souhlasím se zpracováním podobizny, obrazových snímků, + obrazových a zvukových záznamů (především fotografií a videí) vznikajících při + dokumentaci činností souvisejících s NSJ 2023; zejména k jejich dalšímu použití v + rámci činnosti Junáka – českého skauta. +
    • +
    • S ohledem na skutečnost, že všichni účastníci akce musí být zároveň členy Junáka - + českého skauta, lze nalézt případné další podrobnosti ke zpracování osobních údajů + i v poučení na přihlášce člena do organizace, neboť těmito pravidly se řídí i + nakládání s osobními údaji v souvislosti s pořádáním akce. +
    • +
    +
    +
    +
    + +
    +
    +
    + {input agreement} +
    +
    +
    +
    + {input submit class => 'btn-primary button'} +
    +
    +
    +{/form} diff --git a/app/WebModule/Presenters/PagePresenter.php b/app/WebModule/Presenters/PagePresenter.php index 5008119fb..a8d006957 100644 --- a/app/WebModule/Presenters/PagePresenter.php +++ b/app/WebModule/Presenters/PagePresenter.php @@ -27,6 +27,7 @@ use App\WebModule\Components\IProgramsContentControlFactory; use App\WebModule\Components\ISlideshowContentControlFactory; use App\WebModule\Components\ITextContentControlFactory; +use App\WebModule\Components\ITroopApplicationContentControlFactory; use App\WebModule\Components\IUsersContentControlFactory; use App\WebModule\Components\LectorsContentControl; use App\WebModule\Components\NewsContentControl; @@ -35,6 +36,7 @@ use App\WebModule\Components\ProgramsContentControl; use App\WebModule\Components\SlideshowContentControl; use App\WebModule\Components\TextContentControl; +use App\WebModule\Components\TroopApplicationContentControl; use App\WebModule\Components\UsersContentControl; use Nette\Application\BadRequestException; use Nette\DI\Attributes\Inject; @@ -48,6 +50,9 @@ class PagePresenter extends WebBasePresenter #[Inject] public IApplicationContentControlFactory $applicationContentControlFactory; + #[Inject] + public ITroopApplicationContentControlFactory $troopApplicationContentControlFactory; + #[Inject] public IBlocksContentControlFactory $blocksContentControlFactory; @@ -132,6 +137,11 @@ protected function createComponentApplicationContent(): ApplicationContentContro return $this->applicationContentControlFactory->create(); } + protected function createComponentTroopApplicationContent(): TroopApplicationContentControl + { + return $this->troopApplicationContentControlFactory->create(); + } + protected function createComponentBlocksContent(): BlocksContentControl { return $this->blocksContentControlFactory->create(); diff --git a/app/assets/common/main.js b/app/assets/common/main.js index 43dd6dfa2..79247a19c 100644 --- a/app/assets/common/main.js +++ b/app/assets/common/main.js @@ -98,6 +98,7 @@ function initSelects() { .not('.datagrid .row-group-actions select') .not('.datagrid .col-per-page select') .not('.modal-body select') + .not('.ignore-bs-select') .add('select[multiple]') .selectpicker({ noneSelectedText: 'Nic není vybráno', diff --git a/app/lang/admin.cs_CZ.neon b/app/lang/admin.cs_CZ.neon index 96be25b97..3e3a601c1 100644 --- a/app/lang/admin.cs_CZ.neon +++ b/app/lang/admin.cs_CZ.neon @@ -32,6 +32,10 @@ common: role_option: "{0} %role% (%count% uživatelů)|{1} %role% (%count% uživatel)|[2,4] %role% (%count% uživatelé)|[5,Inf[ %role% (%count% uživatelů)" subevent_option: "{0} %subevent% (%count% uživatelů)|{1} %subevent% (%count% uživatel)|[2,4] %subevent% (%count% uživatelé)|[5,Inf[ %subevent% (%count% uživatelů)" + export_note: "Exportují se všechny řádky nebo ty právě zobrazené. Sloupce viz tlačítko vpravo." + export_all: "Export všech" + export_filter: "Export zobrazených" + menu: dashboard: "Úvod" cms: "Web" @@ -381,6 +385,11 @@ program: export_schedule: "Stáhnout harmonogram" users: + menu: + persons: "Osoby" + troops: "Skupiny" + patrols: "Družiny" + users_heading: "Uživatelé" users_name: "Jméno" users_photo: "Fotka" @@ -505,6 +514,12 @@ users: users_detail_schedule: "Harmonogram" users_detail_birthdate_age: "%birthdate% (%age% let)" + troops: + heading: Skupiny + + patrols: + heading: Družiny + payments: payments: heading: "Platby" @@ -518,7 +533,8 @@ payments: account_number: "Číslo protiúčtu" account_name: "Název protiúčtu" message: "Zpráva pro příjemce" - paired_applications: "Spárované přihlášky" + paired_applications: "Spárované osoby" + paired_troops: "Spárované skupiny" state: "Stav" saved: "Platba byla úspěšně uložena." delete_confirm: "Opravdu chcete platbu odstranit?" @@ -580,6 +596,15 @@ acl: note: "Účastník musí minimálního věku dosáhnout první den akce." error_format: "Zadejte číslo." error_low: "Minimální věk nesmí být menší než 0." + custom_msg_label: "Vlastní chybová hláška k [↑]" + custom_msg_note: "Vlastní text chyby, která se zobrazí při nedosažení věku pro tuto roli. Možné použít %d" + maximum_age: + label: "Maximální věk" + note: "Účastník nesmí maximální věk překročit první den akce." + error_format: "Zadejte číslo." + error_low: "Maximální věk nesmí být menší než 0." + custom_msg_label: "Vlastní chybová hláška k [↑]" + custom_msg_note: "Vlastní text chyby, která se zobrazí při překročení věku pro tuto roli. Možné použít %d" mailing: menu: diff --git a/app/lang/common.cs_CZ.neon b/app/lang/common.cs_CZ.neon index 187c69e62..824208f48 100644 --- a/app/lang/common.cs_CZ.neon +++ b/app/lang/common.cs_CZ.neon @@ -34,6 +34,7 @@ content: image: "Obrázek" document: "Dokumenty" application: "Přihlašovací formulář" + troop_application: "Přihlašovací formulář skupiny" html: "HTML box" faq: "FAQ" news: "Aktuality" @@ -52,6 +53,7 @@ content: image: "" document: "Dokumenty" application: "Přihlašovací formulář" + troop_application: "Přihlašovací formulář skupiny" html: "" faq: "FAQ" news: "Aktuality" @@ -117,6 +119,7 @@ application_state: canceled: "Zrušeno" paid: "Zaplaceno" paid_free: "Zaplaceno (zdarma)" + draft: "Nepotvrzeno" calendar_view: timeGridSeminar: "Na výšku" @@ -185,12 +188,14 @@ mailing: template_type: sign_in: "Potvrzení přihlášení přes skautIS" registration: "Potvrzení registrace" + troop_registration: "Potvrzení přihlášení skupiny" registration_canceled: "Potvrzení zrušení registrace" registration_canceled_not_paid: "Potvrzení zrušení registrace (nezaplaceno)" registration_approved: "Potvrzení schválení registrace" roles_changed: "Potvrzení změny rolí" subevents_changed: "Potvrzení změny podakcí" payment_confirmed: "Potvrzení přijetí platby" + troop_payment_confirmed: "Potvrzení přijetí platby skupiny" maturity_reminder: "Připomínka splatnosti" program_registered: "Přihlášení na program" program_unregistered: "Odhlášení z programu" diff --git a/app/lang/web.cs_CZ.neon b/app/lang/web.cs_CZ.neon index 6960bff20..9e5a77086 100644 --- a/app/lang/web.cs_CZ.neon +++ b/app/lang/web.cs_CZ.neon @@ -36,8 +36,9 @@ profile: incompatible_roles_selected: "Není možné kombinovat roli %role% s rolemi: %incompatibleRoles%." roles_empty: "Musí být vybrána alespoň jedna role." roles_not_registerable: "Registrace do některé z rolí již není možná." - roles_capacity_occupied: "Všechna místa v některé roli jsou obsazena." roles_require_minimum_age: "Některá role vyžaduje vyšší věk." + roles_require_maximum_age: "Některá role vyžaduje nižší věk." + roles_capacity_occupied: "Všechna místa v některé roli jsou obsazena." change_roles: "Změnit role" change_roles_confirm: "Pokud nová role vyžaduje schválení organizátora, budete Vám nastavena role \"Neschválený\". Opravdu chcete pokračovat?" change_roles_disabled: "Změna rolí již není možná nebo nejste na seminář registrováni." @@ -166,6 +167,7 @@ application_content: roles_not_registerable: "Registrace do některé z rolí již není možná." roles_capacity_occupied: "Všechna místa v některé roli jsou obsazena." roles_require_minimum_age: "Některá role vyžaduje vyšší věk." + roles_require_maximum_age: "Některá role vyžaduje nižší věk." agreement_empty: "Musíte souhlasit s poskytnutím údajů." register: "Registrovat" register_synchronization_failed: "Synchronizace se skautIS se nepodařila. Zkuste se znovu přihlásit a upravit údaje v profilu." diff --git a/build.xml b/build.xml index 41ad07f5c..e891c3d1a 100644 --- a/build.xml +++ b/build.xml @@ -183,6 +183,11 @@ + + + + + diff --git a/migrations/Version20221015133440.php b/migrations/Version20221015133440.php new file mode 100644 index 000000000..8b36b5d89 --- /dev/null +++ b/migrations/Version20221015133440.php @@ -0,0 +1,38 @@ +abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + + $this->addSql('CREATE TABLE patrol (id INT AUTO_INCREMENT NOT NULL, troop_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, INDEX IDX_BFB2371263060AC (troop_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE troop (id INT AUTO_INCREMENT NOT NULL, variable_symbol_id INT DEFAULT NULL, payment_id INT DEFAULT NULL, income_proof_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, fee INT NOT NULL, application_date DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\', maturity_date DATE DEFAULT NULL COMMENT \'(DC2Type:date_immutable)\', payment_method VARCHAR(255) DEFAULT NULL, payment_date DATE DEFAULT NULL COMMENT \'(DC2Type:date_immutable)\', state VARCHAR(255) NOT NULL, INDEX IDX_FAAD534C25813A9D (variable_symbol_id), INDEX IDX_FAAD534C4C3A3BB (payment_id), INDEX IDX_FAAD534CFE69EDFB (income_proof_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE user_group_role (id INT AUTO_INCREMENT NOT NULL, user_id INT DEFAULT NULL, troop_id INT DEFAULT NULL, patrol_id INT DEFAULT NULL, role_id INT DEFAULT NULL, INDEX IDX_D95417F6A76ED395 (user_id), INDEX IDX_D95417F6263060AC (troop_id), INDEX IDX_D95417F6A7B49BA9 (patrol_id), INDEX IDX_D95417F6D60322AC (role_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE patrol ADD CONSTRAINT FK_BFB2371263060AC FOREIGN KEY (troop_id) REFERENCES troop (id)'); + $this->addSql('ALTER TABLE troop ADD CONSTRAINT FK_FAAD534C25813A9D FOREIGN KEY (variable_symbol_id) REFERENCES variable_symbol (id)'); + $this->addSql('ALTER TABLE troop ADD CONSTRAINT FK_FAAD534C4C3A3BB FOREIGN KEY (payment_id) REFERENCES payment (id)'); + $this->addSql('ALTER TABLE troop ADD CONSTRAINT FK_FAAD534CFE69EDFB FOREIGN KEY (income_proof_id) REFERENCES income_proof (id)'); + $this->addSql('ALTER TABLE user_group_role ADD CONSTRAINT FK_D95417F6A76ED395 FOREIGN KEY (user_id) REFERENCES user (id)'); + $this->addSql('ALTER TABLE user_group_role ADD CONSTRAINT FK_D95417F6263060AC FOREIGN KEY (troop_id) REFERENCES troop (id)'); + $this->addSql('ALTER TABLE user_group_role ADD CONSTRAINT FK_D95417F6A7B49BA9 FOREIGN KEY (patrol_id) REFERENCES patrol (id)'); + $this->addSql('ALTER TABLE user_group_role ADD CONSTRAINT FK_D95417F6D60322AC FOREIGN KEY (role_id) REFERENCES role (id)'); + $this->addSql('ALTER TABLE role ADD type VARCHAR(255) NOT NULL, ADD minimum_age_warning VARCHAR(255) DEFAULT NULL, ADD maximum_age INT NOT NULL, ADD maximum_age_warning VARCHAR(255) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + } +} diff --git a/migrations/Version20221017191553.php b/migrations/Version20221017191553.php new file mode 100644 index 000000000..27e23c8fc --- /dev/null +++ b/migrations/Version20221017191553.php @@ -0,0 +1,28 @@ +abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + + $this->addSql('CREATE TABLE troop_application_content (id INT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE troop_application_content ADD CONSTRAINT FK_9E479B84BF396750 FOREIGN KEY (id) REFERENCES content (id) ON DELETE CASCADE'); + } + + public function down(Schema $schema): void + { + } +} diff --git a/migrations/Version20221021195625.php b/migrations/Version20221021195625.php new file mode 100644 index 000000000..9a08ad5fa --- /dev/null +++ b/migrations/Version20221021195625.php @@ -0,0 +1,30 @@ +abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + + $this->addSql('ALTER TABLE troop ADD leader_id INT DEFAULT NULL, ADD pairing_code VARCHAR(255) NOT NULL, ADD paired_troop_code VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE troop ADD CONSTRAINT FK_FAAD534C73154ED4 FOREIGN KEY (leader_id) REFERENCES user (id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_FAAD534C73154ED4 ON troop (leader_id)'); + $this->addSql('ALTER TABLE user ADD phone VARCHAR(255) DEFAULT NULL, ADD mother_name VARCHAR(255) DEFAULT NULL, ADD mother_phone VARCHAR(255) DEFAULT NULL, ADD father_name VARCHAR(255) DEFAULT NULL, ADD father_phone VARCHAR(255) DEFAULT NULL, ADD health_info LONGTEXT DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + } +} diff --git a/migrations/Version20221022182325.php b/migrations/Version20221022182325.php new file mode 100644 index 000000000..3f031f9ee --- /dev/null +++ b/migrations/Version20221022182325.php @@ -0,0 +1,32 @@ +abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + + $this->addSql('ALTER TABLE patrol ADD confirmed TINYINT(1) NOT NULL'); + $this->addSql('UPDATE role SET type=\'individual\', maximum_age=150'); + $this->addSql('UPDATE role SET type=\'patrol\', minimum_age=10, maximum_age=16 WHERE name=\'Účastník\''); + $this->addSql('INSERT INTO `role` (`name`, `system_name`, `system_role`, `registerable`, `approved_after_registration`, `synced_with_skaut_is`, `occupancy`, `minimum_age`, `type`, `maximum_age`) VALUES (\'Rádce\', \'patrol_leader\', 1, 1, 1, 0, 0, 10, \'patrol\', 17)'); + $this->addSql('INSERT INTO `role` (`name`, `system_name`, `system_role`, `registerable`, `approved_after_registration`, `synced_with_skaut_is`, `occupancy`, `minimum_age`, `type`, `maximum_age`) VALUES (\'Vedoucí\', \'leader\', 1, 1, 1, 0, 0, 18, \'patrol\', 150)'); + $this->addSql('INSERT INTO `role` (`name`, `system_name`, `system_role`, `registerable`, `approved_after_registration`, `synced_with_skaut_is`, `occupancy`, `minimum_age`, `type`, `maximum_age`) VALUES (\'Dospělý doprovod\', \'escort\', 1, 1, 1, 0, 0, 18, \'troop\', 150)'); + } + + public function down(Schema $schema): void + { + } +} diff --git a/migrations/Version20221101001721.php b/migrations/Version20221101001721.php new file mode 100644 index 000000000..692bd520e --- /dev/null +++ b/migrations/Version20221101001721.php @@ -0,0 +1,34 @@ +addSql('INSERT INTO `mail_template` (`id`, `type`, `subject`, `text`, `active`, `system_template`) VALUES (15, \'troop_registration\', \'Registrace na akci NSJ2023!\', \'Ahoj, děkujeme za registraci na NSJ2023. Tento e-mail je potvrzením, že registrace tvé skupiny byla řádně uložena do systému. Abychom s vámi mohli počítat je nutné nejpozději do 30 kalendářních dnů, nejpozději však do 15. února 2023 (pokud by nastalo dříve) uhradit %poplatek% Kč účastnického poplatku za celou skupinu na účet: %cislo-uctu% s variabilním symbolem: %variabilni-symbol%. S platbou prosím neotálej. Svou skupinu můžeš spravovat po přihlášení na nsj2023.cz. Pokud by se vyskytly jakékoliv potíže, ozvi se nám na registrace@nsj2023.cz. Těšíme se! Tým NSJ2023\', \'1\', \'0\')'); + + $this->addSql('INSERT INTO `template_template_variable` (`template_id`, `template_variable_id`) VALUES (\'15\', \'1\')'); + $this->addSql('INSERT INTO `template_template_variable` (`template_id`, `template_variable_id`) VALUES (\'15\', \'5\')'); + $this->addSql('INSERT INTO `template_template_variable` (`template_id`, `template_variable_id`) VALUES (\'15\', \'9\')'); + $this->addSql('INSERT INTO `template_template_variable` (`template_id`, `template_variable_id`) VALUES (\'15\', \'10\')'); + $this->addSql('INSERT INTO `template_template_variable` (`template_id`, `template_variable_id`) VALUES (\'15\', \'11\')'); + } + + public function down(Schema $schema): void + { + } +} diff --git a/migrations/Version20221126184510.php b/migrations/Version20221126184510.php new file mode 100644 index 000000000..574b6839c --- /dev/null +++ b/migrations/Version20221126184510.php @@ -0,0 +1,30 @@ +addSql('INSERT INTO `mail_template` (`id`, `type`, `subject`, `text`, `active`, `system_template`) VALUES (16, \'troop_payment_confirmed\', \'Potvrzení platby akce NSJ2023!\', \'Ahoj, potvrzujeme přijetí platby za tvou skupinu na NSJ2023. Potvrzení platby si můžeš stáhnout po přihlášení.\', \'1\', \'0\')'); + + $this->addSql('INSERT INTO `template_template_variable` (`template_id`, `template_variable_id`) VALUES (\'16\', \'1\')'); + } + + public function down(Schema $schema): void + { + } +} diff --git a/migrations/Version20230303221015.php b/migrations/Version20230303221015.php new file mode 100644 index 000000000..540369ff8 --- /dev/null +++ b/migrations/Version20230303221015.php @@ -0,0 +1,27 @@ +abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + + $this->addSql('INSERT INTO `settings` (`item`, `value`) VALUES (\'group_registration_allowed\', \'0\')'); + } + + public function down(Schema $schema): void + { + } +} diff --git a/phpstan.neon b/phpstan.neon index 333e1067c..6ca9b53a3 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -14,6 +14,9 @@ parameters: - message: '#^Parameter \#1 \$translator of method Ublaboo\\DataGrid\\DataGrid::setTranslator\(\) expects Nette\\Localization\\ITranslator, Nette\\Localization\\Translator given.$#' path: app/*/*GridControl.php + - + message: '#^If condition is always false.$#' + path: app/Services/ExcelExportService.php services: - class: CodeQuality\ObjectIdentityComparisonRule