diff --git a/.github/workflows/php_build_and_qa_test.yml b/.github/workflows/php_build_and_qa_test.yml index 2cecac4..b1ac54b 100644 --- a/.github/workflows/php_build_and_qa_test.yml +++ b/.github/workflows/php_build_and_qa_test.yml @@ -95,5 +95,5 @@ jobs: npm run build-only ## —— Testing ———————————————————————————————————————————————————————————— -# - name: Execute tests (Unit & Integration Tests) via PestPHP +# - name: Execute tests (Unit & Feature Tests) via PestPHP # run: ./vendor/bin/pest diff --git a/README.md b/README.md index 1d9d840..16d5aaa 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Yoshi-Kan Website + Ledenbeheer --- -

+

GitHub license

@@ -12,7 +12,21 @@ Yoshi-Kan Website + Ledenbeheer ## Requirements -... +* A webserver (Apache, NGINX, ...) with PHP 8.2 or higher. +* [Symfony](https://symfony.com/): PHP 8.2 or higher and these PHP extensions (which are installed and enabled by default in most PHP 8 installations): +Ctype, iconv, PCRE, Session, SimpleXML, and Tokenizer. +* [Composer](https://getcomposer.org/download/), which is used to install PHP packages. +* A database server (MySQL, PostgreSQL, SQLite, ... ) that is compatible with [Doctrine](https://www.doctrine-project.org/). +* Node.js,Yarn & NPM for the frontends build process. + +### Third party services + +* https://www.resend.com/ for sending emails concerning the two factor authentication +* https://www.brevo.com/ for sending the transactional emails +* https://www.mollie.com/ for the online payment services +* https://www.sentry.io/ for error logging and monitoring + +You can configure these services in the `.env` file. ## Getting Started diff --git a/application/YoshiKan/Application/Command/Common/SubscriptionItemsFactory.php b/application/YoshiKan/Application/Command/Common/SubscriptionItemsFactory.php index c274208..aa8057f 100644 --- a/application/YoshiKan/Application/Command/Common/SubscriptionItemsFactory.php +++ b/application/YoshiKan/Application/Command/Common/SubscriptionItemsFactory.php @@ -29,6 +29,19 @@ public function __construct( ) { } + public function resetItemsFromSubscription(Subscription $subscription): bool + { + $subscription = $this->subscriptionRepository->getById($subscription->getId()); + $items = $this->subscriptionItemRepository->getBySubscription($subscription); + foreach ($items as $item) { + $this->subscriptionItemRepository->delete($item); + } + $subscription->clearItems(); + $this->subscriptionRepository->save($subscription); + + return true; + } + public function saveItemsFromSubscription( Subscription $subscription, Federation $federation, diff --git a/application/YoshiKan/Application/Command/Member/ChangeLicense/ChangeLicense.php b/application/YoshiKan/Application/Command/Member/ChangeLicense/ChangeLicense.php new file mode 100644 index 0000000..288bb4d --- /dev/null +++ b/application/YoshiKan/Application/Command/Member/ChangeLicense/ChangeLicense.php @@ -0,0 +1,53 @@ +memberId, + $json->federationId, + ); + } + + // ————————————————————————————————————————————————————————————————————————— + // Getters + // ————————————————————————————————————————————————————————————————————————— + + public function getMemberId(): int + { + return $this->memberId; + } + + public function getFederationId(): int + { + return $this->federationId; + } +} diff --git a/application/YoshiKan/Application/Command/Member/ChangeLicense/ChangeLicenseHandler.php b/application/YoshiKan/Application/Command/Member/ChangeLicense/ChangeLicenseHandler.php new file mode 100644 index 0000000..b8c72be --- /dev/null +++ b/application/YoshiKan/Application/Command/Member/ChangeLicense/ChangeLicenseHandler.php @@ -0,0 +1,145 @@ +memberRepository->getById($command->getMemberId()); + $federation = $this->federationRepository->getById($command->getFederationId()); + $settings = $this->settingsRepository->findByCode(SettingsCode::ACTIVE->value); + + $extraTraining = false; + if (3 === $member->getNumberOfTraining()) { + $extraTraining = true; + } + + // -- calculate the new license dates ------------------------------- + + $now = new \DateTimeImmutable(); + $licenseStart = $now->setDate((int) $now->format('Y'), (int) $now->format('m'), 1); + $licenseEnd = $licenseStart->modify('+1 year'); + + // -- create a new subscription ------------------------------------- + + $subscription = Subscription::make( + $this->subscriptionRepository->nextIdentity(), + $member->getContactFirstname(), + $member->getContactLastname(), + $member->getContactEmail(), + $member->getContactPhone(), + $member->getFirstname(), + $member->getLastname(), + $member->getDateOfBirth(), + $member->getGender(), + SubscriptionType::RENEWAL_LICENSE, + $member->getNumberOfTraining(), + $extraTraining, + false, + false, + false, + 'Wijziging vergunning naar '.$federation->getName(), + $member->getLocation(), + json_decode(json_encode(SettingsReadModel::hydrateFromModel($settings)), true), + $federation, + $member->getMemberSubscriptionStart(), + $member->getMemberSubscriptionEnd(), + 0, + false, + false, + true, + $licenseStart, + $licenseEnd, + $federation->getYearlySubscriptionFee(), + true, + false, + ); + + $subscription->setMember($member); + $subscription->changeStatus(SubscriptionStatus::AWAITING_PAYMENT); + $subscription->calculate(); + $this->subscriptionRepository->save($subscription); + $this->entityManager->flush(); + + // -- make some subscription lines ---------------------------------- + + $subscriptionItemFactory = new SubscriptionItemsFactory( + $this->subscriptionRepository, + $this->subscriptionItemRepository + ); + $resultItems = $subscriptionItemFactory->saveItemsFromSubscription( + $subscription, + $federation, + $settings + ); + + // -- flag new dates in the member ---------------------------------- + + $member->setLicenseDates( + $subscription->getLicenseStart(), + $subscription->getLicenseEnd() + ); + $member->syncFromSubscription( + federation: $federation, + numberOfTraining: $subscription->getNumberOfTraining(), + isHalfYearSubscription: $subscription->isMemberSubscriptionIsHalfYear(), + ); + + $this->memberRepository->save($member); + $this->entityManager->flush(); + + // -- compile a result class ---------------------------------------- + + $subscriptionId = $this->subscriptionRepository->getMaxId(); + $result = new \stdClass(); + $result->id = $subscriptionId; + $result->reference = 'YKS-'.$subscriptionId.': '.$member->getFirstname().' '.$member->getLastName(); + + return $result; + } +} diff --git a/application/YoshiKan/Application/Command/Member/ChangeLicense/ChangeLicenseTrait.php b/application/YoshiKan/Application/Command/Member/ChangeLicense/ChangeLicenseTrait.php new file mode 100644 index 0000000..b77b053 --- /dev/null +++ b/application/YoshiKan/Application/Command/Member/ChangeLicense/ChangeLicenseTrait.php @@ -0,0 +1,38 @@ +permission->CheckRole(['ROLE_DEVELOPER', 'ROLE_ADMIN', 'ROLE_CHIEF_EDITOR']); + + $command = ChangeLicense::hydrateFromJson($jsonCommand); + $handler = new ChangeLicenseHandler( + memberRepository: $this->memberRepository, + subscriptionRepository: $this->subscriptionRepository, + subscriptionItemRepository: $this->subscriptionItemRepository, + federationRepository: $this->federationRepository, + locationRepository: $this->locationRepository, + settingsRepository: $this->settingsRepository, + entityManager: $this->entityManager, + ); + + $result = $handler->change($command); + $this->entityManager->flush(); + + return $result; + } +} diff --git a/application/YoshiKan/Application/Command/Member/ChangeSubscriptionDetails/ChangeSubscriptionDetails.php b/application/YoshiKan/Application/Command/Member/ChangeSubscriptionDetails/ChangeSubscriptionDetails.php new file mode 100644 index 0000000..41a14fa --- /dev/null +++ b/application/YoshiKan/Application/Command/Member/ChangeSubscriptionDetails/ChangeSubscriptionDetails.php @@ -0,0 +1,338 @@ +subscriptionId), + intval($json->memberId), + trim($json->type), + intval($json->federationId), + intval($json->locationId), + trim($json->contactFirstname), + trim($json->contactLastname), + trim($json->contactEmail), + trim($json->contactPhone), + trim($json->addressStreet), + trim($json->addressNumber), + trim($json->addressBox), + trim($json->addressZip), + trim($json->addressCity), + trim($json->firstname), + trim($json->lastname), + trim($json->email), + '', + new \DateTimeImmutable($json->dateOfBirth), + trim($json->gender), + new \DateTimeImmutable($json->memberSubscriptionStart), + trim($json->memberSubscriptionStartMM), + trim($json->memberSubscriptionStartYY), + new \DateTimeImmutable($json->memberSubscriptionEnd), + floatval($json->memberSubscriptionTotal), + boolval($json->memberSubscriptionIsPartSubscription), + boolval($json->memberSubscriptionIsHalfYear), + boolval($json->memberSubscriptionIsPayed), + new \DateTimeImmutable($json->licenseStart), + trim($json->licenseStartMM), + trim($json->licenseStartYY), + new \DateTimeImmutable($json->licenseEnd), + floatval($json->licenseTotal), + boolval($json->licenseIsPartSubscription), + boolval($json->licenseIsPayed), + intval($json->numberOfTraining), + boolval($json->isExtraTraining), + boolval($json->isReductionFamily), + floatval($json->total), + trim($json->remarks), + boolval($json->isJudogiBelt), + ); + } + + // ————————————————————————————————————————————————————————————————————————— + // Getters + // ————————————————————————————————————————————————————————————————————————— + + public function getType(): string + { + return $this->type; + } + + public function getFederationId(): int + { + return $this->federationId; + } + + public function getLocationId(): int + { + return $this->locationId; + } + + public function getContactFirstname(): string + { + return $this->contactFirstname; + } + + public function getContactLastname(): string + { + return $this->contactLastname; + } + + public function getContactEmail(): string + { + return $this->contactEmail; + } + + public function getContactPhone(): string + { + return $this->contactPhone; + } + + public function getAddressStreet(): string + { + return $this->addressStreet; + } + + public function getAddressNumber(): string + { + return $this->addressNumber; + } + + public function getAddressBox(): string + { + return $this->addressBox; + } + + public function getAddressZip(): string + { + return $this->addressZip; + } + + public function getAddressCity(): string + { + return $this->addressCity; + } + + public function getFirstname(): string + { + return $this->firstname; + } + + public function getLastname(): string + { + return $this->lastname; + } + + public function getEmail(): string + { + return $this->email; + } + + public function getNationalRegisterNumber(): string + { + return $this->nationalRegisterNumber; + } + + public function getDateOfBirth(): \DateTimeImmutable + { + return $this->dateOfBirth; + } + + public function getGender(): string + { + return $this->gender; + } + + public function getMemberSubscriptionStart(): \DateTimeImmutable + { + return $this->memberSubscriptionStart; + } + + public function getMemberSubscriptionStartMM(): string + { + return $this->memberSubscriptionStartMM; + } + + public function getMemberSubscriptionStartYY(): string + { + return $this->memberSubscriptionStartYY; + } + + public function getMemberSubscriptionEnd(): \DateTimeImmutable + { + return $this->memberSubscriptionEnd; + } + + public function getMemberSubscriptionTotal(): float + { + return $this->memberSubscriptionTotal; + } + + public function getLicenseStart(): \DateTimeImmutable + { + return $this->licenseStart; + } + + public function getLicenseStartMM(): string + { + return $this->licenseStartMM; + } + + public function getLicenseStartYY(): string + { + return $this->licenseStartYY; + } + + public function getLicenseEnd(): \DateTimeImmutable + { + return $this->licenseEnd; + } + + public function getLicenseTotal(): float + { + return $this->licenseTotal; + } + + public function isLicenseIsPartSubscription(): bool + { + return $this->licenseIsPartSubscription; + } + + public function isLicenseIsPayed(): bool + { + return $this->licenseIsPayed; + } + + public function getNumberOfTraining(): int + { + return $this->numberOfTraining; + } + + public function isExtraTraining(): bool + { + return $this->isExtraTraining; + } + + public function isReductionFamily(): bool + { + return $this->isReductionFamily; + } + + public function getTotal(): float + { + return $this->total; + } + + public function getRemarks(): string + { + return $this->remarks; + } + + public function isJudogiBelt(): bool + { + return $this->isJudogiBelt; + } + + public function isMemberSubscriptionIsPartSubscription(): bool + { + return $this->memberSubscriptionIsPartSubscription; + } + + public function isMemberSubscriptionIsHalfYear(): bool + { + return $this->memberSubscriptionIsHalfYear; + } + + public function isMemberSubscriptionIsPayed(): bool + { + return $this->memberSubscriptionIsPayed; + } + + public function getSubscriptionId(): int + { + return $this->subscriptionId; + } + + public function getMemberId(): int + { + return $this->memberId; + } +} diff --git a/application/YoshiKan/Application/Command/Member/ChangeSubscriptionDetails/ChangeSubscriptionDetailsHandler.php b/application/YoshiKan/Application/Command/Member/ChangeSubscriptionDetails/ChangeSubscriptionDetailsHandler.php new file mode 100644 index 0000000..23152ef --- /dev/null +++ b/application/YoshiKan/Application/Command/Member/ChangeSubscriptionDetails/ChangeSubscriptionDetailsHandler.php @@ -0,0 +1,199 @@ +federationRepository->getById($command->getFederationId()); + $location = $this->locationRepository->getById($command->getLocationId()); + $settings = $this->settingsRepository->findByCode(SettingsCode::ACTIVE->value); + $subscription = $this->subscriptionRepository->getById($command->getSubscriptionId()); + $oldTotal = $subscription->getTotal(); + + // -- validate the command ---------------------------------------- + $commandIsValid = $this->isCommandValid($command, $settings); + if (!$commandIsValid) { + throw new \Exception('Membership change command is not valid.'); + } + + if (SubscriptionStatus::PAYED === $subscription->getStatus()) { + throw new \Exception('Membership change command is not possible .'); + } + if (SubscriptionStatus::COMPLETE === $subscription->getStatus()) { + throw new \Exception('Membership change command is not possible .'); + } + if (SubscriptionStatus::ARCHIVED === $subscription->getStatus()) { + throw new \Exception('Membership change command is not possible .'); + } + + $subscription->fullChange( + contactFirstname: $command->getContactFirstname(), + contactLastname: $command->getContactLastname(), + contactEmail: $command->getContactEmail(), + contactPhone: $command->getContactPhone(), + firstname: $command->getFirstname(), + lastname: $command->getLastname(), + dateOfBirth: $command->getDateOfBirth(), + gender: Gender::from($command->getGender()), + type: SubscriptionType::from($command->getType()), + numberOfTraining: $command->getNumberOfTraining(), + isExtraTraining: $command->isExtraTraining(), + isNewMember: $subscription->isNewMember(), + isReductionFamily: $command->isReductionFamily(), + isJudogiBelt: $command->isJudogiBelt(), + remarks: $command->getRemarks(), + location: $location, + federation: $federation, + memberSubscriptionStart: $command->getMemberSubscriptionStart(), + memberSubscriptionEnd: $command->getMemberSubscriptionEnd(), + memberSubscriptionTotal: $command->getMemberSubscriptionTotal(), + memberSubscriptionIsPartSubscription: $subscription->isMemberSubscriptionIsPartSubscription(), + memberSubscriptionIsHalfYear: $command->isMemberSubscriptionIsHalfYear(), + memberSubscriptionIsPayed: $subscription->isMemberSubscriptionIsPayed(), + licenseStart: $command->getLicenseStart(), + licenseEnd: $command->getLicenseEnd(), + licenseTotal: $command->getLicenseTotal(), + licenseIsPartSubscription: $subscription->isLicenseIsPartSubscription(), + licenseIsPayed: $subscription->isLicenseIsPayed(), + ); + $subscription->setNewMemberFields( + nationalRegisterNumber: $command->getNationalRegisterNumber(), + addressStreet: $command->getAddressStreet(), + addressNumber: $command->getAddressNumber(), + addressBox: $command->getAddressBox(), + addressZip: $command->getAddressZip(), + addressCity: $command->getAddressCity(), + ); + + $this->subscriptionRepository->save($subscription); + $this->entityManager->flush(); + + if (0 !== $command->getMemberId()) { + // -- update the member fields -------------------------------- + $member = $this->memberRepository->getById($command->getMemberId()); + + // -- set contact info ---------------------------------------- + $member->setContactInformation( + contactFirstname: $subscription->getContactFirstname(), + contactLastname: $subscription->getContactLastname(), + contactEmail: $subscription->getContactEmail(), + contactPhone: $subscription->getContactPhone() + ); + // -- set subscription dates ---------------------------------- + $member->setSubscriptionDates( + start: $subscription->getMemberSubscriptionStart(), + end: $subscription->getMemberSubscriptionEnd(), + isHalfYearSubscription: $subscription->isMemberSubscriptionIsHalfYear() + ); + // -- set license dates --------------------------------------- + $member->setLicenseDates( + start: $subscription->getLicenseStart(), + end: $subscription->getLicenseEnd() + ); + + // -- save and connect the member ----------------------------- + $this->memberRepository->save($member); + $subscription->setMember($member); + $this->subscriptionRepository->save($subscription); + } + + // -- recalculate subscription and send new mollie link if needed ---- + $subscription->calculate(); + $this->subscriptionRepository->save($subscription); + $this->entityManager->flush(); + + // -- make some new subscription lines --------------------------------- + $subscriptionItemFactory = new SubscriptionItemsFactory( + $this->subscriptionRepository, + $this->subscriptionItemRepository + ); + $resultClearItems = $subscriptionItemFactory->resetItemsFromSubscription($subscription); + $this->entityManager->flush(); + $resultItems = $subscriptionItemFactory->saveItemsFromSubscription( + $subscription, + $federation, + $settings + ); + + $this->subscriptionRepository->save($subscription); + $this->entityManager->flush(); + + // -- make a new mollie link for online payment ---------------- + + if ($oldTotal !== $subscription->getTotal()) { + $mollieCommand = CreateMolliePaymentLink::make($subscription->getId()); + $mollieCommandHandler = new CreateMolliePaymentLinkHandler( + $this->subscriptionRepository, + $this->mollieConfig, + ); + $resultMollie = $mollieCommandHandler->create($mollieCommand, true); + $this->entityManager->flush(); + } + + // -- compile a result class for giving feedback -------------------- + $result = new \stdClass(); + $result->id = $subscription->getId(); + $result->reference = 'YKS-'.$subscription->getId().': '.$command->getFirstName().' '.$command->getLastName(); + + return $result; + } + + private function isCommandValid(ChangeSubscriptionDetails $command, Settings $settings): bool + { + return true; + } +} diff --git a/application/YoshiKan/Application/Command/Member/ChangeSubscriptionDetails/ChangeSubscriptionDetailsTrait.php b/application/YoshiKan/Application/Command/Member/ChangeSubscriptionDetails/ChangeSubscriptionDetailsTrait.php new file mode 100644 index 0000000..7e5021d --- /dev/null +++ b/application/YoshiKan/Application/Command/Member/ChangeSubscriptionDetails/ChangeSubscriptionDetailsTrait.php @@ -0,0 +1,44 @@ +permission->CheckRole(['ROLE_DEVELOPER', 'ROLE_ADMIN', 'ROLE_CHIEF_EDITOR']); + + $handler = new ChangeSubscriptionDetailsHandler( + $this->federationRepository, + $this->locationRepository, + $this->settingsRepository, + $this->memberRepository, + $this->gradeRepository, + $this->subscriptionRepository, + $this->subscriptionItemRepository, + $this->mollieConfig, + $this->entityManager, + ); + + $result = $handler->change($command); + $this->entityManager->flush(); + + return $result; + } +} diff --git a/application/YoshiKan/Application/Command/Member/CreateMolliePaymentLink/CreateMolliePaymentLinkHandler.php b/application/YoshiKan/Application/Command/Member/CreateMolliePaymentLink/CreateMolliePaymentLinkHandler.php index 57a83ef..1bfcc7c 100644 --- a/application/YoshiKan/Application/Command/Member/CreateMolliePaymentLink/CreateMolliePaymentLinkHandler.php +++ b/application/YoshiKan/Application/Command/Member/CreateMolliePaymentLink/CreateMolliePaymentLinkHandler.php @@ -25,12 +25,13 @@ public function __construct( ) { } - public function create(CreateMolliePaymentLink $command): bool + public function create(CreateMolliePaymentLink $command, bool $force = false): bool { $subscription = $this->subscriptionRepository->getById($command->getSubscriptionId()); // -- link already created - if (!(0 === mb_strlen($subscription->getPaymentId()))) { + + if (!$force && !(0 === mb_strlen($subscription->getPaymentId()))) { return false; } diff --git a/application/YoshiKan/Application/Command/Member/NewMemberSubscriptionMail/NewMemberSubscriptionMail.php b/application/YoshiKan/Application/Command/Member/NewMemberSubscriptionMail/NewMemberSubscriptionMail.php index ea27fbe..150417d 100644 --- a/application/YoshiKan/Application/Command/Member/NewMemberSubscriptionMail/NewMemberSubscriptionMail.php +++ b/application/YoshiKan/Application/Command/Member/NewMemberSubscriptionMail/NewMemberSubscriptionMail.php @@ -23,6 +23,7 @@ public function __construct( protected int $subscriptionId, protected string $fromName, protected string $fromEmail, + protected bool $isChange = false, ) { } @@ -44,4 +45,9 @@ public function getFromEmail(): string { return $this->fromEmail; } + + public function isChange(): bool + { + return $this->isChange; + } } diff --git a/application/YoshiKan/Application/Command/Member/NewMemberSubscriptionMail/NewMemberSubscriptionMailHandler.php b/application/YoshiKan/Application/Command/Member/NewMemberSubscriptionMail/NewMemberSubscriptionMailHandler.php index 507f932..24b2672 100644 --- a/application/YoshiKan/Application/Command/Member/NewMemberSubscriptionMail/NewMemberSubscriptionMailHandler.php +++ b/application/YoshiKan/Application/Command/Member/NewMemberSubscriptionMail/NewMemberSubscriptionMailHandler.php @@ -56,16 +56,30 @@ public function go(NewMemberSubscriptionMail $command): bool { $subscription = $this->subscriptionRepository->getById($command->getSubscriptionId()); $items = $this->subscriptionItemRepository->getBySubscription($subscription); - $subject = 'JC Yoshi-Kan: Nieuwe inschrijving voor '.$subscription->getFirstname().' '.$subscription->getLastname(); - $mailTemplate = $this->twig->render( - 'mail/member_newMemberSubscription_mail.html.twig', - [ - 'subject' => $subject, - 'subscription' => $subscription, - 'items' => $items, - 'url' => $_SERVER['REQUEST_SCHEME'].'://'.$_SERVER['HTTP_HOST'], - ] - ); + + if ($command->isChange()) { + $subject = 'JC Yoshi-Kan: Wijziging inschrijving voor '.$subscription->getFirstname().' '.$subscription->getLastname(); + $mailTemplate = $this->twig->render( + 'mail/member_changed_subscription_mail.html.twig', + [ + 'subject' => $subject, + 'subscription' => $subscription, + 'items' => $items, + 'url' => $_SERVER['REQUEST_SCHEME'].'://'.$_SERVER['HTTP_HOST'], + ] + ); + } else { + $subject = 'JC Yoshi-Kan: Nieuwe inschrijving voor '.$subscription->getFirstname().' '.$subscription->getLastname(); + $mailTemplate = $this->twig->render( + 'mail/member_newMemberSubscription_mail.html.twig', + [ + 'subject' => $subject, + 'subscription' => $subscription, + 'items' => $items, + 'url' => $_SERVER['REQUEST_SCHEME'].'://'.$_SERVER['HTTP_HOST'], + ] + ); + } // -- send email ---------------------------------------------------------- diff --git a/application/YoshiKan/Application/Command/Member/NewMemberSubscriptionMail/NewMemberSubscriptionMailTrait.php b/application/YoshiKan/Application/Command/Member/NewMemberSubscriptionMail/NewMemberSubscriptionMailTrait.php index 0c1131b..39f8128 100644 --- a/application/YoshiKan/Application/Command/Member/NewMemberSubscriptionMail/NewMemberSubscriptionMailTrait.php +++ b/application/YoshiKan/Application/Command/Member/NewMemberSubscriptionMail/NewMemberSubscriptionMailTrait.php @@ -17,7 +17,7 @@ trait NewMemberSubscriptionMailTrait { - public function sendMemberNewSubscriptionMail(int $subscriptionId): bool + public function sendMemberNewSubscriptionMail(int $subscriptionId, bool $isChange = false): bool { $this->permission->CheckRole(['ROLE_DEVELOPER', 'ROLE_ADMIN', 'ROLE_CHIEF_EDITOR']); @@ -36,7 +36,8 @@ public function sendMemberNewSubscriptionMail(int $subscriptionId): bool $command = new NewMemberSubscriptionMail( $subscriptionId, Settings::FROM_NAME->value, - Settings::FROM_EMAIL->value + Settings::FROM_EMAIL->value, + $isChange, ); $result = $handler->go($command); diff --git a/application/YoshiKan/Application/MemberCommandBus.php b/application/YoshiKan/Application/MemberCommandBus.php index 745e816..9a812d5 100644 --- a/application/YoshiKan/Application/MemberCommandBus.php +++ b/application/YoshiKan/Application/MemberCommandBus.php @@ -21,11 +21,13 @@ use App\YoshiKan\Application\Command\Member\ChangeFederation\ChangeFederationTrait; use App\YoshiKan\Application\Command\Member\ChangeGrade\ChangeGradeTrait; use App\YoshiKan\Application\Command\Member\ChangeGroup\ChangeGroupTrait; +use App\YoshiKan\Application\Command\Member\ChangeLicense\ChangeLicenseTrait; use App\YoshiKan\Application\Command\Member\ChangeLocation\ChangeLocationTrait; use App\YoshiKan\Application\Command\Member\ChangeMemberDetails\ChangeMemberDetailsTrait; use App\YoshiKan\Application\Command\Member\ChangeMemberGrade\ChangeMemberGradeTrait; use App\YoshiKan\Application\Command\Member\ChangeMemberRemarks\ChangeMemberRemarksTrait; use App\YoshiKan\Application\Command\Member\ChangePeriod\ChangePeriodTrait; +use App\YoshiKan\Application\Command\Member\ChangeSubscriptionDetails\ChangeSubscriptionDetailsTrait; use App\YoshiKan\Application\Command\Member\ConfirmMemberWebSubscription\ConfirmMemberWebSubscriptionTrait; use App\YoshiKan\Application\Command\Member\CreateMolliePaymentLink\CreateMolliePaymentLinkTrait; use App\YoshiKan\Application\Command\Member\DeleteMemberImage\DeleteMemberImageTrait; @@ -113,6 +115,8 @@ class MemberCommandBus use CreateMolliePaymentLinkTrait; use SendPaymentReceivedConfirmationMailTrait; use NewMemberSubscriptionMailTrait; + use ChangeSubscriptionDetailsTrait; + use ChangeLicenseTrait; // -- member images --------------------------------------------------------- use UploadProfileImageTrait; diff --git a/application/YoshiKan/Application/Query/Member/GetMember.php b/application/YoshiKan/Application/Query/Member/GetMember.php index 6e6e469..b4b4db6 100644 --- a/application/YoshiKan/Application/Query/Member/GetMember.php +++ b/application/YoshiKan/Application/Query/Member/GetMember.php @@ -63,7 +63,8 @@ public function search(MemberSearchModel $searchModel): MemberReadModelCollectio $location, $grade, $minYearOfBirth, - $maxYearOfBirth + $maxYearOfBirth, + $searchModel->isActive(), ); // -- covert to readmodel collection ------------------------------------------ diff --git a/application/YoshiKan/Application/Query/Member/GetSubscriptionTrait.php b/application/YoshiKan/Application/Query/Member/GetSubscriptionTrait.php index 6428f83..7f7e496 100644 --- a/application/YoshiKan/Application/Query/Member/GetSubscriptionTrait.php +++ b/application/YoshiKan/Application/Query/Member/GetSubscriptionTrait.php @@ -81,6 +81,7 @@ public function printSubscriptions(array $listIds): bool $this->locationRepository, $this->federationRepository, $this->subscriptionRepository, + $this->settingsRepository, $this->twig, $this->uploadFolder, $this->entityManager, @@ -90,4 +91,23 @@ public function printSubscriptions(array $listIds): bool return true; } + + public function printEmptySubscriptionForm(): bool + { + $this->permission->CheckRole(['ROLE_DEVELOPER', 'ROLE_ADMIN', 'ROLE_CHIEF_EDITOR']); + + $document = new PrintSubscriptions( + $this->locationRepository, + $this->federationRepository, + $this->subscriptionRepository, + $this->settingsRepository, + $this->twig, + $this->uploadFolder, + $this->entityManager, + ); + + $document->printEmptySubscriptionForm(); + + return true; + } } diff --git a/application/YoshiKan/Application/Query/Member/PrintSubscriptions.php b/application/YoshiKan/Application/Query/Member/PrintSubscriptions.php index 9a0d303..4771612 100644 --- a/application/YoshiKan/Application/Query/Member/PrintSubscriptions.php +++ b/application/YoshiKan/Application/Query/Member/PrintSubscriptions.php @@ -15,6 +15,8 @@ use App\YoshiKan\Domain\Model\Member\FederationRepository; use App\YoshiKan\Domain\Model\Member\LocationRepository; +use App\YoshiKan\Domain\Model\Member\SettingsCode; +use App\YoshiKan\Domain\Model\Member\SettingsRepository; use App\YoshiKan\Domain\Model\Member\Subscription; use App\YoshiKan\Domain\Model\Member\SubscriptionRepository; use Doctrine\ORM\EntityManagerInterface; @@ -32,6 +34,7 @@ public function __construct( protected LocationRepository $locationRepository, protected FederationRepository $federationRepository, protected SubscriptionRepository $subscriptionRepository, + protected SettingsRepository $settingsRepository, protected Environment $twig, protected string $uploadFolder, protected EntityManagerInterface $entityManager, @@ -72,4 +75,32 @@ public function printOverview(array $listIds): void exit; } + + public function printEmptySubscriptionForm(): void + { + $settings = $this->settingsRepository->findByCode(SettingsCode::ACTIVE->value); + $federations = $this->federationRepository->getAll(); + $locations = $this->locationRepository->getAll(); + + $data = new \stdClass(); + $data->generatedOn = new \DateTimeImmutable(); + $data->settings = $settings; + $data->federations = $federations; + $data->locations = $locations; + + $pdfHtml = $this->twig->render('pdf/empty_subscription_form.html.twig', ['data' => $data]); + + $options = new Options(); + $options->set('isRemoteEnabled', true); + $options->set('isPhpEnabled', true); + $dompdf = new Dompdf($options); + $dompdf->loadHtml($pdfHtml); + $dompdf->setPaper('A4', 'portrait'); + $dompdf->render(); + + $fileName = $data->generatedOn->format('YmdHis').'_yoshikan_inschrijving_formulier.pdf'; + $dompdf->stream($fileName, ['Attachment' => false]); + + exit; + } } diff --git a/application/YoshiKan/Application/Query/Member/Readmodel/MemberSearchModel.php b/application/YoshiKan/Application/Query/Member/Readmodel/MemberSearchModel.php index c2ae6fe..5570694 100644 --- a/application/YoshiKan/Application/Query/Member/Readmodel/MemberSearchModel.php +++ b/application/YoshiKan/Application/Query/Member/Readmodel/MemberSearchModel.php @@ -25,6 +25,7 @@ private function __construct( protected int $gradeId, protected int $yearOfBirth, protected int $groupId, + protected ?bool $isActive = null, ) { } @@ -39,6 +40,7 @@ public static function hydrateFromJson(\stdClass $json): self $gradeId = 0; $yearOfBirth = 0; $groupId = 0; + $isActive = null; if (isset($json->locationId)) { $locationId = intval($json->locationId); } @@ -51,13 +53,17 @@ public static function hydrateFromJson(\stdClass $json): self if (isset($json->group)) { $groupId = intval($json->group->id); } + if (isset($json->isActive)) { + $isActive = boolval($json->isActive); + } return new self( $keyword, $locationId, $gradeId, $yearOfBirth, - $groupId + $groupId, + $isActive, ); } @@ -89,4 +95,9 @@ public function getGroupId(): int { return $this->groupId; } + + public function isActive(): ?bool + { + return $this->isActive; + } } diff --git a/application/YoshiKan/Domain/Model/Member/Subscription.php b/application/YoshiKan/Domain/Model/Member/Subscription.php index 4a34331..0aee9c7 100644 --- a/application/YoshiKan/Domain/Model/Member/Subscription.php +++ b/application/YoshiKan/Domain/Model/Member/Subscription.php @@ -481,6 +481,11 @@ public function updateSettings(array $settings): void $this->settings = $settings; } + public function clearItems(): void + { + $this->items = new ArrayCollection(); + } + // ————————————————————————————————————————————————————————————————————————— // Payment information setters // ————————————————————————————————————————————————————————————————————————— diff --git a/application/YoshiKan/Infrastructure/Templates/mail/member_changed_subscription_mail.html.twig b/application/YoshiKan/Infrastructure/Templates/mail/member_changed_subscription_mail.html.twig new file mode 100644 index 0000000..bcfb004 --- /dev/null +++ b/application/YoshiKan/Infrastructure/Templates/mail/member_changed_subscription_mail.html.twig @@ -0,0 +1,97 @@ +{% extends 'mail/base_mail.html.twig' %} + +{% block title %}{{ subject }}{% endblock %} + +{% block body %} +

+ Heist op den Berg, {{ 'now' | date('d/m/Y') }} +

+

+ Beste {{ subscription.contactFirstname }} {{ subscription.contactLastname }}, +

+

+ We hebben de inschrijving van {{ subscription.firstname }} {{ subscription.lastname }} aangepast. + Hieronder vind je een overzicht van de wijzigingen. +

+

+ Om de inschrijving definitief te maken en ervoor te zorgen dat je volop kunt genieten + van onze judolessen, verzoeken wij je vriendelijk om een bedrag van {{ subscription.total }} € + over te maken naar onze bankrekening op het volgende nummer: BE37 7330 0101 8328 met vermelding + van volgende referentie + "YKS-{{ subscription.id }} {{ subscription.lastname }} {{ subscription.firstname }}". + Zo kunnen we je betaling snel identificeren. +

+ {% if (subscription.paymentLink is not empty) %} +

+ Of betaal online via deze Mollie-link: + {{ subscription.paymentLink }}. +

+ {% endif %} +

+ Wanneer we je betaling hebben ontvangen, zullen we je officieel + inschrijven en krijg je toegang tot al onze trainingen en evenementen. + We staan te popelen om je te verwelkomen op de mat. +

+

+ Mocht je nog vragen hebben of extra informatie nodig hebben, aarzel dan niet om contact + met ons op te nemen via judo.yoshikan@gmail.com. + Ons team staat altijd klaar om je te helpen. +

+
+

+

+ {{ subscription.lastname | upper }} {{ subscription.firstname }} + (° {{ subscription.dateOfBirth | date('d/m/Y') }} - {{ subscription.gender.value }}) +
+
+ {{ subscription.member.addressStreet }} + {{ subscription.member.addressNumber }} + {% if(subscription.member.addressBox != '') %} + bus {{ subscription.member.addressBox }} + {% endif %} +
+
+ {{ subscription.member.addressZip }} + {{ subscription.member.addressCity }} +
+

+

+ + {% for item in items %} + {% if(item.price == 0) %} + + + + {% else %} + + + + + {% endif %} + {% endfor %} + + + + +
{{ item.name }}
{{ item.name }} {{ item.price }} €
Totaal{{ subscription.total }} € +
+

+

 

+ {% if(subscription.remarks != '') %} +
 
+

+ {{ subscription.remarks }} +

+ {% endif %} +

+ Met sportieve groeten, +
Team Yoshi-Kan. +

+

 

+

+ Door het uitvoeren van de betaling gaat U akkoord met ons reglement + en privacyverklaring, terug te vinden op onze website + www.yoshi-kan.be +

+ +{% endblock %} diff --git a/application/YoshiKan/Infrastructure/Templates/pdf/empty_subscription_form.html.twig b/application/YoshiKan/Infrastructure/Templates/pdf/empty_subscription_form.html.twig new file mode 100644 index 0000000..a3f7bae --- /dev/null +++ b/application/YoshiKan/Infrastructure/Templates/pdf/empty_subscription_form.html.twig @@ -0,0 +1,322 @@ +{# #} +{# This file is part of the Yoshi-Kan software. #} +{# #} +{# (c) Koen Caerels #} +{# #} +{# For the full copyright and license information, please view the LICENSE #} +{# file that was distributed with this source code. #} +{# #} + + + + + + + +{# ------------------------------------------------------------------------------------------------------------------ #} + + + + + + + {# right side of the template -------------------------------------------- #} + + +
+ + + + + +
+ Datum +

.......... / ........... / ..............

+
+ Plaats  +

........................................................

+
+ + + + + + + + + + + + + + + + + + + + + +
Naam +

+ ..............................................................................................

+
Voornaam +

+ ...............................................................................................

+
Geb.datum +

+ ........ / ........ / .............. + +          + O    M       + O    V       + O    X + +

+


Adres
+

Straat: + ...................................................................................

+

Postcode: + .............................................................................

+

Gemeente: + ............................................................................

+

Contact (ouder)
+

Naam: + ..................................................................................

+

Email: + ...................................................................................

+

Telefoon: + .............................................................................

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Locatie +

+ {% for location in data.locations %} + O   + {{ location.name }} +    + {% endfor %} +

+
Vergunning +

+ {% for federation in data.federations %} + O    + {{ federation.name }} +      {{ federation.yearlySubscriptionFee }} € +       + {% endfor %} +

+
Welkomspakket +

+ O    + Judogids, judopaspoort, leskaart en sportzak +    {{ data.settings.newMemberSubscriptionFee }} € +

+
Frequentie +

+        +        +        +        +        +        +       + Halfjaarlijks +        +    + Jaarlijks +

+

+ O    + 1 x per week +        +        + {{ data.settings.halfYearlyFee1Training }} € +              + {{ data.settings.yearlyFee1Training }} € +       +

+

+ O    + 2 x per week +        +        + {{ data.settings.halfYearlyFee2Training }} € +              + {{ data.settings.yearlyFee2Training }} € +       +

+

+ O    + 3-5 x per week +             +             +             +         +         + + {{ data.settings.extraTrainingFee }} € +       +

+
Gezinskorting +

+ O    + + 2e en 3e kind van éénzelfde familie geniet {{ data.settings.familyDiscount }}% korting + op lidgeld +

+
Opmerkingen +

Judopak : + ..................................................................................

+

Gordel : + .....................................................................................

+

+ ...................................................................................................

+

+ ...................................................................................................

+

+ ...................................................................................................

+

+ ...................................................................................................

+

+ ...................................................................................................

+

+ ...................................................................................................

+

+ ...................................................................................................

+

+ ...................................................................................................

+

+ ...................................................................................................

+
+ +
+ + + + +
+
Inschrijven kan ook online via onze website: +
https://www.yoshikan.be/inschrijven +


+
+ +
+
+ +
+ JC Yoshi-Kan v.z.w. +
+
+
Secr: Spekstraat 80 +
2220 Heist o/d Berg +
+
+ G  0474 51 13 98 +
E  judo.yoshikan@gmail.com +
+
+
IBAN: BE37 7330 0101 8328 +
BIC: KRED BE BB +
+
+
+ + Totaal:       + +

....................... €

+ +
+
+
+ Handtekening ouder of voogd: +


  +
+ +
+
+
+
+
+
+
+
+
+
+ +
+
+
+ Door het ondertekenen of het uitvoeren van de betaling, gaat U akkoord met ons + reglement en privacy verklaring, terug te vinden op onze website + www.yoshikan.be +
+
+
+ +
+ + + diff --git a/application/YoshiKan/Infrastructure/Web/Controller/Routes/Member/MemberRoutes.php b/application/YoshiKan/Infrastructure/Web/Controller/Routes/Member/MemberRoutes.php index a9a5d2d..40b4dfb 100644 --- a/application/YoshiKan/Infrastructure/Web/Controller/Routes/Member/MemberRoutes.php +++ b/application/YoshiKan/Infrastructure/Web/Controller/Routes/Member/MemberRoutes.php @@ -145,6 +145,18 @@ public function extendMemberSubscription(int $id, Request $request): JsonRespons return new JsonResponse($response, 200, $this->apiAccess); } + #[Route('/mm/api/member/{id}/change-license', requirements: ['id' => '\d+'], methods: ['POST', 'PUT'])] + public function changeMemberLicense(int $id, Request $request): JsonResponse + { + $command = json_decode($request->request->get('command')); + $response = $this->commandBus->changeLicense($command); + + $result_mollie = $this->commandBus->createMolliePaymentLink($response->id); + $result_mail = $this->commandBus->sendMemberExtendSubscriptionMail($response->id); + + return new JsonResponse($response, 200, $this->apiAccess); + } + /** * @throws \Exception */ @@ -194,6 +206,19 @@ public function confirmWebSubscription(Request $request): JsonResponse return new JsonResponse($response, 200, $this->apiAccess); } + /** + * @throws \Exception + */ + #[Route('/mm/api/member/change-subscription-details', methods: ['POST', 'PUT'])] + public function changeSubscriptionDetails(Request $request): JsonResponse + { + $command = json_decode($request->request->get('command')); + $response = $this->commandBus->changeSubscriptionDetails($command); + $result_mail = $this->commandBus->sendMemberNewSubscriptionMail($response->id, true); + + return new JsonResponse($response, 200, $this->apiAccess); + } + #[Route('/mm/api/member/overview-due-payments', methods: ['GET'])] public function downloadOverviewDuePayments(Request $request): JsonResponse { diff --git a/application/YoshiKan/Infrastructure/Web/Controller/Routes/Member/SubscriptionRoutes.php b/application/YoshiKan/Infrastructure/Web/Controller/Routes/Member/SubscriptionRoutes.php index 4b4b9a4..dff0e74 100644 --- a/application/YoshiKan/Infrastructure/Web/Controller/Routes/Member/SubscriptionRoutes.php +++ b/application/YoshiKan/Infrastructure/Web/Controller/Routes/Member/SubscriptionRoutes.php @@ -102,6 +102,14 @@ public function printSubscriptions(Request $request): void exit; } + #[Route('/mm/api/subscriptions/print/empty', methods: ['GET'])] + public function printEmptySubscriptionsForm(Request $request): void + { + $document = $this->queryBus->printEmptySubscriptionForm(); + + exit; + } + private function convertToArrayOfIds(string $ids): array { $arListIdsInt = []; diff --git a/assets/components/appInschrijving.vue b/assets/components/appInschrijving.vue index dd329a0..129ca1c 100644 --- a/assets/components/appInschrijving.vue +++ b/assets/components/appInschrijving.vue @@ -326,8 +326,13 @@
-
Opmerkingen
+
Opmerkingen:
+
+ Wens je ook een judopak en gordel te bestellen? +
Geef dan de maat van het judopak en de kleur van de gordel door in het veld hieronder. + Dan zorgen wij dat dit klaar ligt bij de volgende training. +
diff --git a/frontends/member_module/.env.development b/frontends/member_module/.env.development index 76a45ea..9506e47 100644 --- a/frontends/member_module/.env.development +++ b/frontends/member_module/.env.development @@ -1,2 +1,2 @@ VITE_API_URL=http://localhost.charlesproxy.com:8080/mm/api -VITE_VERSION=0.9.0 +VITE_VERSION=0.0.1 diff --git a/frontends/member_module/.env.production b/frontends/member_module/.env.production index 3320f62..8800d20 100644 --- a/frontends/member_module/.env.production +++ b/frontends/member_module/.env.production @@ -1,2 +1,2 @@ VITE_API_URL=/mm/api -VITE_VERSION=0.9.0 +VITE_VERSION=0.0.1 diff --git a/frontends/member_module/src/api/command/changeMemberLicense.ts b/frontends/member_module/src/api/command/changeMemberLicense.ts new file mode 100644 index 0000000..ff1e6e4 --- /dev/null +++ b/frontends/member_module/src/api/command/changeMemberLicense.ts @@ -0,0 +1,22 @@ +/* + * This file is part of the Yoshi-Kan software. + * + * (c) Koen Caerels + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import axios from "axios"; + +export interface ChangeMemberLicenseCommand { + memberId: number; + federationId: number; +} + +export async function changeMemberLicense(command: ChangeMemberLicenseCommand) { + const formData = new FormData(); + formData.append('command', JSON.stringify(command)); + const response= await axios.post(`/member/${command.memberId}/change-license`, formData); + return response.data; +} diff --git a/frontends/member_module/src/api/command/subscription/changeSubscription.ts b/frontends/member_module/src/api/command/subscription/changeSubscription.ts new file mode 100644 index 0000000..82acae2 --- /dev/null +++ b/frontends/member_module/src/api/command/subscription/changeSubscription.ts @@ -0,0 +1,75 @@ +/* + * This file is part of the Yoshi-Kan software. + * + * (c) Koen Caerels + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import axios from "axios"; + +export interface changeSubscriptionCommand { + + subscriptionId: number; + memberId: number; + + type: string; + federationId: number; + locationId: number; + + contactFirstname: string; + contactLastname: string; + contactEmail: string; + contactPhone: string; + + addressStreet: string; + addressNumber: string; + addressBox: string; + addressZip: string; + addressCity: string; + + firstname: string; + lastname: string; + email: string; + nationalRegisterNumber: string; + dateOfBirthDD: '', + dateOfBirthMM: '', + dateOfBirthYYYY: '', + dateOfBirth: Date; + gender: string; + + memberSubscriptionStart: Date; + memberSubscriptionStartMM: string; + memberSubscriptionStartYY: string; + memberSubscriptionEnd: Date; + memberSubscriptionTotal: number; + memberSubscriptionIsPartSubscription: boolean; + memberSubscriptionIsHalfYear: boolean; + memberSubscriptionIsPayed: boolean; + + licenseStart: Date; + licenseStartMM: string; + licenseStartYY: string; + licenseEnd: Date; + licenseTotal: number; + licenseIsPartSubscription: boolean; + licenseIsPayed: boolean; + + numberOfTraining: number; + isExtraTraining: boolean; + isNewMember: boolean; + isReductionFamily: boolean; + + total: number; + remarks: string; + + isJudogiBelt: boolean; +} + +export async function changeSubscription(command: changeSubscriptionCommand) { + const formData = new FormData(); + formData.append('command', JSON.stringify(command)); + const response = await axios.post(`/member/change-subscription-details`, formData); + return response.data; +} diff --git a/frontends/member_module/src/api/query/searchMember.ts b/frontends/member_module/src/api/query/searchMember.ts index e033f05..cce816c 100644 --- a/frontends/member_module/src/api/query/searchMember.ts +++ b/frontends/member_module/src/api/query/searchMember.ts @@ -15,6 +15,7 @@ export interface MemberSearchModel { grade?: Grade, yearOfBirth?: string, group?: Group, + isActive?: boolean, } export async function searchMembers(searchModel: MemberSearchModel) { diff --git a/frontends/member_module/src/components/member/memberBadge.vue b/frontends/member_module/src/components/member/memberBadge.vue index a79a6bd..d76b059 100644 --- a/frontends/member_module/src/components/member/memberBadge.vue +++ b/frontends/member_module/src/components/member/memberBadge.vue @@ -5,8 +5,8 @@ :style="'background-color: #'+member.grade.color">
- +
diff --git a/frontends/member_module/src/components/member/memberList.vue b/frontends/member_module/src/components/member/memberList.vue index 5ed3694..4eec60c 100644 --- a/frontends/member_module/src/components/member/memberList.vue +++ b/frontends/member_module/src/components/member/memberList.vue @@ -4,11 +4,22 @@
+ +
+
Status
+
+ +
+
+
Naam of lidnr.
-
@@ -135,7 +146,7 @@
 
-
Lidnr.
+
Lidnr.
Naam
Geboortedatum
Groep / Locatie
@@ -143,7 +154,7 @@
Graad
- +
({ grade: undefined, yearOfBirth: undefined, group: undefined, + isActive: undefined, }); const members = ref([]); const isSearching = ref(false); @@ -262,6 +274,12 @@ watch(counter, (): void => { void searchMemberHandler(); }); +const statusOptions = ref([ + {name: 'niet van toepassing', value: undefined}, + {name: 'Actief', value: true}, + {name: 'Archief', value: false,} +]); + function selectedClass(id: number) { let style = ''; if (id === memberStore.memberId) { @@ -281,7 +299,9 @@ function resetSearch() { keyword: '', locationId: undefined, grade: undefined, - yearOfBirth: undefined + yearOfBirth: undefined, + group: undefined, + isActive: undefined, } searchMemberHandler(); } diff --git a/frontends/member_module/src/components/member/profile/profileDetail.vue b/frontends/member_module/src/components/member/profile/profileDetail.vue index e7ad80d..f0923d5 100644 --- a/frontends/member_module/src/components/member/profile/profileDetail.vue +++ b/frontends/member_module/src/components/member/profile/profileDetail.vue @@ -81,6 +81,12 @@ class="p-button-sm p-button-secondary" icon="pi pi-send"/>
+
+
@@ -146,20 +152,54 @@ + +
+
+
naar
+
+
+ + + + +
+
+
+
+
+
+
+ diff --git a/frontends/member_module/src/components/subscription/changeSubscription/changeSubscriptionForm.vue b/frontends/member_module/src/components/subscription/changeSubscription/changeSubscriptionForm.vue new file mode 100644 index 0000000..eef8c50 --- /dev/null +++ b/frontends/member_module/src/components/subscription/changeSubscription/changeSubscriptionForm.vue @@ -0,0 +1,824 @@ + + +