diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..48b8bf9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+vendor/
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..9f14da9
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,28 @@
+{
+ "name": "websmid/giftdropdown",
+ "description": "Adds a gift paper style selector to the checkout",
+ "type": "prestashop-module",
+ "authors": [
+ {
+ "name": "Isabelle Oving-Anno - De Websmid BV",
+ "email": "isabelle@dewebsmid.nl"
+ }
+ ],
+ "require": {
+ "php": ">=7.2.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "Websmid\\GiftDropdown\\": "src/"
+ },
+ "classmap": [
+ "ws_giftdropdown.php"
+ ],
+ "exclude-from-classmap": []
+ },
+ "config": {
+ "preferred-install": "dist",
+ "prepend-autoloader": false
+ },
+ "license": "MIT"
+}
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 0000000..e11dbba
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,20 @@
+{
+ "_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": "1e297fbe2fb6a5b7310b2215a227d48e",
+ "packages": [],
+ "packages-dev": [],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": {},
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": {
+ "php": ">=7.2.0"
+ },
+ "platform-dev": {},
+ "plugin-api-version": "2.9.0"
+}
diff --git a/config_nl.xml b/config_nl.xml
new file mode 100644
index 0000000..456a6e8
--- /dev/null
+++ b/config_nl.xml
@@ -0,0 +1,11 @@
+
+
+ ws_giftdropdown
+
+
+
+
+
+ 1
+ 0
+
\ No newline at end of file
diff --git a/controllers/front/savepaper.php b/controllers/front/savepaper.php
new file mode 100644
index 0000000..9c6a33b
--- /dev/null
+++ b/controllers/front/savepaper.php
@@ -0,0 +1,54 @@
+ajax) {
+ http_response_code(400);
+ exit;
+ }
+
+ $choice = Tools::getValue('gift_paper', '');
+
+ $saved = Configuration::get(Config::CONFIG_KEY);
+ $options = $saved ? json_decode($saved, true) : Config::DEFAULT_OPTIONS;
+
+ if (!in_array($choice, $options, true)) {
+ $this->ajaxRender(json_encode(['success' => false, 'error' => 'Invalid option']));
+ exit;
+ }
+
+ $idCart = (int) Context::getContext()->cart->id;
+
+ if (!$idCart) {
+ $this->ajaxRender(json_encode(['success' => false, 'error' => 'No cart']));
+ exit;
+ }
+
+ Db::getInstance()->execute(
+ 'INSERT INTO `' . _DB_PREFIX_ . 'ws_gift_dropdown`
+ (`id_cart`, `id_order`, `paper_choice`, `date_add`)
+ VALUES
+ (' . (int) $idCart . ', 0, \'' . pSQL($choice) . '\', NOW())
+ ON DUPLICATE KEY UPDATE
+ `paper_choice` = \'' . pSQL($choice) . '\',
+ `date_add` = NOW()'
+ );
+
+ $this->ajaxRender(json_encode(['success' => true]));
+ exit;
+ }
+}
diff --git a/index.php b/index.php
new file mode 100644
index 0000000..51fd894
--- /dev/null
+++ b/index.php
@@ -0,0 +1,34 @@
+
+* @copyright PrestaShop SA
+* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
+* International Registered Trademark & Property of PrestaShop SA
+*/
+
+header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
+header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");
+
+header("Cache-Control: no-store, no-cache, must-revalidate");
+header("Cache-Control: post-check=0, pre-check=0", false);
+header("Pragma: no-cache");
+
+header("Location: ../");
+exit;
diff --git a/logo.png b/logo.png
new file mode 100644
index 0000000..53257a4
Binary files /dev/null and b/logo.png differ
diff --git a/src/Config/Config.php b/src/Config/Config.php
new file mode 100644
index 0000000..ae3a462
--- /dev/null
+++ b/src/Config/Config.php
@@ -0,0 +1,27 @@
+displayConfirmation($this->trans('Settings saved.', [], 'Admin.Notifications.Success'));
+ }
+
+ $saved = Configuration::get(Config::CONFIG_KEY);
+ $options = $saved ? json_decode($saved, true) : Config::DEFAULT_OPTIONS;
+
+ $this->context->smarty->assign([
+ 'gift_options' => implode("\n", $options),
+ 'form_action' => $this->context->link->getAdminLink('AdminModules') . '&configure=' . $this->name,
+ 'confirmation' => $confirmation,
+ ]);
+
+ return $this->fetch('module:ws_giftdropdown/views/templates/admin/configuration.tpl');
+ }
+
+ public function hookActionFrontControllerSetMedia()
+ {
+ if (Tools::getValue('module') !== 'thecheckout' && $this->context->controller->php_self !== 'order') {
+ return;
+ }
+
+ $ajaxUrl = $this->context->link->getModuleLink($this->name, 'savepaper', [], true);
+ Media::addJsDef(['wsGiftDropdownAjaxUrl' => $ajaxUrl]);
+
+ $this->context->controller->registerStylesheet(
+ 'ws-giftdropdown-css',
+ 'modules/' . $this->name . '/views/css/giftwrap.css'
+ );
+
+ $this->context->controller->registerJavascript(
+ 'ws-giftdropdown-js',
+ 'modules/' . $this->name . '/views/js/giftwrap.js',
+ ['position' => 'bottom', 'priority' => 200]
+ );
+ }
+
+ public function hookDisplayAdminOrderMainBottom(array $params)
+ {
+ $idOrder = (int) $params['id_order'];
+
+ $row = Db::getInstance()->getRow(
+ 'SELECT `paper_choice` FROM `' . _DB_PREFIX_ . 'ws_gift_dropdown`
+ WHERE `id_order` = ' . (int) $idOrder
+ );
+
+ if (!$row) {
+ return '';
+ }
+
+ $order = new Order($idOrder);
+ if (!(bool) $order->gift) {
+ return '';
+ }
+
+ $this->context->smarty->assign([
+ 'gift_paper_choice' => $row['paper_choice'],
+ ]);
+
+ $this->context->controller->addJS($this->getPathUri() . 'views/js/admin_order.js');
+
+ return $this->fetch('module:ws_giftdropdown/views/templates/admin/order_detail.tpl');
+ }
+}
diff --git a/src/Module/DisplayHooks.php b/src/Module/DisplayHooks.php
new file mode 100644
index 0000000..901fa4a
--- /dev/null
+++ b/src/Module/DisplayHooks.php
@@ -0,0 +1,100 @@
+context->controller->php_self !== 'order') {
+ return;
+ }
+
+ $saved = Configuration::get(Config::CONFIG_KEY);
+ $options = $saved ? json_decode($saved, true) : Config::DEFAULT_OPTIONS;
+
+ $this->context->smarty->assign('gift_paper_options', $options);
+
+ return $this->display(__FILE__, 'views/templates/hook/your_template.tpl');
+ }
+
+ public function hookActionGetExtraMailTemplateVars(array $params): void
+ {
+ $paperChoice = $this->loadGiftPaperChoice((int) $params['template_vars']['{id_order}']);
+ if ($paperChoice === null) {
+ return;
+ }
+
+ $params['extra_template_vars']['{ws_gift_paper_choice}'] = $paperChoice;
+ }
+
+ public function hookActionValidateOrder(array $params)
+ {
+ /** @var Order $order */
+ $order = $params['order'];
+ $idCart = (int) $params['cart']->id;
+
+ if (!(bool) $order->gift) {
+ Db::getInstance()->execute(
+ 'DELETE FROM `' . _DB_PREFIX_ . 'ws_gift_dropdown`
+ WHERE `id_cart` = ' . (int) $idCart . ' AND `id_order` = 0'
+ );
+ return;
+ }
+
+ Db::getInstance()->execute(
+ 'UPDATE `' . _DB_PREFIX_ . 'ws_gift_dropdown`
+ SET `id_order` = ' . (int) $order->id . '
+ WHERE `id_cart` = ' . (int) $idCart
+ );
+
+ $paperChoice = Db::getInstance()->getValue(
+ 'SELECT `paper_choice` FROM `' . _DB_PREFIX_ . 'ws_gift_dropdown`
+ WHERE `id_cart` = ' . (int) $idCart
+ );
+
+ if ($paperChoice) {
+ $note = 'Cadeaupapier: ' . $paperChoice;
+ $order->note = $order->note ? $order->note . "\n" . $note : $note;
+ $order->update();
+ }
+ }
+
+ public function hookDisplayPDFDeliverySlip(array $params)
+ {
+ return $this->hookDisplayPDFInvoice($params);
+ }
+
+ public function hookDisplayPDFInvoice(array $params)
+ {
+ /** @var OrderInvoice $orderInvoice */
+ $orderInvoice = $params['object'];
+ $idOrder = (int) $orderInvoice->id_order;
+
+ $paperChoice = Db::getInstance()->getValue(
+ 'SELECT `paper_choice` FROM `' . _DB_PREFIX_ . 'ws_gift_dropdown`
+ WHERE `id_order` = ' . (int) $idOrder
+ );
+
+ $params['smarty']->assign('gift_paper_choice', $paperChoice ?: '');
+
+ return '';
+ }
+
+ private function loadGiftPaperChoice(int $idOrder): ?string
+ {
+ $value = Db::getInstance()->getValue(
+ 'SELECT `paper_choice` FROM `' . _DB_PREFIX_ . 'ws_gift_dropdown`
+ WHERE `id_order` = ' . $idOrder
+ );
+
+ return $value !== false ? (string) $value : null;
+ }
+}
diff --git a/src/Module/Install.php b/src/Module/Install.php
new file mode 100644
index 0000000..d445dab
--- /dev/null
+++ b/src/Module/Install.php
@@ -0,0 +1,82 @@
+checkDependency()) {
+ return false;
+ }
+
+ if (!$this->createTables()) {
+ return false;
+ }
+
+ Configuration::updateValue(Config::CONFIG_KEY, json_encode(Config::DEFAULT_OPTIONS));
+
+ if (!$this->registerHooks()) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private function checkDependency(): bool
+ {
+ $isInstalled = (bool) Db::getInstance()->getValue(
+ 'SELECT `active` FROM `' . _DB_PREFIX_ . 'module` WHERE `name` = "thecheckout"'
+ );
+
+ if (!$isInstalled) {
+ $this->_errors[] = $this->trans(
+ 'This module requires TheCheckout to be installed.',
+ [],
+ 'Modules.Ws_GiftDropdown.Admin'
+ );
+
+ return false;
+ }
+
+ return true;
+ }
+
+ private function createTables()
+ {
+ $sql = '
+ CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'ws_gift_dropdown` (
+ `id_ws_gift_dropdown` INT(11) NOT NULL AUTO_INCREMENT,
+ `id_cart` INT(11) NOT NULL DEFAULT 0,
+ `id_order` INT(11) NOT NULL DEFAULT 0,
+ `paper_choice` VARCHAR(64) NOT NULL,
+ `date_add` DATETIME NOT NULL,
+ PRIMARY KEY (`id_ws_gift_dropdown`),
+ UNIQUE KEY `id_cart` (`id_cart`),
+ KEY `id_order` (`id_order`)
+ );
+ ';
+
+ return Db::getInstance()->execute($sql);
+ }
+
+ public function registerHooks()
+ {
+ foreach (Config::HOOKS as $hook) {
+ if (!$this->registerHook($hook)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/views/css/giftwrap.css b/views/css/giftwrap.css
new file mode 100644
index 0000000..b8fbdc6
--- /dev/null
+++ b/views/css/giftwrap.css
@@ -0,0 +1,32 @@
+#gift-paper-choice {
+ margin-top: 12px;
+ padding: 12px 16px;
+ background: #f9f9f9;
+ border: 1px solid #e0e0e0;
+ border-radius: 4px;
+}
+
+#gift-paper-choice legend,
+#gift-paper-choice .gift-paper-label {
+ font-weight: 600;
+ margin-bottom: 8px;
+ display: block;
+}
+
+#gift-paper-choice .gift-paper-options {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+#gift-paper-choice .gift-paper-option {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ cursor: pointer;
+}
+
+#gift-paper-choice .gift-paper-option input[type="radio"] {
+ margin: 0;
+ cursor: pointer;
+}
diff --git a/views/js/admin_order.js b/views/js/admin_order.js
new file mode 100644
index 0000000..046ad90
--- /dev/null
+++ b/views/js/admin_order.js
@@ -0,0 +1,6 @@
+document.addEventListener("DOMContentLoaded", function () {
+ var panel = document.getElementById("ws-gift-panel");
+ var badge = document.querySelector('[data-badge="gift"]');
+ if (!panel || !badge) return;
+ badge.textContent = "Cadeauverpakking - " + panel.dataset.paperChoice;
+});
diff --git a/views/js/giftwrap.js b/views/js/giftwrap.js
new file mode 100644
index 0000000..f7f7f7a
--- /dev/null
+++ b/views/js/giftwrap.js
@@ -0,0 +1,33 @@
+(function () {
+ 'use strict';
+
+ function savePaperChoice(choice) {
+ var ajaxUrl = window.wsGiftDropdownAjaxUrl || '';
+ if (!ajaxUrl || !choice) {
+ return;
+ }
+ var xhr = new XMLHttpRequest();
+ xhr.open('POST', ajaxUrl, true);
+ xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
+ xhr.send('gift_paper=' + encodeURIComponent(choice) + '&ajax=1');
+ }
+
+ function init() {
+ var checked = document.querySelector('input[name="gift_paper"]:checked');
+ if (checked) {
+ savePaperChoice(checked.value);
+ }
+
+ document.addEventListener('change', function (e) {
+ if (e.target && e.target.name === 'gift_paper') {
+ savePaperChoice(e.target.value);
+ }
+ });
+ }
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ } else {
+ init();
+ }
+}());
diff --git a/views/templates/admin/configuration.tpl b/views/templates/admin/configuration.tpl
new file mode 100644
index 0000000..15f424d
--- /dev/null
+++ b/views/templates/admin/configuration.tpl
@@ -0,0 +1,35 @@
+{if $confirmation}
+ {$confirmation nofilter}
+{/if}
+
+
+
+
+ {l s='Gift Paper Options' mod='ws_giftdropdown'}
+
+
+
+ {l s='Enter one paper style per line. These options will appear as radio buttons in the checkout when the customer selects gift wrapping.' mod='ws_giftdropdown'}
+
+
+
+
diff --git a/views/templates/admin/order_detail.tpl b/views/templates/admin/order_detail.tpl
new file mode 100644
index 0000000..ccbabf6
--- /dev/null
+++ b/views/templates/admin/order_detail.tpl
@@ -0,0 +1,12 @@
+
+
+
+ {l s='Gift wrapping' mod='ws_giftdropdown'}
+
+
+
+ - {l s='Paper style' mod='ws_giftdropdown'}
+ - {$gift_paper_choice|escape:'htmlall':'UTF-8'}
+
+
+
diff --git a/ws_giftdropdown.php b/ws_giftdropdown.php
new file mode 100644
index 0000000..710052b
--- /dev/null
+++ b/ws_giftdropdown.php
@@ -0,0 +1,55 @@
+
+ * @copyright 2007-2025 De Websmid BV
+ * @license Valid for 1 website (or project) for each purchase of license
+ * International Registered Trademark & Property of De Websmid BV
+ */
+
+use Websmid\GiftDropdown\Module\Install;
+use Websmid\GiftDropdown\Module\AdminHooks;
+use Websmid\GiftDropdown\Module\DisplayHooks;
+
+if (!defined('_PS_VERSION_')) {
+ exit;
+}
+
+if (file_exists(__DIR__ . '/vendor/autoload.php')) {
+ require_once __DIR__ . '/vendor/autoload.php';
+}
+
+class Ws_GiftDropdown extends Module
+{
+ use Install;
+ use AdminHooks;
+ use DisplayHooks;
+
+ public function __construct()
+ {
+ $this->name = 'ws_giftdropdown';
+ $this->version = '1.0.0';
+ $this->author = 'Isabelle Oving-Anno | De Websmid BV';
+ $this->need_instance = 0;
+ $this->bootstrap = true;
+
+ parent::__construct();
+
+ $this->displayName = $this->trans('Gift Paper Selector', [], 'Modules.Ws_GiftDropdown.Admin');
+ $this->description = $this->trans('Adds a paper style selector to the gift wrap option at checkout', [], 'Modules.Ws_GiftDropdown.Description');
+ $this->ps_versions_compliancy = ['min' => '1.7.0.0', 'max' => _PS_VERSION_];
+ }
+}