Add wash certificate generation feature and refine booking logic

Introduces a form for generating wash certificates associated with bookings, along with validations and access restrictions. Refactors booking management to integrate a new `bookings_new_o` class, enhancing structure and adding functionalities like caching and detailed error handling.
This commit is contained in:
Jepp9350
2025-03-25 18:40:42 +01:00
parent 78928a6c1b
commit 54940e7cd7
10 changed files with 558 additions and 7 deletions
+8
View File
@@ -12,11 +12,13 @@ use Exception;
use forms\book_interior_wash_f;
use forms\book_wash_f;
use forms\form_helper_c;
use forms\generate_booking_wash_certificate_f;
use traits\form_t;
/** Require the forms */
require_once WD . '/modules/forms/book_wash_f.php';
require_once WD . '/modules/forms/book_interior_wash_f.php';
require_once WD . '/modules/forms/generate_booking_wash_certificate_f.php';
class form
{
@@ -30,11 +32,17 @@ class form
* @var book_interior_wash_f $book_interior_wash The BOOK_INTERIOR_WASH form
*/
public book_interior_wash_f $book_interior_wash;
/**
* The GENERATE_BOOKING_CERTIFICATE form
* @var generate_booking_wash_certificate_f $generate_booking_wash_certificate The GENERATE_BOOKING_CERTIFICATE form
*/
public generate_booking_wash_certificate_f $generate_booking_wash_certificate;
public function __construct()
{
$this->book_wash = new book_wash_f();
$this->book_interior_wash = new book_interior_wash_f();
$this->generate_booking_wash_certificate = new generate_booking_wash_certificate_f();
}
/**
@@ -0,0 +1,50 @@
<?php
namespace email\templates;
use email\helpers\email_template;
class email_template_wash_certificate
{
use email_template;
protected int $booking_id;
protected string $company_name;
public function __construct(
int $booking_id,
string $company_name,
)
{
$this->booking_id = $booking_id;
$this->company_name = $company_name;
}
public function generate_text(): string
{
return "";
}
public function generate_html(): string
{
ob_start();
# Start of the html
?>
<!-- Email template -->
<p>Kære <?= $this->company_name ?>,</p>
<br>
<p>Din ordre er udført.</p>
<p>Vi har vedhæftet dit vaskecertifikat til denne email.</p>
<br>
<p>Tak fordi du valgte Truck Wash vi sætter stor pris på dit samarbejde.</p>
<p>Har du behov for yderligere assistance, er vi kun en e-mail eller et opkald væk.</p>
<p>De bedste hilsner - Kind regards</p>
<p>Truck Wash</p>
<!-- End of the email template -->
<?php
# End of the html
return ob_get_clean();
}
}
@@ -3,6 +3,7 @@
namespace forms;
use classes\email;
use objects\bookings_new_o;
use objects\departments_o;
use objects\users_o;
use traits\form_t;
@@ -53,9 +54,28 @@ class book_wash_f extends form_helper_c
$user = (new users_o())->getUserByCustomerNumber(self::getSanitizedData('customer_number'));
// Get the department name from the department id
$department = (new departments_o())->selectId(self::getSanitizedData('department_id'));
// Get the bookings_new object
$bookings_new = new bookings_new_o();
// Create a new booking
$bookings_new->add(
self::getCustomerNumber(),
self::getFormIdentifier(),
self::getSanitizedData('contact_email'),
self::getSanitizedData('reference'),
strtoupper(self::getSanitizedData('registration_number_tractor')),
strtoupper(self::getSanitizedData('registration_number_trailer')),
self::getSanitizedData('wants_wash_certificate') ? self::getSanitizedData('wants_wash_certificate_email') : '',
self::getSanitizedData('date'),
$department->id,
(bool)self::getSanitizedData('wants_pickup'),
self::getSanitizedData('notes'),
'pending',
'',
'pending'
);
// Send the booking confirmation email
$email->sendBookingConfirmationEmail(
123,
$bookings_new->id,
$user->getCustomerName($user->customer_number->value()),
self::getCustomerNumber(),
self::getSanitizedData('contact_email'),
@@ -2,6 +2,8 @@
namespace forms;
abstract class form_helper_c
require_once __DIR__ . '/form_restrictions_helper_c.php';
abstract class form_helper_c extends form_restrictions_helper_c
{
}
@@ -0,0 +1,62 @@
<?php
namespace forms;
use classes\authentication;
use Exception;
use objects\users_o;
abstract class form_restrictions_helper_c
{
protected users_o $users_o;
/**
* @throws Exception
*/
public function __construct()
{
// Initialize the users_o object
$tmp_users_o = (new authentication())->get_user();
if (!!$tmp_users_o) {
$this->users_o = $tmp_users_o;
}
}
/**
* Restrict access, if the user does not have access to the department
* @throws Exception If the user does not have access to the department
*/
public function restrictAccessDepartment($department_id): void
{
self::requirePermission(
'department_access_' . $department_id,
'User does not have access to the department',
);
}
/**
* Require permission to continue
* @param string $permission The permission to check
* @param string $message The error message to throw, if the user does not have permission
* @throws Exception If the user does not have permission
*/
public function requirePermission(string $permission, string $message): void
{
self::requireAuth();
// Check if the user has permission
if (!$this->users_o->hasPermission($permission)) {
throw new Exception($message);
}
}
/**
* @throws Exception
*/
protected function requireAuth(): void
{
// Check if the user is authenticated
if (!isset($this->users_o)) {
throw new Exception('User not authenticated');
}
}
}
@@ -0,0 +1,124 @@
<?php
namespace forms;
use objects\bookings_new_o;
use traits\form_t;
class generate_booking_wash_certificate_f extends form_helper_c
{
use form_t;
/**
* @inheritDoc
*/
public function sanitize(): void
{
foreach ( $this->form_unsanitized_data as $key => $value ) {
// Sanitize the input TODO: Implement the sanitization logic
$this->form_unsanitized_data[$key] = $value;
}
// Set the sanitized data
$this->form_sanitized_data = $this->form_unsanitized_data;
}
/**
* @inheritDoc
*/
public function validateInput(): void
{
}
/**
* @inheritDoc
*/
public function beforeSave(): void
{
// Get the booking from the booking id
$bookings_new = new bookings_new_o();
$bookings_new->select(self::getSanitizedData('booking_id'));
// Check if the user has access to the booking
self::restrictAccessDepartment($bookings_new->department->value());
// Check if the user has access to issue wash certificates
self::requirePermission(
'issue_wash_certificates',
'User does not have access to issue wash certificates',
);
// Set the department id to the department id of the user
self::setDepartmentId($bookings_new->department->value());
// Set the customer number to the customer number of the user
self::setCustomerNumber($bookings_new->customer_number->value());
// Check if the booking is cancelled
switch ($bookings_new->status->value()) {
case 'cancelled':
throw new \Exception('Booking is cancelled');
break;
case 'completed':
throw new \Exception('Booking is completed');
break;
case 'pending':
break;
default:
throw new \Exception('Booking status is unknown, expected cancelled, completed or pending');
break;
}
// Generate the wash certificate
$bookings_new->generateWashCertificate(
self::getSanitizedData('safety_seal'),
self::getSanitizedData('operator'),
);
}
/**
* @inheritDoc
*/
public function afterSubmit(): void
{
// TODO: Implement afterSubmit() method.
}
/**
* @inheritDoc
*/
public function setup(): void
{
self::setFormIdentifier('GENERATE_BOOKING_WASH_CERTIFICATE');
self::setFormName('Opret vaskecertifikat');
self::setFormDescription('Du kan oprette et vaskecertifikat til en vask, der allerede er booket. Du skal blot indtaste de nødvendige oplysninger nedenfor.');
self::setSubmitButtonText('Opret vaskecertifikat');
// Set the form fields
self::defineInputFieldsAdvanced([
'booking_id' => [
'description' => 'Booking ID er det unikke ID for den booking, du vil oprette et vaskecertifikat til.',
'required' => true,
'placeholder' => 'Indtast booking ID',
'label' => 'Booking ID',
'help' => 'Dette er det unikke ID for den booking, du vil oprette et vaskecertifikat til.',
'error' => 'Du skal indtaste et gyldigt booking ID.',
'validation_method' => 'validateBookingId',
],
'safety_seal' => [
'description' => 'En sikkerhedssikring er en form for beskyttelse, der sikrer, at køretøjet ikke er blevet åbnet eller ændret efter vasken.',
'required' => false,
'placeholder' => 'Indtast sikkerhedssikring',
'label' => 'Sikkerhedssikring',
'help' => 'Dette er en sikkerhedssikring, der bruges til at sikre køretøjet.',
'error' => 'Du skal indtaste en gyldig sikkerhedssikring.',
'validation_method' => 'validateInt',
'default' => '',
],
'operator' => [
'description' => 'En operatør er en person, der arbejder med køretøjet.',
'required' => false,
'placeholder' => 'Indtast operatør',
'label' => 'Operatør',
'help' => 'Dette er en operatør, der arbejder med køretøjet.',
'error' => 'Du skal indtaste en gyldig operatør.',
'validation_method' => 'validateString',
'default' => '',
],
]);
}
}
@@ -0,0 +1,212 @@
<?php
namespace objects;
use classes\db;
use classes\email;
use classes\object_property;
use classes\pdf_generator;
use Exception;
use traits\db_object_t;
class bookings_new_o extends db
{
use db_object_t;
public object_property $customer_number;
public object_property $wash_type;
public object_property $contact_email;
public object_property $reference_number;
public object_property $regNrTraekker;
public object_property $regNrTrailer;
public object_property $washCertificateEmail;
public object_property $date;
public object_property $department;
public object_property $pickup_bool;
public object_property $notes;
public object_property $washCertificateStatus;
public object_property $washCertificateUrl;
public object_property $status;
private static function isCancelled(int $id): bool
{
global $db;
// Check if the booking has been cancelled
$sql = "SELECT status FROM bookings WHERE id = $id";
$result = $db->query($sql);
$row = $db->fetch_assoc($result);
return $row['status'] === 'cancelled';
}
public function structure(): void
{
$this->setTable('bookings_new');
}
public function objectChanged(): void
{
// Clear the cache
redis->clear_department_booking_count($this->department->value());
}
public function delete(int $id): void
{
$this->id = $id;
$this->getObjectProperties();
// Set the status to cancelled
$this->status->set('cancelled');
// Remove the cache
redis->clear_department_booking_count($this->department->value());
}
public function getObjectProperties(): void
{
$this->customer_number = new object_property($this->table, $this->id, 'customer_number', 'int', true);
$this->wash_type = new object_property($this->table, $this->id, 'wash_type', 'string', true);
$this->contact_email = new object_property($this->table, $this->id, 'contact_email', 'string', true);
$this->reference_number = new object_property($this->table, $this->id, 'reference_number', 'string', true);
$this->regNrTraekker = new object_property($this->table, $this->id, 'regNrTraekker', 'string', true);
$this->regNrTrailer = new object_property($this->table, $this->id, 'regNrTrailer', 'string', true);
$this->washCertificateEmail = new object_property($this->table, $this->id, 'washCertificateEmail', 'string', true);
$this->date = new object_property($this->table, $this->id, 'date', 'string', true);
$this->department = new object_property($this->table, $this->id, 'department', 'int', true);
$this->pickup_bool = new object_property($this->table, $this->id, 'pickup_bool', 'int', true);
$this->notes = new object_property($this->table, $this->id, 'notes', 'string', true);
$this->washCertificateStatus = new object_property($this->table, $this->id, 'washCertificateStatus', 'string', true);
$this->washCertificateUrl = new object_property($this->table, $this->id, 'washCertificateUrl', 'string', true);
$this->status = new object_property($this->table, $this->id, 'status', 'string', true);
}
public function parseBookings(array $listObjectsWithPaginationIfSet): array
{
// Parse the customer numbers
$bookings = $this->parseCustomerNumbers($listObjectsWithPaginationIfSet);
return $bookings;
}
public function parseCustomerNumbers(array $listObjectsWithPaginationIfSet): array
{
// Parse the customer numbers
foreach ( $listObjectsWithPaginationIfSet as $key => $value ) {
$listObjectsWithPaginationIfSet[$key]['customer_name'] = (new users_o())->getCustomerName($value['customer_number']);
}
return $listObjectsWithPaginationIfSet;
}
/**
* @param int $customer_number
* @param string $wash_type
* @param string $contact_email
* @param string $reference_number
* @param string $regNrTraekker
* @param string $regNrTrailer
* @param string $washCertificateEmail
* @param string $date
* @param string $department
* @param int $pickup_bool
* @param string $notes
* @param string $washCertificateStatus
* @param string $washCertificateUrl
* @param string $status
* @return void
*/
public function add(int $customer_number, string $wash_type, string $contact_email, string $reference_number, string $regNrTraekker, string $regNrTrailer, string $washCertificateEmail, string $date, string $department, int $pickup_bool, string $notes, string $washCertificateStatus, string $washCertificateUrl, string $status): void
{
global $db;
// Avoid SQL injection
$wash_type = $db->escape_string($wash_type);
$contact_email = $db->escape_string($contact_email);
$reference_number = $db->escape_string($reference_number);
$regNrTraekker = $db->escape_string($regNrTraekker);
$regNrTrailer = $db->escape_string($regNrTrailer);
$washCertificateEmail = $db->escape_string($washCertificateEmail);
$date = $db->escape_string($date);
$department = $db->escape_string($department);
$notes = $db->escape_string($notes);
$washCertificateStatus = $db->escape_string($washCertificateStatus);
$washCertificateUrl = $db->escape_string($washCertificateUrl);
$status = $db->escape_string($status);
// Create a new record in the database ( Replace the code, if an entry already exists )
$sql = "INSERT INTO $this->table (customer_number, wash_type, contact_email, reference_number, regNrTraekker, regNrTrailer, washCertificateEmail, date, department, pickup_bool, notes, washCertificateStatus, washCertificateUrl, status) VALUES ($customer_number, '$wash_type', '$contact_email', '$reference_number', '$regNrTraekker', '$regNrTrailer', '$washCertificateEmail', '$date', '$department', $pickup_bool, '$notes', '$washCertificateStatus', '$washCertificateUrl', '$status')";
$db->query($sql);
// Clear the cache
redis->clear_department_booking_count($department);
// Get the id of the new record
$this->id = $db->insert_id();
}
/**
* Generate a wash certificate for the booking
* @param string|null $safety_seal
* @param string|null $operator
* @return void
* @throws Exception If the booking is cancelled
* @throws Exception If the booking is completed
* @throws Exception If the booking status is unknown
*/
public function generateWashCertificate(string|null $safety_seal, string|null $operator): void
{
self::requireSelected();
// Prepare the date and time
$date = date('d-m-Y', strtotime($this->date->value()));
$time = date('H:i', strtotime($this->date->value()));
// Get the customer name
$customer_name = (new users_o())->getCustomerName($this->customer_number->value());
$pdf_generator = new pdf_generator();
$pdf_generator->add_html($pdf_generator->templates->getTemplate('wash_certificate')
->setCompany([
'name' => 'Truck Wash',
'address' => 'Letland Allé 2',
'zip' => 2630,
'city' => 'Taastrup',
'phone_prefix' => 45,
'phone' => 43717886,
'email' => 'cph@truckwash.dk',
'website' => 'www.truckwash.dk',
'images' => [
'logo' => '/truckwash-banner-png.png',
'banner' => '/truckwash-banner-png.png',
'signature' => '/truckwash-underskrift.png',
],
])
->addData([
'seal_number' => $safety_seal,
'reg_1' => $this->regNrTraekker->value(),
'reg_2' => $this->regNrTrailer->value(),
'date' => $date,
'time' => $time,
'carried_out_by' => $operator,
'department_id' => $this->department->value(),
'customer_name' => $customer_name,
'type' => $this->wash_type->value(),
])
->getHtml()
);
$pdf_path = $pdf_generator->generate_pdf();
// Set the wash certificate url
$this->washCertificateUrl->set($pdf_path);
// Set the wash certificate status
$this->washCertificateStatus->set('completed');
// Set the status to completed
$this->status->set('completed');
}
public function sendWashCertificateEmail(string $email): void
{
// Send the wash certificate email
$email = new email();
$email->sendWashCertificateEmail(
$this->id,
(new users_o())->getCustomerName($this->customer_number->value()),
$this->customer_number->value(),
$email,
$this->reference_number->value(),
strtoupper($this->regNrTraekker->value()),
strtoupper($this->regNrTrailer->value()),
$this->washCertificateUrl->value()
);
}
}
+34 -3
View File
@@ -8,6 +8,7 @@ use classes\response;
use classes\slack;
use classes\wash_certificate_store;
use classes\wordpress_bookings_remote;
use Exception;
use traits\db_object_t;
class bookings_o extends db
@@ -31,7 +32,7 @@ class bookings_o extends db
public function structure(): void
{
$this->setTable('bookings');
$this->setTable('bookings_new');
}
public function objectChanged(): void
@@ -118,6 +119,11 @@ class bookings_o extends db
public function syncBookings(): array
{
return [
"bookings" => 0,
"cancelled" => 0,
"parsed" => 0,
];
global /** @var response $response */
$db, $response;
$wordpress_bookings_remote = new wordpress_bookings_remote();
@@ -213,7 +219,7 @@ class bookings_o extends db
$washCertificateUrl,
$status
));
} catch (\Exception $e) {
} catch (Exception $e) {
// Log the error
$logs = new logs_o();
$logs->add('slack', 0, 3, 0, 'SEND_DEPARTMENT_BOOKING_NOTIFICATION', $e->getMessage());
@@ -306,7 +312,7 @@ class bookings_o extends db
$slack = new slack();
try {
$slack->send_department_booking_notification($department, 'There are ' . $count . ' unfulfilled bookings');
} catch (\Exception $e) {
} catch (Exception $e) {
// Log the error
$logs = new logs_o();
$logs->add('slack', 0, 3, 0, 'SEND_DEPARTMENT_UNFULFILLED_BOOKINGS_NOTIFICATION', $e->getMessage());
@@ -339,4 +345,29 @@ class bookings_o extends db
$db->query($sql);
}
/**
* @throws Exception If the object is not selected
*/
public function asArray(): array
{
self::requireSelected();
return [
'id' => (int)$this->id,
'customer_number' => (int)$this->customer_number->value(),
'wash_type' => $this->wash_type->value(),
'contact_email' => $this->contact_email->value(),
'reference_number' => $this->reference_number->value(),
'regNrTraekker' => $this->regNrTraekker->value(),
'regNrTrailer' => $this->regNrTrailer->value(),
'washCertificateEmail' => $this->washCertificateEmail->value(),
'date' => $this->date->value(),
'department' => (int)$this->department->value(),
'pickup_bool' => $this->pickup_bool->value() ? true : false,
'notes' => $this->notes->value(),
'washCertificateStatus' => $this->washCertificateStatus->value(),
'washCertificateUrl' => $this->washCertificateUrl->value(),
'status' => $this->status->value()
];
}
}
@@ -35,6 +35,25 @@ class bookingsRoute
$response->add_meta('wash_certificate_token', $EMAIL_WASH_CERTIFICATE_TOKEN);
}
$bookings_o = new bookings_o();
// Check if the user has specified a booking id
if (self::isParametersSet(['id'])) {
// Check if the booking id is a number
if (!is_numeric(self::getParameter('id'))) {
$response->error('Booking ID must be a number', 400);
}
// Check if the booking exists
if (!$bookings_o->select((int)self::getParameter('id'))->exists()) {
$response->error('Booking not found', 404);
}
// Check if the user has access to the booking
if (!$user->hasAccessToBooking((int)self::getParameter('id'))) {
$response->error('You are not allowed to access this booking', 403);
}
// Return the booking
$response->success(
$bookings_o->asArray()
);
}
// Return the list of bookings
$response->success(
$bookings_o->parseBookings($bookings_o->listObjectsWithPaginationIfSet(
+25 -2
View File
@@ -110,6 +110,7 @@ trait form_t
*/
public function __construct()
{
parent::__construct();
self::setup();
}
@@ -188,8 +189,19 @@ trait form_t
$callable = is_callable($validation_method) ? $validation_method : [$this, $validation_method];
$is_valid = call_user_func($callable, $DATA[self::getFormFieldPrefix() . $field]);
if (!$is_valid) {
// If the value is empty, and the field isn't required, we don't need to add it to the errors
if (empty($DATA[self::getFormFieldPrefix() . $field]) && self::isFieldRequired($field)) {
// Check if the value is empty, and the field isn't required
// If the field is required, we don't need to add it to the errors
// If the field is not required, we need to add it to the errors
if (self::isFieldRequired($field)) {
// If the value is not valid, add it to the errors
$errors[self::getFormFieldPrefix() . $field] = [
'error' => 'The field is invalid, please check the format',
'field' => self::getFormFieldPrefix() . $field,
];
} else if (!self::isFieldRequired($field) && empty($DATA[self::getFormFieldPrefix() . $field])) {
// If the value is empty, and the field isn't required, we don't need to add it to the errors
} else {
// If the value is not valid, add it to the errors
$errors[self::getFormFieldPrefix() . $field] = [
'error' => 'The field is invalid, please check the format',
@@ -554,6 +566,17 @@ trait form_t
return preg_match('/^[+-]?[0-9]{1,10}$/', $int) === 1;
}
/**
* Validation method for booking ids
* @param int $int The integer to validate
* @return bool True if the integer is valid, false otherwise
*/
public function validateBookingId(int $int): bool
{
// Check if the booking id is valid (A positive integer)
return preg_match('/^[1-9][0-9]*$/', $int) === 1;
}
/**
* Validation method for strings
* @param string $string The string to validate