diff --git a/ws_customerspecialty/.gitignore b/ws_customerspecialty/.gitignore new file mode 100644 index 0000000..48b8bf9 --- /dev/null +++ b/ws_customerspecialty/.gitignore @@ -0,0 +1 @@ +vendor/ diff --git a/ws_customerspecialty/README.md b/ws_customerspecialty/README.md new file mode 100644 index 0000000..61c9f49 --- /dev/null +++ b/ws_customerspecialty/README.md @@ -0,0 +1,75 @@ +# ws_customerspecialty + +PrestaShop module (1.7 - 9.0 compatible) that adds a manageable "Specialty" field to customer +accounts. + +## What it does + +- Back office: a **Customer Specialties** admin screen (under Customers) lets you maintain the + list of selectable specialty values (title per language, position, active/inactive). Values + are never hard-deleted from the UI, only deactivated, since they may already be linked to + customers. +- Back office: the native "Add/Edit customer" page gets a required "Specialty" dropdown, and the + native Customers grid gets a sortable/filterable "Specialty" column and a read-only specialty + block on the customer view page. +- Front office: the registration / "my account" form gets a required "Specialty" dropdown, gated + by a **Configure** toggle (`WS_CUSTOMERSPECIALTY_FRONT_ENABLED`) so it can be hidden from the + storefront without affecting back-office management. +- The customer's specialty title is exposed as a `{specialization}` order confirmation mail + variable (see below). + +## Data model + +The customer's specialty is stored in the module's own tables — `ws_customerspecialty`, +`ws_customerspecialty_lang`, and the pivot table `ws_customerspecialty_customer` — rather than by +altering `ps_customer` or overriding core classes. This keeps the module safe to run unchanged +across PrestaShop 1.7 through 9.0, since it never depends on core method internals that vary +between major versions. + +## Order confirmation email integration + +The module does **not** ship its own copy of the order confirmation mail template. Instead it +exposes the customer's specialty as `{specialization}` via the `actionGetExtraMailTemplateVars` +hook (the same pattern used in `ws_giftdropdown`). To show it in an order confirmation email, add +the placeholder to the shop's own `order_conf.html` / `order_conf.txt` mail templates, e.g.: + +``` +Specialty: {specialization} +``` + +If the placeholder isn't present in the active theme's mail template, the variable is computed +but simply has nothing to render into — this is a one-time manual integration step per shop/theme, +not something the module can safely do on your behalf without overriding core mail templates. + +## Compatibility notes + +- Command handlers are tagged for both `tactician.handler` (PS 1.7/8) and `messenger.message_handler` + (PS 9), with matching `__invoke()`/`handle()` methods on each handler. +- No PrestaShop core class overrides, no `ALTER TABLE` on core tables. + +## Deliberate differences from the legacy `pchcustomerfield` module + +- No drag-and-drop reordering of specialty values. The grid has "Move up" / "Move down" row + actions instead (`customerspecialty_move_up` / `_move_down` routes swapping `position` with the + adjacent row). This avoids depending on PrestaShop's core Grid position/drag-reorder plumbing, + whose exact API wasn't verified live against a running 1.7 and 9.0 instance. +- The active/inactive toggle uses a `ToggleColumn` + `customerspecialty_toggle_active` route. If + that route ever misbehaves on a given PrestaShop version, active/inactive can always be changed + via the Edit form as a fallback — it isn't the only way to flip the flag. +- Grid `getFilters()`/`getColumns()` option keys (`ToggleColumn`, `SearchAndResetType`) follow the + standard, long-stable PrestaShop Grid API from memory; they were not verified against a live + PrestaShop 1.7 or 9.0 install (per request, devdocs research was skipped for this build). Smoke + test the "Customer Specialties" admin grid after install before relying on it in production. + +## Biggest assumption to verify + +The storefront "Specialty" field on registration/account-edit relies on `additionalCustomerFormFields` +already being called by core's own front-office `CustomerFormatter` (passing `fields` by reference). +The legacy module achieved this only by overriding `CustomerFormatter` to add that hook call itself — +an override intentionally not reproduced here (see the architecture note above). If the installed +PrestaShop version's `CustomerFormatter` does not already call this hook natively, the storefront +field will not appear (back office, grid, and mail-variable behavior are unaffected either way, +since none of those depend on this hook). Verify on a real 1.7 and 9.0 install; if the hook isn't +native, the fallback is to make the field mandatory server-side via a small first-party +`CustomerFormatter` override scoped to only adding that one `Hook::exec()` call — much smaller and +safer than the legacy module's full-method copy. diff --git a/ws_customerspecialty/composer.json b/ws_customerspecialty/composer.json new file mode 100644 index 0000000..2f06b89 --- /dev/null +++ b/ws_customerspecialty/composer.json @@ -0,0 +1,25 @@ +{ + "name": "websmid/customerspecialty", + "description": "Adds a manageable specialty/profession field to customer accounts", + "authors": [ + { + "name": "Isabelle Oving-Anno - De Websmid BV", + "email": "isabelle@dewebsmid.nl" + } + ], + "require": {}, + "autoload": { + "psr-4": { + "Websmid\\CustomerSpecialty\\": "src/" + }, + "classmap": ["ws_customerspecialty.php"], + "exclude-from-classmap": [] + }, + "config": { + "preferred-install": "dist", + "prepend-autoloader": false + }, + "type": "prestashop-module", + "author": "Isabelle Oving-Anno - De Websmid BV", + "license": "MIT" +} diff --git a/ws_customerspecialty/composer.lock b/ws_customerspecialty/composer.lock new file mode 100644 index 0000000..d77cbde --- /dev/null +++ b/ws_customerspecialty/composer.lock @@ -0,0 +1,18 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "523e34cf5681a91b03bd57967a2d35e0", + "packages": [], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/ws_customerspecialty/config/command.yml b/ws_customerspecialty/config/command.yml new file mode 100644 index 0000000..40908e8 --- /dev/null +++ b/ws_customerspecialty/config/command.yml @@ -0,0 +1,24 @@ +services: + websmid.customerspecialty.command_handler.save_customer_specialty_handler: + class: 'Websmid\CustomerSpecialty\Command\CommandHandler\SaveCustomerSpecialtyCommandHandler' + tags: + - { name: 'tactician.handler', command: 'Websmid\CustomerSpecialty\Command\SaveCustomerSpecialtyCommand' } + - { name: 'messenger.message_handler' } + + websmid.customerspecialty.command_handler.move_customer_specialty_handler: + class: 'Websmid\CustomerSpecialty\Command\CommandHandler\MoveCustomerSpecialtyCommandHandler' + tags: + - { name: 'tactician.handler', command: 'Websmid\CustomerSpecialty\Command\MoveCustomerSpecialtyCommand' } + - { name: 'messenger.message_handler' } + + websmid.customerspecialty.command_handler.toggle_customer_specialty_active_handler: + class: 'Websmid\CustomerSpecialty\Command\CommandHandler\ToggleCustomerSpecialtyActiveCommandHandler' + tags: + - { name: 'tactician.handler', command: 'Websmid\CustomerSpecialty\Command\ToggleCustomerSpecialtyActiveCommand' } + - { name: 'messenger.message_handler' } + + websmid.customerspecialty.command_handler.link_customer_specialty_handler: + class: 'Websmid\CustomerSpecialty\Command\CommandHandler\LinkCustomerSpecialtyCommandHandler' + tags: + - { name: 'tactician.handler', command: 'Websmid\CustomerSpecialty\Command\LinkCustomerSpecialtyCommand' } + - { name: 'messenger.message_handler' } diff --git a/ws_customerspecialty/config/form.yml b/ws_customerspecialty/config/form.yml new file mode 100644 index 0000000..f038cdc --- /dev/null +++ b/ws_customerspecialty/config/form.yml @@ -0,0 +1,36 @@ +services: + websmid.customerspecialty.form.type.customerspecialty: + public: true + class: 'Websmid\CustomerSpecialty\Form\Type\CustomerSpecialtyType' + parent: 'form.type.translatable.aware' + tags: + - { name: form.type } + + websmid.customerspecialty.form.customerspecialty_data_provider: + public: true + class: 'Websmid\CustomerSpecialty\Form\DataProvider\CustomerSpecialtyDataProvider' + + websmid.customerspecialty.form.customerspecialty_data_form_builder: + public: true + class: 'PrestaShop\PrestaShop\Core\Form\IdentifiableObject\Builder\FormBuilder' + factory: + - '@prestashop.core.form.builder.form_builder_factory' + - 'create' + arguments: + - 'Websmid\CustomerSpecialty\Form\Type\CustomerSpecialtyType' + - '@websmid.customerspecialty.form.customerspecialty_data_provider' + + websmid.customerspecialty.form.customerspecialty_data_handler: + public: true + class: 'Websmid\CustomerSpecialty\Form\DataHandler\CustomerSpecialtyDataHandler' + arguments: + - '@prestashop.core.command_bus' + + websmid.customerspecialty.form.customerspecialty_form_handler: + public: true + class: 'PrestaShop\PrestaShop\Core\Form\IdentifiableObject\Handler\FormHandler' + factory: + - '@prestashop.core.form.identifiable_object.handler.form_handler_factory' + - 'create' + arguments: + - '@websmid.customerspecialty.form.customerspecialty_data_handler' diff --git a/ws_customerspecialty/config/grid.yml b/ws_customerspecialty/config/grid.yml new file mode 100644 index 0000000..3aac885 --- /dev/null +++ b/ws_customerspecialty/config/grid.yml @@ -0,0 +1,34 @@ +services: + websmid.customerspecialty.grid.definition.factory.customerspecialty_definition_factory: + public: true + class: 'Websmid\CustomerSpecialty\Grid\Definition\Factory\CustomerSpecialtyGridDefinitionFactory' + parent: 'prestashop.core.grid.definition.factory.abstract_grid_definition' + arguments: + - '@prestashop.adapter.legacy.configuration' + + websmid.customerspecialty.grid.query.customerspecialty_query_builder: + public: true + class: 'Websmid\CustomerSpecialty\Grid\Query\CustomerSpecialtyQueryBuilder' + parent: 'prestashop.core.grid.abstract_query_builder' + arguments: + - '@prestashop.core.query.doctrine_search_criteria_applicator' + - "@=service('prestashop.adapter.legacy.context').getContext().language.id" + - '@prestashop.core.grid.query.filter.doctrine_filter_applicator' + + websmid.customerspecialty.grid.data.factory.customerspecialty_data_factory: + public: true + class: '%prestashop.core.grid.data.factory.doctrine_grid_data_factory%' + arguments: + - '@websmid.customerspecialty.grid.query.customerspecialty_query_builder' + - '@prestashop.core.hook.dispatcher' + - '@prestashop.core.grid.query.doctrine_query_parser' + - 'CustomerSpecialty' + + websmid.customerspecialty.grid.customerspecialty_grid_factory: + public: true + class: 'PrestaShop\PrestaShop\Core\Grid\GridFactory' + arguments: + - '@websmid.customerspecialty.grid.definition.factory.customerspecialty_definition_factory' + - '@websmid.customerspecialty.grid.data.factory.customerspecialty_data_factory' + - '@prestashop.core.grid.filter.form_factory' + - '@prestashop.core.hook.dispatcher' diff --git a/ws_customerspecialty/config/provider.yml b/ws_customerspecialty/config/provider.yml new file mode 100644 index 0000000..ed49d1c --- /dev/null +++ b/ws_customerspecialty/config/provider.yml @@ -0,0 +1,4 @@ +services: + websmid.customerspecialty.form.choice_provider.customer_specialty_choice_provider: + public: true + class: 'Websmid\CustomerSpecialty\Form\ChoiceProvider\CustomerSpecialtyChoiceProvider' diff --git a/ws_customerspecialty/config/routes.yml b/ws_customerspecialty/config/routes.yml new file mode 100644 index 0000000..1f6b2b7 --- /dev/null +++ b/ws_customerspecialty/config/routes.yml @@ -0,0 +1,49 @@ +customerspecialty_index: + path: customerspecialty + methods: [GET] + defaults: + _controller: 'Websmid\CustomerSpecialty\Controller\CustomerSpecialtyController::indexAction' + _legacy_controller: AdminCustomerSpecialtyOverview + _legacy_link: AdminCustomerSpecialtyOverview:index + +customerspecialty_search: + path: customerspecialty + methods: [POST] + defaults: + _controller: 'Websmid\CustomerSpecialty\Controller\CustomerSpecialtyController::searchAction' + _legacy_controller: AdminCustomerSpecialtyOverview + +customerspecialty_create: + path: customerspecialty/create + methods: [POST, GET] + defaults: + _controller: 'Websmid\CustomerSpecialty\Controller\CustomerSpecialtyController::createAction' + _legacy_controller: AdminCustomerSpecialtyOverview + +customerspecialty_edit: + path: customerspecialty/edit/{idCustomerSpecialty} + methods: [POST, GET] + defaults: + _controller: 'Websmid\CustomerSpecialty\Controller\CustomerSpecialtyController::editAction' + _legacy_controller: AdminCustomerSpecialtyOverview + +customerspecialty_toggle_active: + path: customerspecialty/toggle-active/{idCustomerSpecialty} + methods: [GET] + defaults: + _controller: 'Websmid\CustomerSpecialty\Controller\CustomerSpecialtyController::toggleActiveAction' + _legacy_controller: AdminCustomerSpecialtyOverview + +customerspecialty_move_up: + path: customerspecialty/move-up/{idCustomerSpecialty} + methods: [GET] + defaults: + _controller: 'Websmid\CustomerSpecialty\Controller\CustomerSpecialtyController::moveUpAction' + _legacy_controller: AdminCustomerSpecialtyOverview + +customerspecialty_move_down: + path: customerspecialty/move-down/{idCustomerSpecialty} + methods: [GET] + defaults: + _controller: 'Websmid\CustomerSpecialty\Controller\CustomerSpecialtyController::moveDownAction' + _legacy_controller: AdminCustomerSpecialtyOverview diff --git a/ws_customerspecialty/config/services.yml b/ws_customerspecialty/config/services.yml new file mode 100644 index 0000000..8f283e3 --- /dev/null +++ b/ws_customerspecialty/config/services.yml @@ -0,0 +1,13 @@ +imports: + - { resource: form.yml } + - { resource: grid.yml } + - { resource: command.yml } + - { resource: provider.yml } + +services: + websmid.customerspecialty.entity.table_prefix_subscriber: + class: 'Websmid\CustomerSpecialty\Entity\TablePrefixSubscriber' + arguments: + - '%database_prefix%' + tags: + - { name: doctrine.event_subscriber } diff --git a/ws_customerspecialty/index.php b/ws_customerspecialty/index.php new file mode 100644 index 0000000..dafcd23 --- /dev/null +++ b/ws_customerspecialty/index.php @@ -0,0 +1,8 @@ +handle($command); + } + + public function handle(LinkCustomerSpecialtyCommand $command) + { + $this->entityManager = SymfonyContainer::getInstance()->get('doctrine.orm.entity_manager'); + $repository = $this->entityManager->getRepository(CustomerSpecialtyCustomer::class); + + $link = $repository->findOneBy(['idCustomer' => $command->getIdCustomer()]); + + if (!$link) { + $link = new CustomerSpecialtyCustomer(); + $link->setIdCustomer($command->getIdCustomer()); + $link->setDateAdd(new DateTime()); + } + + $link->setIdCustomerSpecialty($command->getIdCustomerSpecialty()); + $link->setDateUpd(new DateTime()); + + $this->entityManager->persist($link); + $this->entityManager->flush(); + + return true; + } +} diff --git a/ws_customerspecialty/src/Command/CommandHandler/MoveCustomerSpecialtyCommandHandler.php b/ws_customerspecialty/src/Command/CommandHandler/MoveCustomerSpecialtyCommandHandler.php new file mode 100644 index 0000000..12dd9a3 --- /dev/null +++ b/ws_customerspecialty/src/Command/CommandHandler/MoveCustomerSpecialtyCommandHandler.php @@ -0,0 +1,68 @@ +handle($command); + } + + public function handle(MoveCustomerSpecialtyCommand $command) + { + $this->entityManager = SymfonyContainer::getInstance()->get('doctrine.orm.entity_manager'); + $repository = $this->entityManager->getRepository(CustomerSpecialty::class); + + $customerSpecialty = $repository->find($command->getIdCustomerSpecialty()); + + if (!$customerSpecialty) { + throw new PrestaShopException('Customer specialty not found.'); + } + + $neighbor = $this->findNeighbor($repository, $customerSpecialty, $command->getDirection()); + + if (!$neighbor) { + return true; + } + + $currentPosition = $customerSpecialty->getPosition(); + $customerSpecialty->setPosition($neighbor->getPosition()); + $neighbor->setPosition($currentPosition); + + $this->entityManager->persist($customerSpecialty); + $this->entityManager->persist($neighbor); + $this->entityManager->flush(); + + return true; + } + + private function findNeighbor(EntityRepository $repository, CustomerSpecialty $customerSpecialty, string $direction): ?CustomerSpecialty + { + $queryBuilder = $repository->createQueryBuilder('cs'); + + if (MoveCustomerSpecialtyCommand::DIRECTION_UP === $direction) { + $queryBuilder + ->where('cs.position < :position') + ->orderBy('cs.position', 'DESC'); + } else { + $queryBuilder + ->where('cs.position > :position') + ->orderBy('cs.position', 'ASC'); + } + + return $queryBuilder + ->setParameter('position', $customerSpecialty->getPosition()) + ->setMaxResults(1) + ->getQuery() + ->getOneOrNullResult(); + } +} diff --git a/ws_customerspecialty/src/Command/CommandHandler/SaveCustomerSpecialtyCommandHandler.php b/ws_customerspecialty/src/Command/CommandHandler/SaveCustomerSpecialtyCommandHandler.php new file mode 100644 index 0000000..92d124a --- /dev/null +++ b/ws_customerspecialty/src/Command/CommandHandler/SaveCustomerSpecialtyCommandHandler.php @@ -0,0 +1,104 @@ +handle($command); + } + + public function handle(SaveCustomerSpecialtyCommand $command) + { + if (empty(array_filter($command->getTitles()))) { + throw new PrestaShopException('At least one title is required.'); + } + + $this->entityManager = SymfonyContainer::getInstance()->get('doctrine.orm.entity_manager'); + + $customerSpecialty = $this->findOrCreateCustomerSpecialty($command->getIdCustomerSpecialty()); + $customerSpecialty->setActive($command->isActive()); + $this->stampDates($customerSpecialty); + + $this->entityManager->persist($customerSpecialty); + $this->entityManager->flush(); + + $this->saveTitles($customerSpecialty->getId(), $command->getTitles()); + + return $customerSpecialty->getId(); + } + + private function findOrCreateCustomerSpecialty(int $idCustomerSpecialty): CustomerSpecialty + { + if ($idCustomerSpecialty > 0) { + $customerSpecialty = $this->entityManager->getRepository(CustomerSpecialty::class)->find($idCustomerSpecialty); + + if ($customerSpecialty) { + return $customerSpecialty; + } + } + + $customerSpecialty = new CustomerSpecialty(); + $customerSpecialty->setPosition($this->getNextPosition()); + + return $customerSpecialty; + } + + private function getNextPosition(): int + { + $result = $this->entityManager->getRepository(CustomerSpecialty::class) + ->createQueryBuilder('cs') + ->select('MAX(cs.position) as maxPosition') + ->getQuery() + ->getSingleScalarResult(); + + return null === $result ? 0 : ((int) $result + 1); + } + + private function stampDates(CustomerSpecialty $customerSpecialty): void + { + $now = new DateTime(); + + if (null === $customerSpecialty->getId()) { + $customerSpecialty->setDateAdd($now); + } + + $customerSpecialty->setDateUpd($now); + } + + private function saveTitles(int $idCustomerSpecialty, array $titles): void + { + $repository = $this->entityManager->getRepository(CustomerSpecialtyLang::class); + + foreach ($repository->findBy(['idCustomerSpecialty' => $idCustomerSpecialty]) as $existingLang) { + $this->entityManager->remove($existingLang); + } + + $this->entityManager->flush(); + + foreach ($titles as $idLang => $title) { + if ('' === trim((string) $title)) { + continue; + } + + $lang = new CustomerSpecialtyLang(); + $lang->setIdCustomerSpecialty($idCustomerSpecialty); + $lang->setIdLang((int) $idLang); + $lang->setTitle($title); + + $this->entityManager->persist($lang); + } + + $this->entityManager->flush(); + } +} diff --git a/ws_customerspecialty/src/Command/CommandHandler/ToggleCustomerSpecialtyActiveCommandHandler.php b/ws_customerspecialty/src/Command/CommandHandler/ToggleCustomerSpecialtyActiveCommandHandler.php new file mode 100644 index 0000000..f8d3b13 --- /dev/null +++ b/ws_customerspecialty/src/Command/CommandHandler/ToggleCustomerSpecialtyActiveCommandHandler.php @@ -0,0 +1,35 @@ +handle($command); + } + + public function handle(ToggleCustomerSpecialtyActiveCommand $command) + { + $entityManager = SymfonyContainer::getInstance()->get('doctrine.orm.entity_manager'); + $repository = $entityManager->getRepository(CustomerSpecialty::class); + + $customerSpecialty = $repository->find($command->getIdCustomerSpecialty()); + + if (!$customerSpecialty) { + throw new PrestaShopException('Customer specialty not found.'); + } + + $customerSpecialty->setActive(!$customerSpecialty->isActive()); + + $entityManager->persist($customerSpecialty); + $entityManager->flush(); + + return true; + } +} diff --git a/ws_customerspecialty/src/Command/LinkCustomerSpecialtyCommand.php b/ws_customerspecialty/src/Command/LinkCustomerSpecialtyCommand.php new file mode 100644 index 0000000..bfd20ea --- /dev/null +++ b/ws_customerspecialty/src/Command/LinkCustomerSpecialtyCommand.php @@ -0,0 +1,25 @@ +idCustomer = $idCustomer; + $this->idCustomerSpecialty = $idCustomerSpecialty; + } + + public function getIdCustomer(): int + { + return $this->idCustomer; + } + + public function getIdCustomerSpecialty(): int + { + return $this->idCustomerSpecialty; + } +} diff --git a/ws_customerspecialty/src/Command/MoveCustomerSpecialtyCommand.php b/ws_customerspecialty/src/Command/MoveCustomerSpecialtyCommand.php new file mode 100644 index 0000000..cf7c7a1 --- /dev/null +++ b/ws_customerspecialty/src/Command/MoveCustomerSpecialtyCommand.php @@ -0,0 +1,28 @@ +idCustomerSpecialty = $idCustomerSpecialty; + $this->direction = $direction; + } + + public function getIdCustomerSpecialty(): int + { + return $this->idCustomerSpecialty; + } + + public function getDirection(): string + { + return $this->direction; + } +} diff --git a/ws_customerspecialty/src/Command/SaveCustomerSpecialtyCommand.php b/ws_customerspecialty/src/Command/SaveCustomerSpecialtyCommand.php new file mode 100644 index 0000000..9adc034 --- /dev/null +++ b/ws_customerspecialty/src/Command/SaveCustomerSpecialtyCommand.php @@ -0,0 +1,32 @@ +idCustomerSpecialty = $idCustomerSpecialty; + $this->active = $active; + $this->titles = $titles; + } + + public function getIdCustomerSpecialty(): int + { + return $this->idCustomerSpecialty; + } + + public function isActive(): bool + { + return $this->active; + } + + public function getTitles(): array + { + return $this->titles; + } +} diff --git a/ws_customerspecialty/src/Command/ToggleCustomerSpecialtyActiveCommand.php b/ws_customerspecialty/src/Command/ToggleCustomerSpecialtyActiveCommand.php new file mode 100644 index 0000000..40f717c --- /dev/null +++ b/ws_customerspecialty/src/Command/ToggleCustomerSpecialtyActiveCommand.php @@ -0,0 +1,18 @@ +idCustomerSpecialty = $idCustomerSpecialty; + } + + public function getIdCustomerSpecialty(): int + { + return $this->idCustomerSpecialty; + } +} diff --git a/ws_customerspecialty/src/Config/Config.php b/ws_customerspecialty/src/Config/Config.php new file mode 100644 index 0000000..8dc5dc1 --- /dev/null +++ b/ws_customerspecialty/src/Config/Config.php @@ -0,0 +1,36 @@ + 'AdminCustomerSpecialtyOverview', + 'parent_class_name' => 'AdminParentCustomer', + 'name' => [ + 'en' => 'Customer Specialties', + 'nl' => 'Klantspecialisaties', + ], + 'module' => 'ws_customerspecialty', + 'icon' => 'work', + 'route_name' => 'customerspecialty_index', + 'active' => 1, + ], + ]; + + const FRONT_ENABLED_CONFIG_KEY = 'WS_CUSTOMERSPECIALTY_FRONT_ENABLED'; +} diff --git a/ws_customerspecialty/src/Controller/CustomerSpecialtyController.php b/ws_customerspecialty/src/Controller/CustomerSpecialtyController.php new file mode 100644 index 0000000..a882aaa --- /dev/null +++ b/ws_customerspecialty/src/Controller/CustomerSpecialtyController.php @@ -0,0 +1,157 @@ +get('websmid.customerspecialty.grid.customerspecialty_grid_factory'); + $grid = $gridFactory->getGrid($filters); + + return $this->render('@Modules/ws_customerspecialty/views/templates/admin/grid/customerspecialty.html.twig', [ + 'customerSpecialtyGrid' => $this->presentGrid($grid), + 'layoutHeaderToolbarBtn' => $this->getToolbarButtons($request->get('_legacy_controller')), + ]); + } + + public function searchAction(Request $request): RedirectResponse + { + return $this->get('prestashop.bundle.grid.response_builder')->buildSearchResponse( + $this->get('websmid.customerspecialty.grid.definition.factory.customerspecialty_definition_factory'), + $request, + CustomerSpecialtyGridDefinitionFactory::GRID_ID, + 'customerspecialty_index' + ); + } + + public function createAction(Request $request): Response + { + $formBuilder = $this->get('websmid.customerspecialty.form.customerspecialty_data_form_builder'); + $formHandler = $this->get('websmid.customerspecialty.form.customerspecialty_form_handler'); + + $form = $formBuilder->getForm(); + $form->handleRequest($request); + + try { + $handlerResult = $formHandler->handle($form); + + if (null !== $handlerResult->getIdentifiableObjectId()) { + $this->addFlash('success', $this->trans('Successful creation', [], 'Admin.Notifications.Success')); + + return $this->redirectToRoute('customerspecialty_index'); + } + } catch (Exception $e) { + $this->addFlash('error', $this->getErrorMessageForException($e, $this->getErrorMessages())); + } + + return $this->render('@Modules/ws_customerspecialty/views/templates/admin/form/customerspecialty.html.twig', [ + 'customerSpecialtyForm' => $form->createView(), + ]); + } + + public function editAction(Request $request, int $idCustomerSpecialty): Response + { + $formBuilder = $this->get('websmid.customerspecialty.form.customerspecialty_data_form_builder'); + $formHandler = $this->get('websmid.customerspecialty.form.customerspecialty_form_handler'); + + $form = $formBuilder->getFormFor($idCustomerSpecialty); + + try { + $form->handleRequest($request); + $handlerResult = $formHandler->handleFor($idCustomerSpecialty, $form); + + if ($handlerResult->isSubmitted() && $handlerResult->isValid()) { + $this->addFlash('success', $this->trans('Successful update', [], 'Admin.Notifications.Success')); + + return $this->redirectToRoute('customerspecialty_index'); + } + } catch (Exception $e) { + $this->addFlash('error', $this->getErrorMessageForException($e, $this->getErrorMessages())); + } + + return $this->render('@Modules/ws_customerspecialty/views/templates/admin/form/customerspecialty.html.twig', [ + 'customerSpecialtyForm' => $form->createView(), + ]); + } + + public function toggleActiveAction(int $idCustomerSpecialty): RedirectResponse + { + try { + $this->getCommandBus()->handle(new ToggleCustomerSpecialtyActiveCommand($idCustomerSpecialty)); + $this->addFlash('success', $this->trans('Successful update', [], 'Admin.Notifications.Success')); + } catch (Exception $e) { + $this->addFlash('error', $this->getErrorMessageForException($e, $this->getErrorMessages())); + } + + return $this->redirectToRoute('customerspecialty_index'); + } + + public function moveUpAction(int $idCustomerSpecialty): RedirectResponse + { + return $this->move($idCustomerSpecialty, MoveCustomerSpecialtyCommand::DIRECTION_UP); + } + + public function moveDownAction(int $idCustomerSpecialty): RedirectResponse + { + return $this->move($idCustomerSpecialty, MoveCustomerSpecialtyCommand::DIRECTION_DOWN); + } + + private function move(int $idCustomerSpecialty, string $direction): RedirectResponse + { + try { + $this->getCommandBus()->handle(new MoveCustomerSpecialtyCommand($idCustomerSpecialty, $direction)); + } catch (Exception $e) { + $this->addFlash('error', $this->getErrorMessageForException($e, $this->getErrorMessages())); + } + + return $this->redirectToRoute('customerspecialty_index'); + } + + private function getToolbarButtons(string $securitySubject): array + { + $toolbarButtons = []; + + if (!$this->isGranted(PageVoter::CREATE, $securitySubject)) { + return $toolbarButtons; + } + + $toolbarButtons['customerspecialty_create'] = [ + 'href' => $this->generateUrl('customerspecialty_create'), + 'desc' => $this->trans('New specialty', [], 'Admin.Actions'), + 'icon' => 'add_circle_outline', + 'class' => 'btn-primary', + 'floating_class' => '', + ]; + + return $toolbarButtons; + } + + private function getErrorMessages(): array + { + return [ + PrestaShopException::class => [ + 'key' => 'An error occurred while saving the customer specialty.', + 'domain' => 'Modules.Wscustomerspecialty.Admin', + 'parameters' => [], + ], + ]; + } + + private function getCommandBus() + { + return $this->get('prestashop.core.command_bus'); + } +} diff --git a/ws_customerspecialty/src/Entity/CustomerSpecialty.php b/ws_customerspecialty/src/Entity/CustomerSpecialty.php new file mode 100644 index 0000000..12d468e --- /dev/null +++ b/ws_customerspecialty/src/Entity/CustomerSpecialty.php @@ -0,0 +1,104 @@ +id; + } + + public function getPosition(): int + { + return $this->position; + } + + public function setPosition(int $position): self + { + $this->position = $position; + + return $this; + } + + public function isActive(): bool + { + return $this->active; + } + + public function setActive(bool $active): self + { + $this->active = $active; + + return $this; + } + + public function getDateAdd(): ?DateTime + { + return $this->dateAdd; + } + + public function setDateAdd(DateTime $dateAdd): self + { + $this->dateAdd = $dateAdd; + + return $this; + } + + public function getDateUpd(): ?DateTime + { + return $this->dateUpd; + } + + public function setDateUpd(DateTime $dateUpd): self + { + $this->dateUpd = $dateUpd; + + return $this; + } + + public function toArray(): array + { + return [ + 'id_ws_customerspecialty' => $this->id, + 'position' => $this->position, + 'active' => $this->active, + 'date_add' => $this->dateAdd ? $this->dateAdd->format('Y-m-d H:i:s') : null, + 'date_upd' => $this->dateUpd ? $this->dateUpd->format('Y-m-d H:i:s') : null, + ]; + } +} diff --git a/ws_customerspecialty/src/Entity/CustomerSpecialtyCustomer.php b/ws_customerspecialty/src/Entity/CustomerSpecialtyCustomer.php new file mode 100644 index 0000000..0fe4a9d --- /dev/null +++ b/ws_customerspecialty/src/Entity/CustomerSpecialtyCustomer.php @@ -0,0 +1,104 @@ +id; + } + + public function getIdCustomer(): int + { + return $this->idCustomer; + } + + public function setIdCustomer(int $idCustomer): self + { + $this->idCustomer = $idCustomer; + + return $this; + } + + public function getIdCustomerSpecialty(): int + { + return $this->idCustomerSpecialty; + } + + public function setIdCustomerSpecialty(int $idCustomerSpecialty): self + { + $this->idCustomerSpecialty = $idCustomerSpecialty; + + return $this; + } + + public function getDateAdd(): ?DateTime + { + return $this->dateAdd; + } + + public function setDateAdd(DateTime $dateAdd): self + { + $this->dateAdd = $dateAdd; + + return $this; + } + + public function getDateUpd(): ?DateTime + { + return $this->dateUpd; + } + + public function setDateUpd(DateTime $dateUpd): self + { + $this->dateUpd = $dateUpd; + + return $this; + } + + public function toArray(): array + { + return [ + 'id_ws_customerspecialty_customer' => $this->id, + 'id_customer' => $this->idCustomer, + 'id_ws_customerspecialty' => $this->idCustomerSpecialty, + 'date_add' => $this->dateAdd ? $this->dateAdd->format('Y-m-d H:i:s') : null, + 'date_upd' => $this->dateUpd ? $this->dateUpd->format('Y-m-d H:i:s') : null, + ]; + } +} diff --git a/ws_customerspecialty/src/Entity/CustomerSpecialtyLang.php b/ws_customerspecialty/src/Entity/CustomerSpecialtyLang.php new file mode 100644 index 0000000..430eb76 --- /dev/null +++ b/ws_customerspecialty/src/Entity/CustomerSpecialtyLang.php @@ -0,0 +1,85 @@ +id; + } + + public function getIdCustomerSpecialty(): int + { + return $this->idCustomerSpecialty; + } + + public function setIdCustomerSpecialty(int $idCustomerSpecialty): self + { + $this->idCustomerSpecialty = $idCustomerSpecialty; + + return $this; + } + + public function getIdLang(): int + { + return $this->idLang; + } + + public function setIdLang(int $idLang): self + { + $this->idLang = $idLang; + + return $this; + } + + public function getTitle(): string + { + return $this->title; + } + + public function setTitle(string $title): self + { + $this->title = $title; + + return $this; + } + + public function toArray(): array + { + return [ + 'id_ws_customerspecialty_lang' => $this->id, + 'id_ws_customerspecialty' => $this->idCustomerSpecialty, + 'id_lang' => $this->idLang, + 'title' => $this->title, + ]; + } +} diff --git a/ws_customerspecialty/src/Entity/TablePrefixSubscriber.php b/ws_customerspecialty/src/Entity/TablePrefixSubscriber.php new file mode 100644 index 0000000..304437c --- /dev/null +++ b/ws_customerspecialty/src/Entity/TablePrefixSubscriber.php @@ -0,0 +1,39 @@ +prefix = $prefix ?: 'ps_'; + } + + public function getSubscribedEvents(): array + { + return [Events::loadClassMetadata]; + } + + public function loadClassMetadata(LoadClassMetadataEventArgs $args): void + { + $classMetadata = $args->getClassMetadata(); + + if (strpos($classMetadata->getName(), 'Websmid\\CustomerSpecialty\\') !== 0) { + return; + } + + $tableName = $classMetadata->getTableName(); + + if (strpos($tableName, $this->prefix) === 0) { + return; + } + + $classMetadata->setPrimaryTable(['name' => $this->prefix . $tableName]); + } +} diff --git a/ws_customerspecialty/src/Form/ChoiceProvider/CustomerSpecialtyChoiceProvider.php b/ws_customerspecialty/src/Form/ChoiceProvider/CustomerSpecialtyChoiceProvider.php new file mode 100644 index 0000000..5b15c90 --- /dev/null +++ b/ws_customerspecialty/src/Form/ChoiceProvider/CustomerSpecialtyChoiceProvider.php @@ -0,0 +1,45 @@ +language->id; + + $sql = ' + SELECT cs.`id_ws_customerspecialty`, csl.`title` + FROM `' . _DB_PREFIX_ . 'ws_customerspecialty` cs + INNER JOIN `' . _DB_PREFIX_ . 'ws_customerspecialty_lang` csl + ON csl.`id_ws_customerspecialty` = cs.`id_ws_customerspecialty` + AND csl.`id_lang` = ' . $idLang . ' + ' . ($includeInactive ? '' : 'WHERE cs.`active` = 1') . ' + ORDER BY cs.`position` ASC + '; + + $rows = Db::getInstance()->executeS($sql); + $choices = []; + + foreach ($rows as $row) { + $choices[$row['title']] = (int) $row['id_ws_customerspecialty']; + } + + return $choices; + } + + public function getChoicesAttributes(): array + { + return []; + } +} diff --git a/ws_customerspecialty/src/Form/DataHandler/CustomerSpecialtyDataHandler.php b/ws_customerspecialty/src/Form/DataHandler/CustomerSpecialtyDataHandler.php new file mode 100644 index 0000000..5ee368b --- /dev/null +++ b/ws_customerspecialty/src/Form/DataHandler/CustomerSpecialtyDataHandler.php @@ -0,0 +1,36 @@ +commandBus = $commandBus; + } + + public function create(array $data) + { + return $this->save(0, $data); + } + + public function update($id, array $data) + { + return $this->save((int) $id, $data); + } + + private function save(int $id, array $data) + { + return $this->commandBus->handle(new SaveCustomerSpecialtyCommand( + $id, + (bool) ($data['active'] ?? false), + $data['title'] ?? [] + )); + } +} diff --git a/ws_customerspecialty/src/Form/DataProvider/CustomerSpecialtyDataProvider.php b/ws_customerspecialty/src/Form/DataProvider/CustomerSpecialtyDataProvider.php new file mode 100644 index 0000000..23ccf2c --- /dev/null +++ b/ws_customerspecialty/src/Form/DataProvider/CustomerSpecialtyDataProvider.php @@ -0,0 +1,47 @@ + [], + 'active' => true, + ]; + } + + public function getData($id): array + { + $entityManager = SymfonyContainer::getInstance()->get('doctrine.orm.entity_manager'); + + $customerSpecialty = $entityManager->getRepository(CustomerSpecialty::class)->find((int) $id); + + if (!$customerSpecialty) { + return $this->getDefaultData(); + } + + return [ + 'title' => $this->loadTitles($entityManager, (int) $id), + 'active' => $customerSpecialty->isActive(), + ]; + } + + private function loadTitles(EntityManagerInterface $entityManager, int $idCustomerSpecialty): array + { + $titles = []; + + foreach ($entityManager->getRepository(CustomerSpecialtyLang::class)->findBy(['idCustomerSpecialty' => $idCustomerSpecialty]) as $lang) { + $titles[$lang->getIdLang()] = $lang->getTitle(); + } + + return $titles; + } +} diff --git a/ws_customerspecialty/src/Form/Type/CustomerSpecialtyType.php b/ws_customerspecialty/src/Form/Type/CustomerSpecialtyType.php new file mode 100644 index 0000000..13b48a7 --- /dev/null +++ b/ws_customerspecialty/src/Form/Type/CustomerSpecialtyType.php @@ -0,0 +1,41 @@ +translator = $translator; + $this->locales = $locales; + } + + public function buildForm(FormBuilderInterface $formBuilder, array $options): void + { + $formBuilder + ->add('title', TranslatableType::class, [ + 'type' => TextType::class, + 'label' => $this->trans('Title'), + 'required' => true, + ]) + ->add('active', SwitchType::class, [ + 'label' => $this->trans('Active'), + 'required' => false, + ]); + } + + private function trans(string $label): string + { + return $this->translator->trans($label, [], 'Modules.Wscustomerspecialty.Admin'); + } +} diff --git a/ws_customerspecialty/src/Grid/Definition/Factory/CustomerSpecialtyGridDefinitionFactory.php b/ws_customerspecialty/src/Grid/Definition/Factory/CustomerSpecialtyGridDefinitionFactory.php new file mode 100644 index 0000000..a110c39 --- /dev/null +++ b/ws_customerspecialty/src/Grid/Definition/Factory/CustomerSpecialtyGridDefinitionFactory.php @@ -0,0 +1,139 @@ +trans('Customer Specialties', [], 'Modules.Wscustomerspecialty.Admin'); + } + + protected function getColumns() + { + return (new ColumnCollection()) + ->add( + (new DataColumn('id_ws_customerspecialty')) + ->setName($this->trans('ID', [], 'Admin.Global')) + ->setOptions([ + 'field' => 'id_ws_customerspecialty', + ]) + ) + ->add( + (new DataColumn('title')) + ->setName($this->trans('Title', [], 'Modules.Wscustomerspecialty.Admin')) + ->setOptions([ + 'field' => 'title', + ]) + ) + ->add( + (new ToggleColumn('active')) + ->setName($this->trans('Active', [], 'Admin.Global')) + ->setOptions([ + 'field' => 'active', + 'primary_field' => 'id_ws_customerspecialty', + 'route' => 'customerspecialty_toggle_active', + 'route_param_name' => 'idCustomerSpecialty', + ]) + ) + ->add( + (new ActionColumn('actions')) + ->setName($this->trans('Actions', [], 'Admin.Global')) + ->setOptions([ + 'actions' => (new RowActionCollection()) + ->add( + (new LinkRowAction('edit')) + ->setName($this->trans('Edit', [], 'Admin.Actions')) + ->setIcon('edit') + ->setOptions([ + 'route' => 'customerspecialty_edit', + 'route_param_name' => 'idCustomerSpecialty', + ]) + ) + ->add( + (new LinkRowAction('move_up')) + ->setName($this->trans('Move up', [], 'Modules.Wscustomerspecialty.Admin')) + ->setIcon('arrow_upward') + ->setOptions([ + 'route' => 'customerspecialty_move_up', + 'route_param_name' => 'idCustomerSpecialty', + ]) + ) + ->add( + (new LinkRowAction('move_down')) + ->setName($this->trans('Move down', [], 'Modules.Wscustomerspecialty.Admin')) + ->setIcon('arrow_downward') + ->setOptions([ + 'route' => 'customerspecialty_move_down', + 'route_param_name' => 'idCustomerSpecialty', + ]) + ), + ]) + ); + } + + protected function getFilters() + { + return (new FilterCollection()) + ->add( + (new Filter('title', TextType::class)) + ->setAssociatedColumn('title') + ) + ->add( + (new Filter('active', ChoiceType::class)) + ->setTypeOptions([ + 'choices' => [ + $this->trans('Active', [], 'Admin.Global') => 1, + $this->trans('Inactive', [], 'Admin.Global') => 0, + ], + 'required' => false, + ]) + ->setAssociatedColumn('active') + ) + ->add( + (new Filter('actions', SearchAndResetType::class)) + ->setTypeOptions([ + 'reset_route' => 'customerspecialty_index', + 'redirect_route' => 'customerspecialty_index', + ]) + ->setAssociatedColumn('actions') + ); + } + + protected function getGridActions() + { + return (new GridActionCollection()) + ->add( + (new SimpleGridAction('common_refresh_list')) + ->setName($this->trans('Refresh list', [], 'Admin.Catalog.Feature')) + ->setIcon('refresh') + ) + ->add( + (new SimpleGridAction('common_show_query')) + ->setName($this->trans('Show SQL query', [], 'Admin.Catalog.Feature')) + ->setIcon('code') + ); + } +} diff --git a/ws_customerspecialty/src/Grid/Filters/CustomerSpecialtyFilters.php b/ws_customerspecialty/src/Grid/Filters/CustomerSpecialtyFilters.php new file mode 100644 index 0000000..b33978c --- /dev/null +++ b/ws_customerspecialty/src/Grid/Filters/CustomerSpecialtyFilters.php @@ -0,0 +1,22 @@ + 50, + 'offset' => 0, + 'orderBy' => 'cs.position', + 'sortOrder' => 'asc', + 'filters' => [], + ]; + } +} diff --git a/ws_customerspecialty/src/Grid/Query/CustomerSpecialtyQueryBuilder.php b/ws_customerspecialty/src/Grid/Query/CustomerSpecialtyQueryBuilder.php new file mode 100644 index 0000000..444eca0 --- /dev/null +++ b/ws_customerspecialty/src/Grid/Query/CustomerSpecialtyQueryBuilder.php @@ -0,0 +1,90 @@ +searchCriteriaApplicator = $searchCriteriaApplicator; + $this->contextLanguageId = $contextLanguageId; + $this->filterApplicator = $filterApplicator; + } + + public function getSearchQueryBuilder(SearchCriteriaInterface $searchCriteria) + { + $queryBuilder = $this->getBaseQuery(); + $queryBuilder->select('cs.id_ws_customerspecialty, cs.active, cs.position, csl.title'); + + $this->applyFilters($queryBuilder, $searchCriteria); + + $this->searchCriteriaApplicator + ->applyPagination($searchCriteria, $queryBuilder) + ->applySorting($searchCriteria, $queryBuilder); + + return $queryBuilder; + } + + public function getCountQueryBuilder(SearchCriteriaInterface $searchCriteria) + { + $queryBuilder = $this->getBaseQuery(); + $queryBuilder->select('COUNT(cs.id_ws_customerspecialty)'); + + $this->applyFilters($queryBuilder, $searchCriteria); + + return $queryBuilder; + } + + private function getBaseQuery(): QueryBuilder + { + $queryBuilder = $this->connection->createQueryBuilder(); + + $queryBuilder + ->from($this->dbPrefix . 'ws_customerspecialty', 'cs') + ->leftJoin( + 'cs', + $this->dbPrefix . 'ws_customerspecialty_lang', + 'csl', + 'csl.id_ws_customerspecialty = cs.id_ws_customerspecialty AND csl.id_lang = :contextLanguageId' + ) + ->setParameter('contextLanguageId', $this->contextLanguageId); + + return $queryBuilder; + } + + private function applyFilters(QueryBuilder $queryBuilder, SearchCriteriaInterface $searchCriteria): void + { + foreach ($searchCriteria->getFilters() as $filterName => $filterValue) { + if ('' === $filterValue || null === $filterValue) { + continue; + } + + switch ($filterName) { + case 'title': + $queryBuilder->andWhere('csl.title LIKE :title') + ->setParameter('title', '%' . pSQL($filterValue) . '%'); + break; + case 'active': + $queryBuilder->andWhere('cs.active = :active') + ->setParameter('active', (int) $filterValue); + break; + } + } + } +} diff --git a/ws_customerspecialty/src/Module/AdminHooks.php b/ws_customerspecialty/src/Module/AdminHooks.php new file mode 100644 index 0000000..a260795 --- /dev/null +++ b/ws_customerspecialty/src/Module/AdminHooks.php @@ -0,0 +1,206 @@ +displayConfirmation($this->trans('Settings saved.', [], 'Admin.Notifications.Success')); + } + + $this->context->smarty->assign([ + 'front_enabled' => (bool) Configuration::get(Config::FRONT_ENABLED_CONFIG_KEY), + 'form_action' => $this->context->link->getAdminLink('AdminModules') . '&configure=' . $this->name, + 'confirmation' => $confirmation, + ]); + + return $this->fetch('module:ws_customerspecialty/views/templates/admin/configuration.tpl'); + } + + public function hookActionCustomerFormBuilderModifier(array $params): void + { + /** @var FormBuilderInterface $formBuilder */ + $formBuilder = $params['form_builder']; + $idCustomer = (int) $params['id']; + + $formBuilder->add('specialization', ChoiceType::class, [ + 'label' => $this->trans('Specialty', [], 'Modules.Wscustomerspecialty.Admin'), + 'required' => true, + 'placeholder' => $this->trans('Please select', [], 'Admin.Global'), + 'choices' => $this->getCustomerSpecialtyChoiceProvider()->getChoices([ + 'include_inactive' => $idCustomer > 0, + ]), + ]); + + if ($idCustomer > 0) { + $params['data']['specialization'] = $this->loadCustomerSpecialtyId($idCustomer); + $formBuilder->setData($params['data']); + } + } + + public function hookActionAfterCreateCustomerFormHandler(array $params): void + { + $this->persistBackOfficeCustomerSpecialty($params); + } + + public function hookActionAfterUpdateCustomerFormHandler(array $params): void + { + $this->persistBackOfficeCustomerSpecialty($params); + } + + public function hookActionCustomerGridDefinitionModifier(array $params): void + { + $definition = $params['definition']; + + $definition->getColumns()->addBefore( + 'social_title', + (new DataColumn('specialization')) + ->setName($this->trans('Specialty', [], 'Modules.Wscustomerspecialty.Admin')) + ->setOptions([ + 'field' => 'specialization', + ]) + ); + + $choices = ['--' => 0] + $this->getCustomerSpecialtyChoiceProvider()->getChoices(['include_inactive' => true]); + + $definition->getFilters()->add( + (new Filter('specialization', ChoiceType::class)) + ->setTypeOptions([ + 'choices' => $choices, + 'required' => false, + ]) + ->setAssociatedColumn('specialization') + ); + } + + public function hookActionCustomerGridQueryBuilderModifier(array $params): void + { + $searchQueryBuilder = $params['search_query_builder']; + $searchCriteria = $params['search_criteria']; + $idLang = (int) $this->context->language->id; + + $searchQueryBuilder->addSelect( + '(SELECT csl.title + FROM ' . _DB_PREFIX_ . 'ws_customerspecialty_customer cscu + INNER JOIN ' . _DB_PREFIX_ . 'ws_customerspecialty_lang csl + ON csl.id_ws_customerspecialty = cscu.id_ws_customerspecialty AND csl.id_lang = ' . $idLang . ' + WHERE cscu.id_customer = c.id_customer + ) as specialization' + ); + + $this->applyCustomerSpecialtySorting($searchCriteria, $searchQueryBuilder); + $this->applyCustomerSpecialtyFilter($searchCriteria, $searchQueryBuilder); + } + + public function hookDisplayAdminCustomers(array $params) + { + $title = $this->loadCustomerSpecialtyTitle((int) $params['id_customer']); + + if (null === $title) { + return ''; + } + + $this->context->smarty->assign(['specialization' => $title]); + + return $this->fetch('module:ws_customerspecialty/views/templates/hook/display_admin_customers.tpl'); + } + + private function persistBackOfficeCustomerSpecialty(array $params): void + { + $idCustomer = (int) $params['id']; + $specialization = (int) ($params['form_data']['specialization'] ?? 0); + + if ($idCustomer <= 0 || $specialization <= 0) { + return; + } + + $this->get('prestashop.core.command_bus')->handle( + new LinkCustomerSpecialtyCommand($idCustomer, $specialization) + ); + } + + private function applyCustomerSpecialtySorting($searchCriteria, $searchQueryBuilder): void + { + if ('specialization' !== $searchCriteria->getOrderBy()) { + return; + } + + $searchQueryBuilder->orderBy('specialization', $searchCriteria->getOrderWay()); + } + + private function applyCustomerSpecialtyFilter($searchCriteria, $searchQueryBuilder): void + { + $filters = $searchCriteria->getFilters(); + + if (!isset($filters['specialization']) || '' === $filters['specialization']) { + return; + } + + $idCustomerSpecialty = (int) $filters['specialization']; + + if (0 === $idCustomerSpecialty) { + $searchQueryBuilder->andWhere( + 'NOT EXISTS (SELECT 1 FROM ' . _DB_PREFIX_ . 'ws_customerspecialty_customer cscu WHERE cscu.id_customer = c.id_customer)' + ); + + return; + } + + $searchQueryBuilder + ->andWhere( + 'EXISTS ( + SELECT 1 FROM ' . _DB_PREFIX_ . 'ws_customerspecialty_customer cscu + WHERE cscu.id_customer = c.id_customer AND cscu.id_ws_customerspecialty = :idCustomerSpecialtyFilter + )' + ) + ->setParameter('idCustomerSpecialtyFilter', $idCustomerSpecialty); + } + + private function loadCustomerSpecialtyId(int $idCustomer): int + { + return (int) Db::getInstance()->getValue( + 'SELECT `id_ws_customerspecialty` FROM `' . _DB_PREFIX_ . 'ws_customerspecialty_customer` + WHERE `id_customer` = ' . $idCustomer + ); + } + + private function loadCustomerSpecialtyTitle(int $idCustomer): ?string + { + $idLang = (int) $this->context->language->id; + + $title = Db::getInstance()->getValue(' + SELECT csl.title + FROM `' . _DB_PREFIX_ . 'ws_customerspecialty_customer` cscu + INNER JOIN `' . _DB_PREFIX_ . 'ws_customerspecialty_lang` csl + ON csl.id_ws_customerspecialty = cscu.id_ws_customerspecialty AND csl.id_lang = ' . $idLang . ' + WHERE cscu.id_customer = ' . $idCustomer . ' + '); + + return false !== $title && null !== $title ? (string) $title : null; + } + + private function getCustomerSpecialtyChoiceProvider() + { + return $this->get('websmid.customerspecialty.form.choice_provider.customer_specialty_choice_provider'); + } +} diff --git a/ws_customerspecialty/src/Module/DisplayHooks.php b/ws_customerspecialty/src/Module/DisplayHooks.php new file mode 100644 index 0000000..0968b83 --- /dev/null +++ b/ws_customerspecialty/src/Module/DisplayHooks.php @@ -0,0 +1,121 @@ +isFrontEnabled()) { + return; + } + + $choices = $this->get('websmid.customerspecialty.form.choice_provider.customer_specialty_choice_provider')->getChoices(); + + if (empty($choices)) { + return; + } + + $specializationField = (new FormField()) + ->setName('specialization') + ->setType('select') + ->setLabel($this->trans('Specialty', [], 'Modules.Wscustomerspecialty.Shop')) + ->setRequired(true) + ->setAvailableValues($choices); + + if (isset($params['fields'])) { + $this->insertFieldAfterEmail($params['fields'], $specializationField); + + return; + } + + return $specializationField; + } + + public function hookActionObjectCustomerAddAfter(array $params): void + { + $this->persistFrontOfficeCustomerSpecialty($params); + } + + public function hookActionObjectCustomerUpdateAfter(array $params): void + { + $this->persistFrontOfficeCustomerSpecialty($params); + } + + public function hookActionGetExtraMailTemplateVars(array $params): void + { + $specialization = $this->loadOrderCustomerSpecialization((int) $params['template_vars']['{id_order}']); + + if (null === $specialization) { + return; + } + + $params['extra_template_vars']['{specialization}'] = $specialization; + } + + private function insertFieldAfterEmail(array &$fields, FormField $field): void + { + $keys = array_keys($fields); + $emailPosition = array_search('email', $keys, true); + $insertAt = false === $emailPosition ? count($fields) : $emailPosition + 1; + + $before = array_slice($fields, 0, $insertAt, true); + $after = array_slice($fields, $insertAt, null, true); + + $fields = $before + ['specialization' => $field] + $after; + } + + private function persistFrontOfficeCustomerSpecialty(array $params): void + { + if (!$this->isFrontEnabled()) { + return; + } + + $customer = $params['object']; + $idCustomer = (int) $customer->id; + $specialization = (int) Tools::getValue('specialization'); + + if ($idCustomer <= 0 || $specialization <= 0) { + return; + } + + $this->get('prestashop.core.command_bus')->handle( + new LinkCustomerSpecialtyCommand($idCustomer, $specialization) + ); + } + + private function loadOrderCustomerSpecialization(int $idOrder): ?string + { + $idCustomer = (int) Db::getInstance()->getValue( + 'SELECT `id_customer` FROM `' . _DB_PREFIX_ . 'orders` WHERE `id_order` = ' . $idOrder + ); + + if (!$idCustomer) { + return null; + } + + $idLang = (int) $this->context->language->id; + + $title = Db::getInstance()->getValue(' + SELECT csl.title + FROM `' . _DB_PREFIX_ . 'ws_customerspecialty_customer` cscu + INNER JOIN `' . _DB_PREFIX_ . 'ws_customerspecialty_lang` csl + ON csl.id_ws_customerspecialty = cscu.id_ws_customerspecialty AND csl.id_lang = ' . $idLang . ' + WHERE cscu.id_customer = ' . $idCustomer . ' + '); + + return false !== $title && null !== $title ? (string) $title : null; + } + + private function isFrontEnabled(): bool + { + return (bool) Configuration::get(Config::FRONT_ENABLED_CONFIG_KEY); + } +} diff --git a/ws_customerspecialty/src/Module/Install.php b/ws_customerspecialty/src/Module/Install.php new file mode 100644 index 0000000..3880b0e --- /dev/null +++ b/ws_customerspecialty/src/Module/Install.php @@ -0,0 +1,124 @@ +createTables()) { + return false; + } + + if (!$this->registerTabs(Config::TABS)) { + return false; + } + + if (!$this->registerHooks()) { + return false; + } + + Configuration::updateValue(Config::FRONT_ENABLED_CONFIG_KEY, false); + + return true; + } + + private function createTables(): bool + { + $sqlToExec = [ + ' + CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'ws_customerspecialty` ( + `id_ws_customerspecialty` INT(11) NOT NULL AUTO_INCREMENT, + `position` INT(10) UNSIGNED NOT NULL DEFAULT 0, + `active` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1, + `date_add` DATETIME NOT NULL, + `date_upd` DATETIME NOT NULL, + PRIMARY KEY (`id_ws_customerspecialty`) + ); + ', + ' + CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'ws_customerspecialty_lang` ( + `id_ws_customerspecialty_lang` INT(11) NOT NULL AUTO_INCREMENT, + `id_ws_customerspecialty` INT(11) NOT NULL, + `id_lang` INT(11) NOT NULL, + `title` VARCHAR(255) NOT NULL, + PRIMARY KEY (`id_ws_customerspecialty_lang`), + UNIQUE KEY `ws_customerspecialty_lang_unique` (`id_ws_customerspecialty`, `id_lang`) + ); + ', + ' + CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'ws_customerspecialty_customer` ( + `id_ws_customerspecialty_customer` INT(11) NOT NULL AUTO_INCREMENT, + `id_customer` INT(11) NOT NULL, + `id_ws_customerspecialty` INT(11) NOT NULL, + `date_add` DATETIME NOT NULL, + `date_upd` DATETIME NOT NULL, + PRIMARY KEY (`id_ws_customerspecialty_customer`), + UNIQUE KEY `ws_customerspecialty_customer_unique` (`id_customer`), + KEY `id_ws_customerspecialty` (`id_ws_customerspecialty`) + ); + ', + ]; + + foreach ($sqlToExec as $sql) { + if (!Db::getInstance()->execute($sql)) { + return false; + } + } + + return true; + } + + private function registerHooks(): bool + { + foreach (Config::HOOKS as $hook) { + if (!$this->registerHook($hook)) { + return false; + } + } + + return true; + } + + private function registerTabs(array $moduleTabs): bool + { + $languages = Language::getLanguages(false); + + foreach ($moduleTabs as $moduleTab) { + if (Tab::getIdFromClassName($moduleTab['class_name'])) { + continue; + } + + $tab = new Tab(); + $tab->class_name = $moduleTab['class_name']; + $tab->module = $moduleTab['module']; + $tab->active = (int) $moduleTab['active']; + $tab->route_name = $moduleTab['route_name']; + $tab->icon = $moduleTab['icon']; + $tab->id_parent = (int) Tab::getIdFromClassName($moduleTab['parent_class_name']); + + foreach ($languages as $language) { + $tab->name[(int) $language['id_lang']] = $moduleTab['name'][$language['iso_code']] + ?? $moduleTab['name']['en']; + } + + $tab->add(); + + if (!$tab->id) { + return false; + } + } + + return true; + } +} diff --git a/ws_customerspecialty/views/templates/admin/configuration.tpl b/ws_customerspecialty/views/templates/admin/configuration.tpl new file mode 100644 index 0000000..9a86d44 --- /dev/null +++ b/ws_customerspecialty/views/templates/admin/configuration.tpl @@ -0,0 +1,29 @@ +{if $confirmation} + {$confirmation nofilter} +{/if} + +
+ {l s='When enabled, a required "Specialty" field is added to the registration and account edit forms on the storefront. Back-office management of specialty values and customer records is unaffected by this setting.' d='Modules.Wscustomerspecialty.Admin'} +
+ +{$specialization|escape:'htmlall':'UTF-8'}
+