Init project

Future fixes:
-Remove logic for showing current order reference in ajax.js. Instead grab from database using cart ID
This commit is contained in:
2026-02-09 10:08:02 +01:00
committed by Isabelle
parent 65e21d48c3
commit e5fe85c8e5
21 changed files with 702 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
vendor/

29
composer.json Normal file
View File

@@ -0,0 +1,29 @@
{
"name": "dewebsmid/ws_orderreference",
"description": "Adds an optional order reference field to the shopping cart summary.",
"authors": [
{
"name": "Isabelle Oving-Anno",
"email": "isabelle@dewebsmid.nl"
}
],
"require": {
"php": ">=8.2.0"
},
"autoload": {
"psr-4": {
"Module\\WsOrderreference\\": "src/"
},
"classmap": [
"ws_orderreference.php"
],
"exclude-from-classmap": []
},
"config": {
"preferred-install": "dist",
"prepend-autoloader": false
},
"type": "prestashop-module",
"author": "Isabelle Oving-Anno",
"license": "AFL-3.0"
}

20
composer.lock generated Normal file
View File

@@ -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": "14a8801fe935c090041cfc25afe6829f",
"packages": [],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=8.2.0"
},
"platform-dev": {},
"plugin-api-version": "2.6.0"
}

18
config/services.yml Normal file
View File

@@ -0,0 +1,18 @@
services:
_defaults:
autowire: true
autoconfigure: true
# Repository - this makes it available via dependency injection
Module\WsOrderreference\Repository\OrderReferenceRepository:
public: true
factory: ["@doctrine.orm.default_entity_manager", getRepository]
arguments:
- Module\WsOrderreference\Entity\OrderReference
# Database installer
Module\WsOrderreference\Database\ReferenceInstaller:
public: true
arguments:
- "@doctrine.dbal.default_connection"
- "%database_prefix%"

11
config_nl.xml Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" ?>
<module>
<name>ws_orderreference</name>
<displayName><![CDATA[Orderreferentie]]></displayName>
<version><![CDATA[1.0.0]]></version>
<description><![CDATA[Voegt een optioneel orderreferentieveld toe aan het winkelwagenoverzicht.]]></description>
<author><![CDATA[Isabelle Oving-Anno | De Websmid b.v.]]></author>
<tab><![CDATA[other]]></tab>
<is_configurable>0</is_configurable>
<need_instance>0</need_instance>
</module>

View File

@@ -0,0 +1,63 @@
<?php
use Module\WsOrderreference\Entity\OrderReference;
use Module\WsOrderreference\Repository\OrderReferenceRepository;
if (file_exists(__DIR__ . '../../vendor/autoload.php')) {
require_once __DIR__ . '../../vendor/autoload.php';
}
class ws_orderreferenceAjaxModuleFrontController extends ModuleFrontController
{
public function initContent()
{
$orderReference = Tools::getValue('order_reference_input');
$idCart = $this->context->cart->id;
if (!$idCart) {
$this->ajaxRender(json_encode([
'success' => false,
'message' => $this->module->l('No cart found.', 'ajax')
]));
return;
}
if (!preg_match('/^[a-zA-Z0-9_\- ]+$/', $orderReference)) {
$this->ajaxRender(json_encode([
'success' => false,
'message' => $this->module->l('Please enter a valid order reference. Characters allowed: a-z, 0-9, -, _ and space', 'ajax')
]));
return;
}
try {
/** @var \Doctrine\ORM\EntityManagerInterface $entityManager */
$entityManager = $this->get('doctrine.orm.default_entity_manager');
/** @var OrderReferenceRepository $repository */
$repository = $entityManager->getRepository(OrderReference::class);
$orderRef = $repository->findByCartId($idCart);
if (!$orderRef) {
$orderRef = new OrderReference();
$orderRef->setIdCart($idCart);
}
$orderRef->setOrderReference($orderReference);
$entityManager->persist($orderRef);
$entityManager->flush();
$this->ajaxRender(json_encode([
'success' => true,
'message' => $this->module->l('Order reference received!', 'ajax')
]));
} catch (Exception $e) {
$this->ajaxRender(json_encode([
'success' => false,
'message' => $this->module->l('Could not save order reference.', 'ajax')
]));
}
}
}

35
index.php Normal file
View File

@@ -0,0 +1,35 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to https://devdocs.prestashop.com/ for more information.
*
* @author PrestaShop SA and Contributors <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
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;

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,82 @@
<?php
/**
* 2026 De Websmid
*
* @author De Websmid <contact@De Websmid.com>
* @copyright 2026 De Websmid
* @license De Websmid
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0).
* It is also available through the world-wide-web at this URL: https://opensource.org/licenses/AFL-3.0
*/
declare(strict_types=1);
namespace Module\WsOrderreference\Database;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception as DBALException;
class ReferenceInstaller
{
private Connection $connection;
private string $dbPrefix;
public function __construct(Connection $connection, string $dbPrefix)
{
$this->connection = $connection;
$this->dbPrefix = $dbPrefix;
}
public function createTables(): array
{
$errors = [];
$this->dropTables();
$sqlInstallFile = dirname(__DIR__) . '/Resources/data/install.sql';
$sqlContent = file_get_contents($sqlInstallFile);
$sqlContent = str_replace('PREFIX_', $this->dbPrefix, $sqlContent);
$sqlQueries = array_filter(array_map('trim', explode(';', $sqlContent)));
foreach ($sqlQueries as $query) {
if (empty($query)) {
continue;
}
try {
$this->connection->executeQuery($query);
} catch (DBALException $e) {
$errors[] = [
'key' => $e->getMessage(),
'parameters' => [],
'domain' => 'Admin.Modules.Notification',
];
}
}
return $errors;
}
public function dropTables(): array
{
$errors = [];
$tableNames = [
'ws_orderreference',
];
foreach ($tableNames as $tableName) {
$sql = 'DROP TABLE IF EXISTS ' . $this->dbPrefix . $tableName;
try {
$this->connection->executeQuery($sql);
} catch (DBALException $e) {
$errors[] = [
'key' => $e->getMessage(),
'parameters' => [],
'domain' => 'Admin.Modules.Notification',
];
}
}
return $errors;
}
}

View File

@@ -0,0 +1,91 @@
<?php
/**
* 2026 De Websmid
*
* @author De Websmid <contact@De Websmid.com>
* @copyright 2026 De Websmid
* @license Academic Free License 3.0 (AFL-3.0)
*/
declare(strict_types=1);
namespace Module\WsOrderreference\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table()
* @ORM\Entity(repositoryClass="Module\WsOrderreference\Repository\OrderReferenceRepository")
*/
class OrderReference
{
/**
* @var int
*
* @ORM\Id
* @ORM\Column(name="id", type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @var int
*
* @ORM\Column(name="id_cart", type="integer", unique=true)
*/
private $idCart;
/**
* @var string
*
* @ORM\Column(name="order_reference", type="string", length=255)
*/
private $orderReference;
/**
* @return int|null
*/
public function getId(): ?int
{
return $this->id;
}
/**
* @return int|null
*/
public function getIdCart(): ?int
{
return $this->idCart;
}
/**
* @param int $idCart
* @return $this
*/
public function setIdCart(int $idCart): self
{
$this->idCart = $idCart;
return $this;
}
/**
* @return string
*/
public function getOrderReference(): string
{
return $this->orderReference;
}
/**
* @param string $orderReference
* @return $this
*/
public function setOrderReference(string $orderReference): self
{
$this->orderReference = $orderReference;
return $this;
}
}

45
src/Module/Install.php Normal file
View File

@@ -0,0 +1,45 @@
<?php
namespace Module\WsOrderreference\Module;
use Module\WsOrderreference\Database\ReferenceInstaller;
trait Install
{
public function install()
{
return $this->installTables()
&& parent::install()
&& $this->registerHook('displayShoppingCart')
&& $this->registerHook('displayPDFDeliverySlip')
&& $this->registerHook('actionFrontControllerSetMedia');
}
private function getInstaller(): ReferenceInstaller
{
try {
$installer = $this->get(ReferenceInstaller::class);
} catch (\Throwable $e) {
$installer = null;
}
if (!$installer) {
$installer = new ReferenceInstaller(
$this->get('doctrine.dbal.default_connection'),
$this->getContainer()->getParameter('database_prefix')
);
}
return $installer;
}
private function installTables(): bool
{
/** @var ReferenceInstaller $installer */
$installer = $this->getInstaller();
$errors = $installer->createTables();
return empty($errors);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Module\WsOrderreference\Module;
trait actionHooks
{
public function hookActionFrontControllerSetMedia($params)
{
\Media::addJsDef([
'ajax_url' => $this->context->link->getModuleLink($this->name, 'ajax'),
]);
$this->context->controller->registerJavascript(
'index.js',
'modules/'.$this->name.'/views/js/index.js',
['position' => 'bottom', 'priority' => 150]
);
$this->context->controller->registerStylesheet(
'styles.css',
'modules/'.$this->name.'/views/css/styles.css',
['media' => 'all', 'priority' => 150]
);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Module\WsOrderreference\Module;
trait displayHooks
{
public function hookDisplayShoppingCart($params)
{
return $this->fetch('module:ws_orderreference/views/templates/front/orderreference.tpl');
}
public function hookDisplayPDFDeliverySlip($params)
{
$orderId = $params['object']->id_order;
$orderReferenceRepo = \PrestaShop\PrestaShop\Adapter\SymfonyContainer::getInstance()->get('Module\WsOrderreference\Repository\OrderReferenceRepository');
$orderReference = $orderReferenceRepo->customRefByOrderId($orderId);
$this->context->smarty->assign('orderReference', $orderReference);
return $this->fetch('module:ws_orderreference/views/templates/pdf/delivery-slip.tpl');
}
}

View File

@@ -0,0 +1,53 @@
<?php
/**
* 2026 De Websmid
*
* @author De Websmid <contact@De Websmid.com>
* @copyright 2026 De Websmid
* @license Academic Free License 3.0 (AFL-3.0)
*/
declare(strict_types=1);
namespace Module\WsOrderreference\Repository;
use Doctrine\ORM\EntityRepository;
use Module\WsOrderreference\Entity\OrderReference;
class OrderReferenceRepository extends EntityRepository
{
/**
* Find order reference by cart ID
*
* @param int $cartId
* @return OrderReference|null
*/
public function findByCartId(int $cartId): ?OrderReference
{
return $this->findOneBy(['idCart' => $cartId]);
}
// Create a function that accepts an order Id and uses the cart ID to check if there is a custom order reference.
// Requires an order ID as a parameter.
// Returns the order reference if found, otherwise returns null.
// Used in the display hook to show the order reference on the pdf.
// Steps to build and break the problem down:
// 1.) Just return the cart ID from the order ID to see if that works.
// 2.) Use the cart ID to find the order reference.
public function customRefByOrderId(int $orderId): ?string
{
$conn = $this->getEntityManager()->getConnection();
$sql = 'SELECT id_cart FROM ps_orders WHERE id_order = :orderId';
$stmt = $conn->prepare($sql);
$resultSet = $stmt->executeQuery(['orderId' => $orderId]);
$idCart = $resultSet->fetchOne();
if ($idCart) {
$orderReference = $this->findByCartId((int)$idCart);
return $orderReference ? $orderReference->getOrderReference() : null;
}
return null;
}
}

View File

@@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS `PREFIX_order_reference` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`id_cart` int(11) NOT NULL,
`order_reference` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `id_cart` (`id_cart`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

14
translations/nl.php Normal file
View File

@@ -0,0 +1,14 @@
<?php
global $_MODULE;
$_MODULE = array();
$_MODULE['<{ws_orderreference}prestashop>ws_orderreference_1e860e56970a81a1ba3e1fcb7fccc846'] = 'Orderreferentie';
$_MODULE['<{ws_orderreference}prestashop>ws_orderreference_9f0d684399938af879ce3fb38710642c'] = 'Voegt een optioneel orderreferentieveld toe aan het winkelwagenoverzicht.';
$_MODULE['<{ws_orderreference}prestashop>ajax_ec19148e5460115c7abda82799f02269'] = 'Geen winkelwagen gevonden.';
$_MODULE['<{ws_orderreference}prestashop>ajax_ffc8410c88b32eb4547230088b9df62a'] = 'Voer een geldige orderreferentie in. Toegestane tekens: a-z, 0-9, -, _ en spatie';
$_MODULE['<{ws_orderreference}prestashop>ajax_588d7a076adcf5a584c049d954496ae6'] = 'Orderreferentie ontvangen!';
$_MODULE['<{ws_orderreference}prestashop>ajax_4602466a12672337138c0925bd5f737f'] = 'Kon orderreferentie niet opslaan.';
$_MODULE['<{ws_orderreference}prestashop>orderreference_9f5ba9ae96aa82530d03f434f4da0670'] = 'Orderreferentie (optioneel)';
$_MODULE['<{ws_orderreference}prestashop>orderreference_a4d3b161ce1309df1c4e25df28694b7b'] = 'Opslaan';
$_MODULE['<{ws_orderreference}prestashop>orderreference_fe8cc190e37aa5083375088bd5873c29'] = 'Uw bestelreferentie:';
$_MODULE['<{ws_orderreference}prestashop>delivery-slip_f8c9e6a6b5ac4fd353d569c78cbabd90'] = 'Uw aangepaste bestelreferentie:';

13
views/css/styles.css Normal file
View File

@@ -0,0 +1,13 @@
/* Fade out for alert */
.fade-out {
animation: fadeOut 0.75s forwards;
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}

105
views/js/index.js Normal file
View File

@@ -0,0 +1,105 @@
document.addEventListener("DOMContentLoaded", function () {
// Load saved reference on page load (with retry for dynamic content)
loadSavedReferenceWithRetry();
// Ajax submission
document.addEventListener("submit", function (e) {
if (e.target && e.target.id === "order_reference_form") {
e.preventDefault();
const form = e.target;
const url = typeof ajax_url !== "undefined" ? ajax_url : null;
const input = document.querySelector("#order_reference_input");
const inputValue = input.value.trim();
fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body:
"ajax=1&order_reference_input=" +
encodeURIComponent(inputValue),
})
.then((response) => response.json())
.then((data) => {
if (data.success) {
// Save to localStorage
localStorage.setItem("order_reference", inputValue);
displaySavedReference(inputValue);
input.value = "";
}
alertFadeOut(form, data);
})
.catch((error) => {
console.error(error);
alertFadeOut(form, {
success: false,
message: "An error occurred",
});
});
}
});
// Load saved reference with retry mechanism for dynamic content
function loadSavedReferenceWithRetry(attempts = 0, maxAttempts = 10) {
const savedReference = localStorage.getItem("order_reference");
if (savedReference) {
const success = displaySavedReference(savedReference);
if (!success && attempts < maxAttempts) {
setTimeout(() => {
loadSavedReferenceWithRetry(attempts + 1, maxAttempts);
}, 100);
}
}
}
function displaySavedReference(value) {
const form = document.querySelector("#order_reference_form");
if (!form) return false;
const lastSubmittedContainer = form.querySelector(
".last-submitted-container",
);
if (lastSubmittedContainer) {
const lastSubmittedSpan =
lastSubmittedContainer.querySelector(".last-submitted");
if (lastSubmittedSpan) {
lastSubmittedSpan.textContent = value;
lastSubmittedContainer.classList.remove("d-none");
return true;
}
}
return false;
}
// Reset alert to initial state
function resetAlert(alert, messageEl) {
alert.classList.add("d-none");
alert.classList.remove("alert-success", "alert-danger", "fade-out");
messageEl.textContent = "";
}
// Fade out alert and reset
function alertFadeOut(form, data) {
const alert = form.querySelector(".alert");
if (!alert) return;
const messageEl = alert.querySelector(".alert-message");
if (!messageEl) return;
// Show alert with appropriate class and message
alert.classList.remove("d-none");
alert.classList.add(data.success ? "alert-success" : "alert-danger");
messageEl.textContent = data.message;
// Fade out after delay
setTimeout(() => {
alert.classList.add("fade-out");
setTimeout(() => {
resetAlert(alert, messageEl);
}, 750); // 750ms for fade-out animation - make sure it matches in styles.css
}, 5000);
}
});

View File

@@ -0,0 +1,21 @@
<div class="col-lg-12 col-md-12 box card p-0" id="ref_order" style="border: 0px;">
<form action="#" class="p-0" method="post" id="order_reference_form">
<fieldset>
<p class="h4">{l s='Order reference (optional)' mod='ws_orderreference'}</p>
<div class="d-flex flex-nowrap mb-2" style="gap: 10px;">
<div class="form-group w-100 mb-0">
<label for="order_reference_input" class="sr-only">{l s='Order reference (optional)' mod='ws_orderreference'}</label>
<input type="text" class="form-control w-100 mb-0" id="order_reference_input" name="order_reference_input" value="{$order_reference_value|escape:'html':'UTF-8'}" required="required">
</div>
<button type="submit" class="btn btn-primary" id="order_reference_submit">{l s='Submit' mod='ws_orderreference'}</button>
</div>
<div class="d-none last-submitted-container mb-2">
<span class="text-danger">{l s='Your order reference:' mod='ws_orderreference'}</span>
<span class="last-submitted text-danger"></span>
</div>
<div id="order_ref_message" class="alert d-none">
<p class="alert-message mb-0"></p>
</div>
</fieldset>
</form>
</div>

View File

@@ -0,0 +1 @@
<h3>{l s='Your custom order reference:' mod='ws_orderreference'} {$orderReference}</h3>

44
ws_orderreference.php Normal file
View File

@@ -0,0 +1,44 @@
<?php
if (!defined('_PS_VERSION_')) {
exit;
}
use Module\WsOrderreference\Module\Install;
use Module\WsOrderreference\Module\displayHooks;
use Module\WsOrderreference\Module\actionHooks;
if (file_exists(__DIR__ . '/vendor/autoload.php')) {
require_once __DIR__ . '/vendor/autoload.php';
}
class Ws_OrderReference extends Module
{
/** @var OrderReferenceRepository */
protected $orderReferenceRepository;
use Install;
use displayHooks;
use actionHooks;
public function __construct()
{
$this->name = 'ws_orderreference';
$this->tab = 'other';
$this->version = '1.0.0';
$this->author = 'Isabelle Oving-Anno | De Websmid b.v.';
$this->need_instance = 0;
$this->ps_versions_compliancy = [
'min' => '9.0.0',
'max' => '9.99.99',
];
$this -> bootstrap = true;
parent::__construct();
$this->displayName = $this->l('Order Reference');
$this->description = $this->l('Adds an optional order reference field to the shopping cart summary.');
}
}