From 29a4e5d554deba1c96e92db693964bff35011171 Mon Sep 17 00:00:00 2001 From: Lung Date: Thu, 21 Sep 2023 18:53:19 +0200 Subject: [PATCH] added troops --- public/styles.css | 15 +++ src/Application/Route.php | 31 ++++- src/FlashMessages/FlashMessagesBySession.php | 1 + src/Middleware/CheckLeaderParticipants.php | 71 +++++++++++ .../CheckPatrolLeaderParticipants.php | 46 ------- .../PatrolLeadersOnlyMiddleware.php | 2 +- src/Middleware/TroopLeadersOnlyMiddleware.php | 38 ++++++ .../TroopParticipantsOnlyMiddleware.php | 38 ++++++ src/Participant/Admin/AdminController.php | 8 -- src/Participant/Participant.php | 6 +- src/Participant/ParticipantController.php | 5 + src/Participant/ParticipantService.php | 67 ++++++++++ .../Patrol/PatrolParticipantRepository.php | 1 - src/Participant/Patrol/PatrolService.php | 1 + src/Participant/Troop/TroopController.php | 118 ++++++++++++++++++ .../Troop/TroopLeaderRepository.php | 20 +++ .../Troop/TroopParticipantRepository.php | 26 ++++ src/Participant/Troop/TroopService.php | 46 +++++++ src/Payment/PaymentService.php | 23 +++- src/Settings/TwigExtension.php | 11 ++ src/Templates/cs.yaml | 36 +++++- .../translatable/admin/open-admin.twig | 1 - .../translatable/admin/payments-admin.twig | 21 ---- src/Templates/translatable/delete-tp.twig | 18 +++ .../translatable/participant/dashboard.twig | 117 ++++++++++++----- .../translatable/widgets/troopTieForm.twig | 19 +++ .../translatable/widgets/userCustomHelp.twig | 8 +- 27 files changed, 671 insertions(+), 123 deletions(-) create mode 100755 src/Middleware/CheckLeaderParticipants.php delete mode 100755 src/Middleware/CheckPatrolLeaderParticipants.php create mode 100755 src/Middleware/TroopLeadersOnlyMiddleware.php create mode 100755 src/Middleware/TroopParticipantsOnlyMiddleware.php create mode 100755 src/Participant/Troop/TroopController.php create mode 100644 src/Templates/translatable/delete-tp.twig create mode 100644 src/Templates/translatable/widgets/troopTieForm.twig diff --git a/public/styles.css b/public/styles.css index 3f07c5c7..61f2ef25 100755 --- a/public/styles.css +++ b/public/styles.css @@ -276,6 +276,13 @@ li.step.is-active::after { margin-bottom: 1rem; } +.form-group-middle { + display: flex; + flex-flow: column; + align-items: center; + row-gap: 10px; +} + .form-control { border: none; box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3); @@ -306,10 +313,18 @@ li.step.is-active::after { color: var(--color-accent); } +.hightlight { + color: var(--color-accent); +} + .btn[type=submit] { width: 100%; } +.btn-small[type=submit] { + width: inherit; +} + .btn-mini[type=submit] { width: inherit; } diff --git a/src/Application/Route.php b/src/Application/Route.php index 92d692e1..643f5fa7 100755 --- a/src/Application/Route.php +++ b/src/Application/Route.php @@ -7,16 +7,19 @@ use kissj\Event\EventController; use kissj\Export\ExportController; use kissj\Middleware\AdminsOnlyMiddleware; -use kissj\Middleware\CheckPatrolLeaderParticipants; +use kissj\Middleware\CheckLeaderParticipants; use kissj\Middleware\ChoosedRoleOnlyMiddleware; use kissj\Middleware\LoggedOnlyMiddleware; use kissj\Middleware\NonChoosedRoleOnlyMiddleware; use kissj\Middleware\NonLoggedOnlyMiddleware; use kissj\Middleware\OpenStatusOnlyMiddleware; use kissj\Middleware\PatrolLeadersOnlyMiddleware; +use kissj\Middleware\TroopLeadersOnlyMiddleware; +use kissj\Middleware\TroopParticipantsOnlyMiddleware; use kissj\Participant\Admin\AdminController; use kissj\Participant\ParticipantController; use kissj\Participant\Patrol\PatrolController; +use kissj\Participant\Troop\TroopController; use kissj\User\UserController; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; @@ -85,7 +88,7 @@ public function addRoutesInto(App $app): App $app->group('/patrol', function (RouteCollectorProxy $app) { $app->get('/participant/{participantId}/show', PatrolController::class . '::showParticipant') - ->setName('p-show'); + ->setName('p-show'); // TODO check if CheckLeaderParticipants is needed here $app->group('', function (RouteCollectorProxy $app) { $app->get('/closeRegistration', PatrolController::class . '::showCloseRegistration') @@ -115,9 +118,31 @@ public function addRoutesInto(App $app): App $app->post('/delete', PatrolController::class . '::deleteParticipant') ->setName('p-delete'); - })->add(CheckPatrolLeaderParticipants::class); + })->add(CheckLeaderParticipants::class); })->add(OpenStatusOnlyMiddleware::class); })->add(PatrolLeadersOnlyMiddleware::class); + + $app->group('/troop', function (RouteCollectorProxy $app) { + $app->post('/tieParticipantToTroopByLeader', TroopController::class . '::tieParticipantToTroopByLeader') + ->setName('tie-tp-by-tl') + ->add(TroopLeadersOnlyMiddleware::class) + ->add(OpenStatusOnlyMiddleware::class); + + $app->post('/tieParticipantToTroopByParticipant', TroopController::class . '::tieParticipantToTroopByParticipant') + ->setName('tie-tp-by-tp') + ->add(TroopParticipantsOnlyMiddleware::class); + + $app->group('/participant/{participantId}', function (RouteCollectorProxy $app) { + $app->get('/show', TroopController::class . '::showParticipant') + ->setName('tp-show'); + + $app->get('/showUntie', TroopController::class . '::showUntieParticipant') + ->setName('tp-showUntie'); + + $app->post('/untie', TroopController::class . '::untieParticipant') + ->setName('tp-untie'); + })->add(CheckLeaderParticipants::class); + }); // TODO refactor for patrols $app->group('/participant', function (RouteCollectorProxy $app) { diff --git a/src/FlashMessages/FlashMessagesBySession.php b/src/FlashMessages/FlashMessagesBySession.php index 37d81542..ef6174ef 100644 --- a/src/FlashMessages/FlashMessagesBySession.php +++ b/src/FlashMessages/FlashMessagesBySession.php @@ -4,6 +4,7 @@ namespace kissj\FlashMessages; +// TODO add translator to this class and make all messages translatable class FlashMessagesBySession implements FlashMessagesInterface { public function info(string $message): void diff --git a/src/Middleware/CheckLeaderParticipants.php b/src/Middleware/CheckLeaderParticipants.php new file mode 100755 index 00000000..df5cae1a --- /dev/null +++ b/src/Middleware/CheckLeaderParticipants.php @@ -0,0 +1,71 @@ +getRoute(); + if ($route === null) { + throw new \RuntimeException('Cannot access route in CheckLeaderParticipants middleware'); + } + + $participantId = (int)$route->getArgument('participantId'); + $leader = $this->participantRepository->getParticipantFromUser($this->getUser($request)); + + if ($leader instanceof PatrolLeader) { + if (!$this->patrolService->patrolParticipantBelongsPatrolLeader( + $this->patrolService->getPatrolParticipant($participantId), + $leader, + )) { + $this->flashMessages->error($this->translator->trans('flash.error.wrongPatrol')); + + return $this->createRedirectResponse($request, 'dashboard'); + } + } elseif ($leader instanceof TroopLeader) { + if (!$this->troopService->troopParticipantBelongsTroopLeader( + $this->troopParticipantRepository->get($participantId), + $leader, + )) { + $this->flashMessages->error($this->translator->trans('flash.error.wrongTroop')); + + return $this->createRedirectResponse($request, 'dashboard'); + } + } else { + $this->flashMessages->error($this->translator->trans('flash.error.notLeader')); + + return $this->createRedirectResponse($request, 'dashboard'); + } + + return $handler->handle($request); + } +} diff --git a/src/Middleware/CheckPatrolLeaderParticipants.php b/src/Middleware/CheckPatrolLeaderParticipants.php deleted file mode 100755 index 6fe9e810..00000000 --- a/src/Middleware/CheckPatrolLeaderParticipants.php +++ /dev/null @@ -1,46 +0,0 @@ -getRoute(); - if ($route === null) { - throw new \RuntimeException('Cannot access route in CheckPatrolLeaderParticipatns middleware'); - } - - $participantId = (int)$route->getArgument('participantId'); - if (!$this->patrolService->patrolParticipantBelongsPatrolLeader( - $this->patrolService->getPatrolParticipant($participantId), - $this->patrolService->getPatrolLeader($this->getUser($request)), - )) { - $this->flashMessages->error($this->translator->trans('flash.error.wrongPatrol')); - - return $this->createRedirectResponse($request, 'dashboard'); - } - - return $handler->handle($request); - } -} diff --git a/src/Middleware/PatrolLeadersOnlyMiddleware.php b/src/Middleware/PatrolLeadersOnlyMiddleware.php index 013fe9bb..4c41347b 100755 --- a/src/Middleware/PatrolLeadersOnlyMiddleware.php +++ b/src/Middleware/PatrolLeadersOnlyMiddleware.php @@ -30,7 +30,7 @@ public function process(Request $request, ResponseHandler $handler): Response ) { $this->flashMessages->error($this->translator->trans('flash.error.plOnly')); - return $this->createRedirectResponse($request, 'loginAskEmail'); + return $this->createRedirectResponse($request, 'landing'); } return $handler->handle($request); diff --git a/src/Middleware/TroopLeadersOnlyMiddleware.php b/src/Middleware/TroopLeadersOnlyMiddleware.php new file mode 100755 index 00000000..461c2802 --- /dev/null +++ b/src/Middleware/TroopLeadersOnlyMiddleware.php @@ -0,0 +1,38 @@ +getAttribute('user'); + + if ( + $user instanceof User + && $this->participantRepository->getParticipantFromUser($user)->role !== ParticipantRole::TroopLeader + ) { + $this->flashMessages->error($this->translator->trans('flash.error.tlOnly')); + + return $this->createRedirectResponse($request, 'landing'); + } + + return $handler->handle($request); + } +} diff --git a/src/Middleware/TroopParticipantsOnlyMiddleware.php b/src/Middleware/TroopParticipantsOnlyMiddleware.php new file mode 100755 index 00000000..73081e2d --- /dev/null +++ b/src/Middleware/TroopParticipantsOnlyMiddleware.php @@ -0,0 +1,38 @@ +getAttribute('user'); + + if ( + $user instanceof User + && $this->participantRepository->getParticipantFromUser($user)->role !== ParticipantRole::TroopParticipant + ) { + $this->flashMessages->error($this->translator->trans('flash.error.tpOnly')); + + return $this->createRedirectResponse($request, 'landing'); + } + + return $handler->handle($request); + } +} diff --git a/src/Participant/Admin/AdminController.php b/src/Participant/Admin/AdminController.php index 64e6a8c3..6fd31df1 100755 --- a/src/Participant/Admin/AdminController.php +++ b/src/Participant/Admin/AdminController.php @@ -274,12 +274,6 @@ public function showPayments( $event, $user, ), - 'approvedTroopParticipants' => $this->participantRepository->getAllParticipantsWithStatus( - [ParticipantRole::TroopParticipant], - [UserStatus::Approved], - $event, - $user, - ), ]); } @@ -331,8 +325,6 @@ public function confirmPayment(int $paymentId, User $user, Request $request, Res $this->logger->info('Payment ID ' . $paymentId . ' cannot be confirmed from admin with event id ' . $user->event->id); } else { - $participant->registrationCloseDate = DateTimeUtils::getDateTime(); - $this->participantRepository->persist($participant); $this->paymentService->confirmPayment($payment); $this->flashMessages->success($this->translator->trans('flash.success.confirmPayment')); $this->logger->info('Payment ID ' . $paymentId . ' manually confirmed as paid'); diff --git a/src/Participant/Participant.php b/src/Participant/Participant.php index 7fd3e170..681ac708 100755 --- a/src/Participant/Participant.php +++ b/src/Participant/Participant.php @@ -18,8 +18,8 @@ * * @property int $id * @property User|null $user m:hasOne - * @property ParticipantRole|null $role m:passThru(roleFromString|roleToString) needed for DB working (see Mapper.php) - * @property string|null $patrolName + * @property ParticipantRole|null $role m:passThru(roleFromString|roleToString) #needed for DB working (see Mapper.php) + * @property string|null $patrolName #used for troops too # TODO move to PatrolLeader + TroopLeader * @property string|null $contingent * @property string|null $firstName * @property string|null $lastName @@ -71,7 +71,7 @@ class Participant extends EntityDatetime protected function initDefaults(): void { parent::initDefaults(); - $this->tieCode = $this->generateTieCode(6); // TODO check if another code exists + $this->tieCode = $this->generateTieCode(6); // TODO check if another code exists in DB } public function setUser(User $user): void diff --git a/src/Participant/ParticipantController.php b/src/Participant/ParticipantController.php index 192fd981..2a6e418c 100755 --- a/src/Participant/ParticipantController.php +++ b/src/Participant/ParticipantController.php @@ -9,6 +9,8 @@ use kissj\Participant\Patrol\PatrolLeader; use kissj\Participant\Patrol\PatrolParticipant; use kissj\Participant\Patrol\PatrolParticipantRepository; +use kissj\Participant\Troop\TroopLeader; +use kissj\Participant\Troop\TroopParticipantRepository; use kissj\User\User; use kissj\User\UserStatus; use Psr\Http\Message\ResponseInterface as Response; @@ -20,6 +22,7 @@ public function __construct( private readonly ParticipantService $participantService, private readonly ParticipantRepository $participantRepository, private readonly PatrolParticipantRepository $patrolParticipantRepository, + private readonly TroopParticipantRepository $troopParticipantRepository, ) { } @@ -96,6 +99,8 @@ private function getTemplateData(Participant $participant): array $participants = []; if ($participant instanceof PatrolLeader) { $participants = $this->patrolParticipantRepository->findAllPatrolParticipantsForPatrolLeader($participant); + } elseif ($participant instanceof TroopLeader) { + $participants = $this->troopParticipantRepository->findAllTroopParticipantsForTroopLeader($participant); } return [ diff --git a/src/Participant/ParticipantService.php b/src/Participant/ParticipantService.php index 1f357bc1..7494d254 100755 --- a/src/Participant/ParticipantService.php +++ b/src/Participant/ParticipantService.php @@ -6,10 +6,12 @@ use kissj\Application\DateTimeUtils; use kissj\Event\AbstractContentArbiter; +use kissj\Event\Event; use kissj\FileHandler\FileHandler; use kissj\FlashMessages\FlashMessagesBySession; use kissj\Mailer\PhpMailerWrapper; use kissj\Participant\Guest\Guest; +use kissj\Participant\Troop\TroopLeader; use kissj\Participant\Troop\TroopParticipant; use kissj\Payment\Payment; use kissj\Payment\PaymentService; @@ -141,6 +143,13 @@ public function isCloseRegistrationValid(Participant $participant): bool $validityFlag = true; $event = $participant->getUserButNotNull()->event; + + // TODO move check for patrol leader here + + if ($participant instanceof TroopLeader) { + $validityFlag = $this->isCloseRegistrationValidForTroopLeader($participant, $event); + } + if (!$this->isParticipantValidForClose($participant, $this->getContentArbiterForParticipant($participant))) { $this->flashMessages->warning($this->translator->trans('flash.warning.noLock')); @@ -177,6 +186,64 @@ public function isCloseRegistrationValid(Participant $participant): bool return $validityFlag; } + private function isCloseRegistrationValidForTroopLeader(TroopLeader $troopLeader, Event $event): bool + { + $validityFlag = true; + $troopParticipants = $troopLeader->troopParticipants; + + $participantsCount = count($troopParticipants); + if ($participantsCount < $event->minimalTroopParticipantsCount) { + $this->flashMessages->warning( + $this->translator->trans( + 'flash.warning.plTooFewParticipantsTroop', + ['%minimalTroopParticipantsCount%' => $event->minimalTroopParticipantsCount], + ) + ); + + $validityFlag = false; + } + if ($participantsCount > $event->maximalTroopParticipantsCount) { + $this->flashMessages->warning( + $this->translator->trans( + 'flash.warning.plTooManyParticipantsTroop', + ['%maximalTroopParticipantsCount%' => $event->maximalTroopParticipantsCount], + ) + ); + + $validityFlag = false; + } + + foreach ($troopParticipants as $participant) { + if (!$this->isParticipantValidForClose( + $participant, + $event->getEventType()->getContentArbiterTroopParticipant(), + )) { + $this->flashMessages->warning( + $this->translator->trans( + 'flash.warning.tlWrongDataParticipant', + ['%participantFullName%' => $participant->getFullName()], + ) + ); + + $validityFlag = false; + } + + if ($participant->user->status !== UserStatus::Closed) { + + $this->flashMessages->warning( + $this->translator->trans( + 'flash.warning.tpNotClosed', + ['%participantFullName%' => $participant->getFullName()], + ) + ); + + $validityFlag = false; + } + } + + return $validityFlag; + } + public function isParticipantValidForClose(Participant $p, AbstractContentArbiter $ca): bool { if ( diff --git a/src/Participant/Patrol/PatrolParticipantRepository.php b/src/Participant/Patrol/PatrolParticipantRepository.php index 62a8e84d..ab9de2c0 100755 --- a/src/Participant/Patrol/PatrolParticipantRepository.php +++ b/src/Participant/Patrol/PatrolParticipantRepository.php @@ -16,7 +16,6 @@ class PatrolParticipantRepository extends Repository { /** - * @param PatrolLeader $patrolLeader * @return PatrolParticipant[] */ public function findAllPatrolParticipantsForPatrolLeader(PatrolLeader $patrolLeader): array diff --git a/src/Participant/Patrol/PatrolService.php b/src/Participant/Patrol/PatrolService.php index 8d43492a..323c27da 100755 --- a/src/Participant/Patrol/PatrolService.php +++ b/src/Participant/Patrol/PatrolService.php @@ -56,6 +56,7 @@ public function addPatrolParticipant(PatrolLeader $patrolLeader): PatrolParticip return $patrolParticipant; } + // TODO refactor to repository->get() public function getPatrolParticipant(int $patrolParticipantId): PatrolParticipant { return $this->patrolParticipantRepository->getOneBy(['id' => $patrolParticipantId]); diff --git a/src/Participant/Troop/TroopController.php b/src/Participant/Troop/TroopController.php new file mode 100755 index 00000000..8ad0d935 --- /dev/null +++ b/src/Participant/Troop/TroopController.php @@ -0,0 +1,118 @@ +getParameterFromBody($request, 'tieCode'); + + $troopLeader = $this->troopLeaderRepository->getFromUser($user); + if ($tieCode === $troopLeader->tieCode) { + $this->flashMessages->warning($this->translator->trans('flash.warning.cannotTieYourself')); + + return $this->redirect($request, $response, 'getDashboard'); + } + + $troopParticipant = $this->troopParticipantRepository->findTroopParticipantFromTieCode($tieCode, $event); + if ($troopParticipant === null) { + $this->flashMessages->warning($this->translator->trans('flash.warning.wrongTieCodeForTroopParticipant')); + + return $this->redirect($request, $response, 'getDashboard'); + } + $this->troopService->tieTroopParticipantToTroopLeader( + $troopParticipant, + $troopLeader, + ); + + return $this->redirect( + $request, + $response, + 'getDashboard', + ); + } + + public function tieParticipantToTroopByParticipant( + Request $request, + Response $response, + User $user, + Event $event, + ): Response { + $troopParticipant = $this->troopParticipantRepository->getFromUser($user); + if ($troopParticipant->troopLeader !== null) { + $this->flashMessages->warning($this->translator->trans('flash.warning.alreadyTied')); + + return $this->redirect($request, $response, 'getDashboard'); + } + + $tieCode = $this->getParameterFromBody($request, 'tieCode'); + $troopLeader = $this->troopLeaderRepository->findTroopLeaderFromTieCode($tieCode, $event); + + if ($troopLeader === null) { + $this->flashMessages->warning($this->translator->trans('flash.warning.wrongTieCodeForTroopLeader')); + + return $this->redirect($request, $response, 'getDashboard'); + } + $this->troopService->tieTroopParticipantToTroopLeader( + $troopParticipant, + $troopLeader, + ); + + return $this->redirect( + $request, + $response, + 'getDashboard', + ); + } + + public function showParticipant(int $participantId, Response $response, User $user): Response + { + return $this->view->render( + $response, + 'show-p.twig', + [ + 'pDetail' => $this->troopParticipantRepository->get($participantId), + 'ca' => $user->event->eventType->getContentArbiterTroopParticipant(), + ] + ); + } + + public function showUntieParticipant(int $participantId, Response $response): Response + { + $patrolParticipant = $this->troopParticipantRepository->get($participantId); + + return $this->view->render($response, 'delete-tp.twig', ['pDetail' => $patrolParticipant]); + } + + public function untieParticipant(int $participantId, Request $request, Response $response): Response + { + $this->troopService->untieTroopParticipant($participantId); + $this->flashMessages->info($this->translator->trans('flash.info.participantUntied')); + + return $this->redirect( + $request, + $response, + 'dashboard', + ); + } +} diff --git a/src/Participant/Troop/TroopLeaderRepository.php b/src/Participant/Troop/TroopLeaderRepository.php index b0b6b44e..b0f0b90c 100755 --- a/src/Participant/Troop/TroopLeaderRepository.php +++ b/src/Participant/Troop/TroopLeaderRepository.php @@ -6,6 +6,8 @@ use kissj\Event\Event; use kissj\Orm\Repository; +use kissj\Participant\ParticipantRole; +use kissj\User\User; /** * @table participant @@ -32,4 +34,22 @@ public function findAllWithEvent(Event $event): array return $troopLeaders; } + + public function findTroopLeaderFromTieCode(string $tieCode, Event $event): ?TroopLeader + { + $troopLeader = $this->findOneBy([ + 'tie_code' => strtoupper($tieCode), + 'role' => ParticipantRole::TroopLeader, + ]); + if ($troopLeader?->user->event->id !== $event->id) { + return null; + } + + return $troopLeader; + } + + public function getFromUser(User $user): TroopLeader + { + return $this->getOneBy(['user' => $user]); + } } diff --git a/src/Participant/Troop/TroopParticipantRepository.php b/src/Participant/Troop/TroopParticipantRepository.php index e84d3113..ec462f52 100755 --- a/src/Participant/Troop/TroopParticipantRepository.php +++ b/src/Participant/Troop/TroopParticipantRepository.php @@ -6,6 +6,8 @@ use kissj\Event\Event; use kissj\Orm\Repository; +use kissj\Participant\ParticipantRole; +use kissj\User\User; /** * @table participant @@ -32,4 +34,28 @@ public function findAllWithEvent(Event $event): array return $troopParticipants; } + + public function findTroopParticipantFromTieCode(string $tieCode, Event $event): ?TroopParticipant + { + $troopParticipant = $this->findOneBy([ + 'tie_code' => strtoupper($tieCode), + 'role' => ParticipantRole::TroopParticipant, + ]); + if ($troopParticipant?->user->event->id !== $event->id) { + return null; + } + + return $troopParticipant; + } + + public function findAllTroopParticipantsForTroopLeader(TroopLeader $troopLeader): array + { + return $this->findBy(['patrol_leader_id' => $troopLeader->id]); + } + + // TODO check why? + public function getFromUser(User $user): TroopParticipant + { + return $this->getOneBy(['user' => $user]); + } } diff --git a/src/Participant/Troop/TroopService.php b/src/Participant/Troop/TroopService.php index 6a768ae1..4a1dfe8b 100755 --- a/src/Participant/Troop/TroopService.php +++ b/src/Participant/Troop/TroopService.php @@ -5,16 +5,21 @@ namespace kissj\Participant\Troop; use kissj\Event\Event; +use kissj\FlashMessages\FlashMessagesInterface; use kissj\Participant\Admin\StatisticValueObject; use kissj\Participant\ParticipantRepository; use kissj\Participant\ParticipantRole; use kissj\User\User; use kissj\User\UserStatus; +use Symfony\Contracts\Translation\TranslatorInterface; class TroopService { public function __construct( + private readonly TroopParticipantRepository $troopParticipantRepository, private readonly ParticipantRepository $participantRepository, + private readonly FlashMessagesInterface $flashMessages, + private readonly TranslatorInterface $translator, ) { } @@ -41,4 +46,45 @@ public function getAllTroopParticipantStatistics(Event $event, User $admin): Sta return new StatisticValueObject($troopLeaders); } + + public function tieTroopParticipantToTroopLeader( + TroopParticipant $troopParticipant, + TroopLeader $troopLeader, + ): TroopParticipant { + if ( + $troopLeader->getUserButNotNull()->status !== UserStatus::Open + ) { + $this->flashMessages->warning($this->translator->trans('flash.warning.troopLeaderNotOpen')); + + return $troopParticipant; + } + + if ($troopParticipant->troopLeader?->id === $troopLeader->id) { + $this->flashMessages->warning($this->translator->trans('flash.warning.troopParticipantAlreadyTied')); + + return $troopParticipant; + } + + $troopParticipant->troopLeader = $troopLeader; + $this->troopParticipantRepository->persist($troopParticipant); + $this->flashMessages->success($this->translator->trans('flash.success.troopParticipantTiedToTroopLeader')); + + return $troopParticipant; + } + + public function troopParticipantBelongsTroopLeader( + TroopParticipant $troopParticipant, + TroopLeader $troopLeader + ): bool { + return $troopParticipant->troopLeader->id === $troopLeader->id; + } + + public function untieTroopParticipant(int $participantId): TroopParticipant + { + $troopParticipant = $this->troopParticipantRepository->get($participantId); + $troopParticipant->troopLeader = null; + $this->troopParticipantRepository->persist($troopParticipant); + + return $troopParticipant; + } } diff --git a/src/Payment/PaymentService.php b/src/Payment/PaymentService.php index 08dc6664..43476429 100755 --- a/src/Payment/PaymentService.php +++ b/src/Payment/PaymentService.php @@ -13,7 +13,9 @@ use kissj\FlashMessages\FlashMessagesBySession; use kissj\Mailer\PhpMailerWrapper; use kissj\Participant\Participant; +use kissj\Participant\ParticipantRepository; use kissj\Participant\Patrol\PatrolLeader; +use kissj\Participant\Troop\TroopLeader; use kissj\User\UserService; use Monolog\Logger; use Symfony\Contracts\Translation\TranslatorInterface; @@ -24,6 +26,7 @@ public function __construct( private readonly FioBankPaymentService $bankPaymentService, private readonly BankPaymentRepository $bankPaymentRepository, private readonly PaymentRepository $paymentRepository, + private readonly ParticipantRepository $participantRepository, private readonly UserService $userService, private readonly FlashMessagesBySession $flashMessages, private readonly PhpMailerWrapper $mailer, @@ -101,10 +104,26 @@ public function confirmPayment(Payment $payment): Payment . PaymentStatus::Waiting->value); } - $this->userService->setUserPaid($payment->participant->getUserButNotNull()); $payment->status = PaymentStatus::Paid; $this->paymentRepository->persist($payment); - $this->mailer->sendRegistrationPaid($payment->participant); + + $participant = $payment->participant; + $this->userService->setUserPaid($participant->getUserButNotNull()); + + $now = DateTimeUtils::getDateTime(); + $participant->registrationCloseDate = $now; + $this->participantRepository->persist($participant); + + if ($participant instanceof TroopLeader) { + foreach ($participant->troopParticipants as $tp) { + $this->userService->setUserPaid($tp->getUserButNotNull()); + + $tp->registrationCloseDate = $now; + $this->participantRepository->persist($tp); + } + } + + $this->mailer->sendRegistrationPaid($participant); return $payment; } diff --git a/src/Settings/TwigExtension.php b/src/Settings/TwigExtension.php index ec17820b..39c4bd9a 100644 --- a/src/Settings/TwigExtension.php +++ b/src/Settings/TwigExtension.php @@ -7,6 +7,7 @@ use kissj\Participant\Patrol\PatrolLeader; use kissj\Participant\Troop\TroopLeader; use kissj\Participant\Troop\TroopParticipant; +use kissj\User\UserStatus; use Twig\Extension\AbstractExtension; use Twig\TwigTest; @@ -24,12 +25,22 @@ public function getTests(): array new TwigTest('TroopLeader', function ($participant): bool { return $participant instanceof TroopLeader; }), + new TwigTest('TroopParticipant', function ($participant): bool { + return $participant instanceof TroopParticipant; + }), new TwigTest('Leader', function ($participant): bool { return $participant instanceof PatrolLeader || $participant instanceof TroopLeader; }), new TwigTest('Troop', function ($participant): bool { return $participant instanceof TroopLeader || $participant instanceof TroopParticipant; }), + new TwigTest('eligibleForShowTieCode', function ($participant): bool { + return ( + $participant instanceof TroopLeader && $participant->user->status === UserStatus::Open + ) || ( + $participant instanceof TroopParticipant && $participant->troopLeader === null + ); + }), ]; } } diff --git a/src/Templates/cs.yaml b/src/Templates/cs.yaml index 488bf51e..0a75b36c 100755 --- a/src/Templates/cs.yaml +++ b/src/Templates/cs.yaml @@ -10,6 +10,8 @@ closeRegistration: userCustomHelp: statusOpen: "Vyplň všechny potřebné informace informace a poté uzamkni přihlášku tlačítkem dole." statusOpenPl: "Napiš o sobě všechny uvedené informace, přidej členy své patroly a informace o nich, poté uzamkni vaši přihlášku tlačítkem dole." + statusOpenTl: "Chceš připnout účastníka do skupiny? Jsou dvě možnosti - buď pošli svůj kód pro připnutí účastníkovi a on ho vloží do své registrace, nebo on musí poslat svůj kód pro připnutí tobě a ty ho vložíš sem" + statusOpenTp: "Chceš se připnout do skupiny? Jsou dvě možnosti - buď pošli svůj kód pro připnutí vedoucímu skupiny a on ho vloží do své registrace, nebo on musí poslat svůj kód pro připnutí tobě a ty ho vložíš sem" statusClosed: "Tvá přihláška čeká na schválení. Dáme Ti vědět, až bude připravena, a následně Ti pošleme informace k platbě. Pokud schvalování trvá moc dlouho, napiš nám na e-mail " statusClosedGuest: "Tvá přihláška čeká na schválení. Dáme Ti vědět až bude připravena. Pokud schvalování trvá moc dlouho (v řádu týdnů), napiš nám na e-mail " statusApproved: "Tvá přihláška byla schválena. Nyní už musíš jen zaplatit registrační poplatek." @@ -19,13 +21,22 @@ dashboard: personalInfo: "Osobní údaje" editDetails: "Vyplnit svoje údaje" delete: "Smazat" - youWantDelete: "Opravdu chcete smazat účastníka " + youWantDelete: "Opravdu chceš smazat účastníka " withoutName: "(prozatím bez jména)" wontBeAbleUndo: "? Toto smazání nepůjde vrátit zpět!" + untie: "Odepnout" + youWantUntie: "Opravdu chceš odepnout účastníka " + youHaveTroop: "Jsi ve skupině %troopName% s vedoucím skupiny %troopLeaderFullName%" details: "podrobnosti" tieCode: "Kód pro připnutí" listOfParticipants: "Seznam účastníků" - addParticipant: "Přidat účastníka" + addParticipant: "Přidej účastníka" + tieTroopParticipant: "Připni účastníka" + tieToTroopLeader: "Připni se ke skupině" + tieCodeLabel: "Kód pro připnutí účastníka" + codeForTieToTroop: "Kód skupiny pro připnutí" + tieCodeFormat: "šest písmen" + withoutFullName: "bez vyplněného jména" youNeed: "Potřebuješ" exactly: "přesne" minimally: "nejméně" @@ -41,6 +52,10 @@ dashboard: waiting: "čekáme na zaplacení" paid: "zaplacená (:" canceled: "zrušená" + userStatus: + open: "odemčený ⚠" + closed: "uzamčený ✔" + paid: "zaplacený (:" lockRegistration: "Uzamknout registraci" changeDetails: editDetails: "Upravit údaje" @@ -371,6 +386,7 @@ transferPayment-admin: flash: info: participantDeleted: "Účastník byl smazán" + participantUntied: "Účastník byl odepnut" paymentCanceled: "Platba stornována, e-mail s důvodem odeslán" chooseRoleNeeded: "Nejprve si musíš zvolit svoji roli pro tuto akci" denied: "účastník odemknut, e-mail byl poslán" @@ -393,13 +409,18 @@ flash: tpApproved: "Účastník akce schválen a e-mail odeslán (bez platby)" adminPairedPayments: "Spárováno bankovních transakcí: " transfer: "Platba úspěšně přesunuta!" + troopParticipantTiedToTroopLeader: "Účastník úspěšně připojen do skupiny!" warning: noLock: "Registraci nelze uzamknout - nějaké informace jsou špatně vyplněné nebo chybí (pravděpodobně nějaké datum)" plWrongData: "Nemůžeme uzamknout registraci - prosím oprav svoje údaje (nejspíš email nebo nějaké datum)" plWrongDataParticipant: "Nemůžeme uzamknout registraci - prosím oprav údaje účastníka %participantFullName% (nejspíš email nebo nějaké datum)" + tlWrongDataParticipant: "Nemůžeme uzamknout registraci - prosím oprav údaje účastníka %participantFullName% (nejspíš email nebo nějaké datum)" + tpNotClosed: "Nemůžeme uzamknout registraci - účastník %participantFullName% není uzamčený, popožeň ho prosím, aby svou registraci uzamknul" plTooFewParticipants: "Nemůžeme uzamknout registraci - v patrole je příliš málo účastníků. Je jich potřeba nejméně %minimalPatrolParticipantsCount%." plTooManyParticipants: "Nemůžeme uzamknout registraci - v patrole je příliš účastníků. Může jich být nejvíce %maximalPatrolParticipantsCount%." fullRegistration: "Už máme plno a ty jsi pod čarou, takže tě zatím nemůžeme registrovat. Počkej prosím než navýšíme kapacitu, nebo než někdo zruší svojí přihlášku" + plTooFewParticipantsTroop: "Nemůžeme uzamknout registraci - ve skupině je příliš málo účastníků. Je jich potřeba jich připnout nejméně %minimalTroopParticipantsCount%." + plTooManyParticipantsTroop: "Nemůžeme uzamknout registraci - ve skupině je příliš účastníků. Může jich být nejvíce %maximalTroopParticipantsCount%, nějaké odepni prosím." notLogged: "Omlouváme se, ale nejsi přihlášen/a. Přihlaš se prosím vyplněním svého e-mailu" loggedIn: "Omlouváme se, ale jsi stále přihlášený/á - nejprve se odhlaš kliknutím na \"Odhlásit se\"" roleChoosed: "Omlouváme se, ale už sis zvolil/a roli pro tuto akci" @@ -417,11 +438,22 @@ flash: nonexistentEvent: "Tato akce nejspíš neexistuje. Asi špatný odkaz?" testingSite: "Tato stránka je určena pouze pro testovací účely - nezadávej žádné reálné osobní či citlivé údaje!" multiplePaymentsNotAllowed: "Pardon, ale generování více plateb pro účastníka není pro tuto akci povoleno." + cannotTieYourself: "Připnout se ke své vlastní skupině nedává smysl" + wrongTieCodeForTroopParticipant: "S tímto kódem pro připnutí neevidujeme žádného účastníka. Zkontroluj prosím kód pro připnutí, jestli je správně" + wrongTieCodeForTroopLeader: "S tímto kódem pro připnutí neevidujeme žádnou skupinu. Zkontroluj prosím kód pro připnutí, jestli je správně" + troopLeaderNotOpen: "Vedoucí skupiny není odemčený, proto nelze připojit účastníka ke skupině" + troopParticipantAlreadyTied: "Účastník již byl do skupiny přidán" + alreadyTied: "Nemůžeš se přidat do skupiny, protože již ve skupině jsi. Jestli chceš k jiné skupině, popros vedoucího o odepnutí" error: adminOnly: "Sorry bro, pouze pro administrátory" plOnly: "Pardon, nejsi přihlášený jako vedoucí patroly" + tlOnly: "Pardon, nejsi přihlášený jako vedoucí skupiny" + tpOnly: "Pardon, nejsi přihlášený jako účastník skupiny" wrongPatrol: "Pardon, ale nemůžeš upravovat či si zobrazit informace účastníků mimo tvou patrolu." + wrongTroop: "Pardon, ale nemůžeš upravovat či si zobrazit informace účastníků mimo tvou skupinu." + notLeader: "Pardon, ale na tuto adresu mohou jen vedoucí patrol či vedoucí skupin" wrongData: "Přihláška nelze uložit, informace nejsou platné" mailError: "Pardon, odeslání e-mailu selhalo. Zkuste to prosím znovu za pár minut." confirmNotAllowed: "Tato platba nepřísluší tvé akce, potvrzení nelze provést" fioConnectionFailed: "Spojení do Fio banky selhalo, nejspíše kvůli špatně zadanému API klíči" + tieParticipantToLeaderError: "Připojení účastníka do skupiny se nepovedlo :(" diff --git a/src/Templates/translatable/admin/open-admin.twig b/src/Templates/translatable/admin/open-admin.twig index f724add0..e982ab0f 100644 --- a/src/Templates/translatable/admin/open-admin.twig +++ b/src/Templates/translatable/admin/open-admin.twig @@ -88,7 +88,6 @@
{% for tp in openTroopParticipants %}
-

{{ tp.getFullName }}

{% include 'widgets/detailsMinimal.twig' with {'person': tp, 'ca': caTp} %}
diff --git a/src/Templates/translatable/admin/payments-admin.twig b/src/Templates/translatable/admin/payments-admin.twig index 49fe4bf9..04aa334b 100644 --- a/src/Templates/translatable/admin/payments-admin.twig +++ b/src/Templates/translatable/admin/payments-admin.twig @@ -80,26 +80,5 @@ {% endfor %} {% endif %}
- - {% if approvedTroopParticipants is empty %} -
-

{% trans %}payments-admin.allTroopParticipantsPaid{% endtrans %}

-
- {% else %} -
-

{% trans %}role.tp{% endtrans %}

-
-
- {% for tp in approvedTroopParticipants %} -
- {% include 'widgets/paymentDetails.twig' with { - 'person': tp, - 'ca': event.getEventType.getContentArbiterTroopParticipant, - } %} -
-
- {% endfor %} - {% endif %} -
{% endif %} {% endblock %} diff --git a/src/Templates/translatable/delete-tp.twig b/src/Templates/translatable/delete-tp.twig new file mode 100644 index 00000000..9a11f880 --- /dev/null +++ b/src/Templates/translatable/delete-tp.twig @@ -0,0 +1,18 @@ +{% extends "_layout.twig" %} + +{% block content %} +
+

{% trans %}dashboard.untie{% endtrans %} - {% trans %}role.tp{% endtrans %}

+

+ {% trans %}dashboard.youWantUntie{% endtrans %} + {% if pDetail.getFirstName is not empty %}{{ pDetail.getFullName }}{% else %} + {% trans %}dashboard.withoutName{% endtrans %}{% endif %}? +

+
+ + {% trans %}changeDetails.back{% endtrans %} +
+
+{% endblock %} diff --git a/src/Templates/translatable/participant/dashboard.twig b/src/Templates/translatable/participant/dashboard.twig index 76eaf3b6..4d3ba384 100644 --- a/src/Templates/translatable/participant/dashboard.twig +++ b/src/Templates/translatable/participant/dashboard.twig @@ -1,15 +1,15 @@ {% extends "_layout.twig" %} {% block content %} -
+
{# TODO remove different themes #}

{{ ('role.' ~ person.role.value)|trans }} - {% if (person is Leader) %}{{ person.getPatrolName }}{% endif %} + {% if person is Leader %}{{ person.getPatrolName }}{% endif %} {% trans %}dashboard.details{% endtrans %}

{% include('widgets/userCustomHelp.twig') %} - {% if person is Troop %} + {% if person is eligibleForShowTieCode %}

{% trans %}dashboard.tieCode{% endtrans %} - {{ person.tieCode }}

@@ -17,7 +17,7 @@ {% if person is Leader %}
- + {# Patrol/Troop Leader #}

{% trans %}role.pl{% endtrans %} {{ person.getFullName }}

{% if userStatus == 'open' %} @@ -29,61 +29,114 @@
- + {# Participants #}

{% trans %}dashboard.listOfParticipants{% endtrans %}

- {% if userStatus == 'open' %} -
- -
-
- {% endif %} + {# list of participants in group #} {% set count = 1 %}
    {% for p in participants %}
  1. - {% trans %}role.p{% endtrans %} {{ p.getFullName }} - {% if userStatus == 'open' %} - edit - delete - {% else %} - search + {% if person is PatrolLeader %} + {% trans %}role.p{% endtrans %} {{ p.getFullName }} + {% if userStatus == 'open' %} + edit + delete + {% else %} + search + {% endif %} + {% elseif person is TroopLeader %} + {% set participantUserStatus = p.user.status.value %} + + {% trans %}role.tp{% endtrans %} + {% if p.getFullName != " " %} + {{ p.getFullName }} + {% else %} + {% trans %}dashboard.withoutFullName{% endtrans %} + {% endif %} + - {{ ('dashboard.userStatus.' ~ participantUserStatus)|trans }} + + search + {% if userStatus == 'open' %} + delete + {% endif %} {% endif %}
  2. {% set count = count + 1 %} {% endfor %}
+ + {# help text how many participants is needed #} {% if userStatus == 'open' %}

{% trans %}dashboard.youNeed {% endtrans %} - {% if event.minimalPatrolParticipantsCount == event.maximalPatrolParticipantsCount %} - {% trans %}dashboard.exactly {% endtrans %} {{ event.minimalPatrolParticipantsCount }} - {% else %} - {% trans %}dashboard.minimally {% endtrans %} {{ event.minimalPatrolParticipantsCount }} - {% trans %}dashboard.andMaximally {% endtrans %} {{ event.maximalPatrolParticipantsCount }} + {% if person is PatrolLeader %} + {% if event.minimalPatrolParticipantsCount == event.maximalPatrolParticipantsCount %} + {% trans %}dashboard.exactly {% endtrans %} {{ event.minimalPatrolParticipantsCount }} + {% else %} + {% trans %}dashboard.minimally {% endtrans %} {{ event.minimalPatrolParticipantsCount }} + {% trans %}dashboard.andMaximally {% endtrans %} {{ event.maximalPatrolParticipantsCount }} + {% endif %} + {% elseif person is TroopLeader %} + {% if event.minimalTroopParticipantsCount == event.maximalTroopParticipantsCount %} + {% trans %}dashboard.exactly {% endtrans %} {{ event.minimalTroopParticipantsCount }} + {% else %} + {% trans %}dashboard.minimally {% endtrans %} {{ event.minimalTroopParticipantsCount }} + {% trans %}dashboard.andMaximally {% endtrans %} {{ event.maximalTroopParticipantsCount }} + {% endif %} {% endif %} {% trans %}dashboard.pForValidReg {% endtrans %}

{% endif %} + + {# adding new participants into group #} + {% if userStatus == 'open' %} + {% if person is PatrolLeader %} +
+ +
+ {% elseif person is TroopLeader %} + {% include('widgets/troopTieForm.twig') with { + formUrl: url_for('tie-tp-by-tl', {'eventSlug': event.slug}), + textInputLabel: 'dashboard.tieCodeLabel', + buttonLabel: 'dashboard.tieTroopParticipant' + } %} + {% endif %} +
+ {% endif %}
{% if userStatus == 'open' %} - + {% if person is PatrolLeader %} + {% set linkLeaderLock = url_for('pl-showCloseRegistration', {'eventSlug': event.slug}) %} + {% elseif person is TroopLeader %} + {% set linkLeaderLock = url_for('showCloseRegistration', {'eventSlug': event.slug}) %} + {% endif %} + {% trans %}dashboard.lockRegistration{% endtrans %} {% endif %} - {% else %} - + {% else %}{# participant #}
+ {% if person is TroopParticipant %} + {% if person.getTroopLeader() is null %} + {% include('widgets/troopTieForm.twig') with { + formUrl: url_for('tie-tp-by-tp', {'eventSlug': event.slug}), + textInputLabel: 'dashboard.codeForTieToTroop', + buttonLabel: 'dashboard.tieToTroopLeader' + } %}
+ {% else %} +

{% trans with {'%troopName%': person.troopLeader.patrolName, '%troopLeaderFullName%': person.troopLeader.getFullName()} %}dashboard.youHaveTroop{% endtrans %}

+ {% endif %} + {% endif %}

{% trans %}dashboard.personalInfo{% endtrans %}

{% if userStatus == 'open' %} diff --git a/src/Templates/translatable/widgets/troopTieForm.twig b/src/Templates/translatable/widgets/troopTieForm.twig new file mode 100644 index 00000000..1d232754 --- /dev/null +++ b/src/Templates/translatable/widgets/troopTieForm.twig @@ -0,0 +1,19 @@ +
+ +
+ +
diff --git a/src/Templates/translatable/widgets/userCustomHelp.twig b/src/Templates/translatable/widgets/userCustomHelp.twig index 0fbf050f..c8e40c32 100644 --- a/src/Templates/translatable/widgets/userCustomHelp.twig +++ b/src/Templates/translatable/widgets/userCustomHelp.twig @@ -1,10 +1,12 @@

- {% set userRole = person.getRole %} + {% set userRole = person.getRole.value %} {% if userStatus == 'open' %} {% if userRole == 'pl' %} {% trans %}userCustomHelp.statusOpenPl{% endtrans %} - {% else %} - {% trans %}userCustomHelp.statusOpen{% endtrans %} + {% elseif userRole == 'tl' %} + {% trans %}userCustomHelp.statusOpenTl{% endtrans %} + {% elseif userRole == 'tp' %} + {% trans %}userCustomHelp.statusOpenTp{% endtrans %} {% endif %} {% elseif userStatus == 'closed' %} {% if userRole == 'guest' %}