diff --git a/services/nginx/app/classes/form.php b/services/nginx/app/classes/form.php index c153b8dc..d6afbc90 100644 --- a/services/nginx/app/classes/form.php +++ b/services/nginx/app/classes/form.php @@ -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(); } /** diff --git a/services/nginx/app/modules/email/templates/email_template_wash_certificate.php b/services/nginx/app/modules/email/templates/email_template_wash_certificate.php new file mode 100644 index 00000000..d43b03d4 --- /dev/null +++ b/services/nginx/app/modules/email/templates/email_template_wash_certificate.php @@ -0,0 +1,50 @@ +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 + ?> + + +
Kære = $this->company_name ?>,
+Din ordre er udført.
+Vi har vedhæftet dit vaskecertifikat til denne email.
+Tak fordi du valgte Truck Wash – vi sætter stor pris på dit samarbejde.
+Har du behov for yderligere assistance, er vi kun en e-mail eller et opkald væk.
+De bedste hilsner - Kind regards
+Truck Wash
+ + + 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'), diff --git a/services/nginx/app/modules/forms/form_helper_c.php b/services/nginx/app/modules/forms/form_helper_c.php index 9bdd19d9..ee262d19 100644 --- a/services/nginx/app/modules/forms/form_helper_c.php +++ b/services/nginx/app/modules/forms/form_helper_c.php @@ -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 { } \ No newline at end of file diff --git a/services/nginx/app/modules/forms/form_restrictions_helper_c.php b/services/nginx/app/modules/forms/form_restrictions_helper_c.php new file mode 100644 index 00000000..35638b7c --- /dev/null +++ b/services/nginx/app/modules/forms/form_restrictions_helper_c.php @@ -0,0 +1,62 @@ +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'); + } + } +} \ No newline at end of file diff --git a/services/nginx/app/modules/forms/generate_booking_wash_certificate_f.php b/services/nginx/app/modules/forms/generate_booking_wash_certificate_f.php new file mode 100644 index 00000000..8da3a522 --- /dev/null +++ b/services/nginx/app/modules/forms/generate_booking_wash_certificate_f.php @@ -0,0 +1,124 @@ +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' => '', + ], + ]); + + } +} \ No newline at end of file diff --git a/services/nginx/app/objects/bookings_new_o.php b/services/nginx/app/objects/bookings_new_o.php new file mode 100644 index 00000000..a269a2dd --- /dev/null +++ b/services/nginx/app/objects/bookings_new_o.php @@ -0,0 +1,212 @@ +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() + ); + } + +} \ No newline at end of file diff --git a/services/nginx/app/objects/bookings_o.php b/services/nginx/app/objects/bookings_o.php index 87119869..ab0fc06b 100644 --- a/services/nginx/app/objects/bookings_o.php +++ b/services/nginx/app/objects/bookings_o.php @@ -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() + ]; + } + } \ No newline at end of file diff --git a/services/nginx/app/routes/bookingsRoute.php b/services/nginx/app/routes/bookingsRoute.php index b717b3b3..7544b022 100644 --- a/services/nginx/app/routes/bookingsRoute.php +++ b/services/nginx/app/routes/bookingsRoute.php @@ -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( diff --git a/services/nginx/app/traits/form_t.php b/services/nginx/app/traits/form_t.php index cfdae5ee..62e42176 100644 --- a/services/nginx/app/traits/form_t.php +++ b/services/nginx/app/traits/form_t.php @@ -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