Merge pull request #17 from copenhagentruckwash/development

Enhance booking and customer data handling
This commit is contained in:
Jeppe B
2025-04-16 14:00:26 +02:00
committed by GitHub
7 changed files with 354 additions and 112 deletions
@@ -81,6 +81,8 @@ class generate_booking_wash_certificate_f extends form_helper_c
);
// Send the wash certificate to the customer
$bookings->sendWashCertificateToCustomer();
// Create the transaction based on the booking
$transaction = $bookings->createTransaction();
// Mark the booking as completed
$bookings->status->set('completed');
$bookings->washCertificateStatus->set('completed');
+252 -7
View File
@@ -356,12 +356,23 @@ class bookings_o extends db
}
/**
* Convert the object to an array
* @options include_customer_name Include the customer name
* @options include_department_name Include the department name
* @options include_parsed_services Include the parsed services
* @param array $options [@options] The options to include in the array
* @return array The object as an array
* @throws Exception If the object is not selected
*/
public function asArray(): array
public function asArray(array $options = []): array
{
self::requireSelected();
return [
$options = array_merge([
'include_customer_name' => false,
'include_department_name' => false,
'include_parsed_services' => false,
], $options);
$tmp_array = [
'id' => (int)$this->id,
'customer_number' => (int)$this->customer_number->value(),
'wash_type' => $this->wash_type->value(),
@@ -380,16 +391,39 @@ class bookings_o extends db
'status' => $this->status->value(),
'data' => $this->data->value() ? json_decode($this->data->value(), true) : null,
];
// Include the customer name
if ($options['include_customer_name']) {
$customer = (new users_o())->getUserByCustomerNumber((int)$this->customer_number->value());
$tmp_array['customer_name'] = $customer->getCustomerName($this->customer_number->value());
}
// Include the department name
if ($options['include_department_name']) {
$department = (new departments_o())->select((int)$this->department->value());
if ($department->exists()) {
$tmp_array['department_name'] = $department->name->value();
}
}
// Include the parsed services
if ($options['include_parsed_services']) {
// Format the wash type from the services array as followed:
// 1 => 'Udvendig sættevognstræk ( Trækker / trailer )',
$tmp_array['parsed_services']['string'] = $this->formatWashTypeFromServices(
$this->data->value() ? json_decode($this->data->value(), true) : []
);
$tmp_array['parsed_services']['array'] = $this->data->value() ? json_decode($this->data->value(), true) : [];
}
return $tmp_array;
}
/**
* Format the wash type from the services array
* @param array $services The services array
* @return string The formatted wash type
*/
private function formatWashTypeFromServices(array $services): string
{
// Format the wash type from the services array
$wash_services = [
1 => 'Udvendig sættevognstræk ( Trækker / trailer )',
2 => 'Udvendigt trailer vask',
3 => 'Indvendig trailer vask',
];
$wash_services = self::getWashServicesString(self::getWashServices());
$string = '';
foreach ( $services as $service ) {
if (isset($wash_services[$service])) {
@@ -400,6 +434,89 @@ class bookings_o extends db
return rtrim($string, ', ');
}
private static function getWashServicesString(array $services): array
{
// Format the wash services as a string (product1, product2, product3)
$string = [];
foreach ( $services as $key => $service ) {
if (isset($service['overrides']['name'])) {
$string[$key] = $service['overrides']['name'];
} else {
$string[$key] = $service['name'];
}
}
return $string;
}
/**
* @throws Exception
*/
private function getWashServices(): array
{
// Define the product function
// Get the wash services from the database
return [
// Exterior wash "Sættevognstræk"
1 => self::defineProduct(1, 3, [
'name' => 'Udvendig sættevognstræk ( Trækker / trailer )',
'description' => 'Udvendig vask af sættevognstræk',
]),
// Exterior trailer wash "Udvendig trailer vask"
2 => self::defineProduct(2, 2, [
'name' => 'Udvendig trailer vask',
'description' => 'Udvendig vask af trailer',
]),
3 => self::defineProduct(3, 10, [
'name' => 'Indvendig trailer vask',
'description' => 'Indvendig vask af trailer',
]),
4 => self::defineProduct(41, 41),
];
}
/**
* Define a product for the booking
* @param int $id
* @param int $product_id
* @param array $overrides
* @param array $options
* @param array $addons
* @return array
* @throws Exception
* @see getWashServices()
* @see getWashServicesString()
*/
private function defineProduct(int $id, int $product_id, array $overrides = [], array $options = [], array $addons = []): array
{
// Check if the product exists
if (!empty($product_id)) {
$product_obj = (new products_o())->select((int)$product_id);
if ($product_obj->exists()) {
$default_options = $product_obj->asArray();
}
}
// Return the product
return [
'id' => (int)$id,
'product_id' => (int)$product_id,
'addons' => [...$addons],
...($default_options ?? []),
'overrides' => [
'name' => null,
'quantity' => null,
'description' => null,
'notes' => null,
'reference' => null,
...$overrides,
],
...$options,
];
}
public function checkUnfulfilledBookings(): void
{
$unfulfilled_bookings = [];
@@ -543,6 +660,10 @@ class bookings_o extends db
$pdf_path = $pdf_generator->generate_pdf();
// Set the PDF in the booking
$this->wash_certificate_pdf->set($pdf_path);
// Add the wash certificate to the bookings data (If it does not exist)
if (!self::hasServiceInBookingData(4)) {
self::addServiceToBookingData(4);
}
}
/**
@@ -559,6 +680,37 @@ class bookings_o extends db
return false;
}
/**
* Check if the booking data has a service
* @param int $service_id The service id
* @return bool True if the service exists, false otherwise
* @throws Exception If the object is not selected
*/
public function hasServiceInBookingData(int $service_id): bool
{
self::requireSelected();
// Get the current data
$data = $this->data->value() ? json_decode($this->data->value(), true) : [];
// Check if the service exists
return in_array($service_id, $data);
}
/**
* Add a service to the booking data
* @param int $service_id The service id
* @throws Exception If the object is not selected
*/
public function addServiceToBookingData(int $service_id): void
{
self::requireSelected();
// Get the current data
$data = $this->data->value() ? json_decode($this->data->value(), true) : [];
// Add the service to the data
$data[] = (int)$service_id;
// Set the data
$this->data->set(json_encode($data));
}
/**
* Send the wash certificate to the customer
* @throws Exception If the object is not selected
@@ -584,4 +736,97 @@ class bookings_o extends db
}
/**
* @throws Exception
*/
public function createTransaction(): orders_o
{
global $response;
self::requireSelected();
// Get the services from the booking
$services = $this->data->value() ? json_decode($this->data->value(), true) : [];
// Get the department from the booking
$department = (new departments_o())->select((int)$this->department->value());
if (!$department->exists()) {
throw new Exception('Department not found');
}
// Create a new order
$order = new orders_o();
$order->add(
(int)$this->customer_number->value(),
(int)$this->id,
(string)$this->reference_number->value(),
(string)$this->notes->value(),
(int)$this->department->value(),
(string)$this->regNrTraekker->value(),
(string)$this->regNrTrailer->value(),
);
// Set the orders booking id
$order->booking_id->set((int)$this->id);
// Define the relation order item ids, when applicable.
$relations = [
// This is the order item id for the primary product, the wash certificate applies to.
// This can be left null, if the product does not exist.
4 => null,
];
// Sort the services, so the primary product is first, and the services with relations are last.
$services_sorted = [];
$services_not_relation = [];
$services_relation = [];
foreach ( $services as $service ) {
// Get the service id
$service_id = (int)$service;
// Check if the service has the posibility to have a relation (null values are allowed)
if (in_array($service_id, array_keys($relations))) {
$services_relation[] = $service_id;
} else {
$services_not_relation[] = $service_id;
}
}
// Sort the services, so the primary product is first, and the services with (potential) relations are last.
$services_sorted = [
...$services_not_relation,
...$services_relation,
];
// Add the services to the order
foreach ( $services_sorted as $service_id ) {
$service = self::getWashServiceProduct((int)$service_id);
// Add the service to the order
$tmp_order_item = new order_items_o();
$tmp_order_item->addItemToOrder(
$order->id,
(int)$service->id,
(int)$response->get_user()->id,
(int)1,
$relations[(int)$service_id] ?? null,
);
// Check if the service allows for a wash certificate
if ((int)$service_id === 3) {
// Set the relation id for the wash certificate
$relations[4] = (int)$tmp_order_item->id;
}
}
// Return the order
return $order;
}
/**
* @throws Exception
*/
private function getWashServiceProduct(int $service_id): products_o
{
// Get the product id from the service id
$services = self::getWashServices();
if (!isset($services[$service_id])) {
throw new Exception('Service not found');
}
$product_id = $services[$service_id]['product_id'];
// Get the wash service product from the database
$product = (new products_o())->select($product_id);
if (!$product->exists()) {
throw new Exception('Product not found');
}
return $product;
}
}
@@ -63,61 +63,6 @@ class department_daily_reports_o extends db
];
}
/**
* Add a department daily report
* @param int $department_id The id of the department
* @param int $water_usage The cm3 of water used in the department today
* @param int $water_usage_morning The cm3 of water used in the department (Checked in the morning)
* @param string $notes Any notes for the report
* @param int $filled_by The user (id) that filled the report
* @return department_daily_reports_o
* @throws Exception If the object was not created successfully
*/
public function add(
int $department_id,
int $water_usage,
int $water_usage_morning,
string $notes,
int $filled_by,
string $created_at = null
): department_daily_reports_o
{
// Validate the created_at date, if provided
if ($created_at !== null) {
$dateTime = \DateTime::createFromFormat('Y-m-d', $created_at);
if (!$dateTime || $dateTime->format('Y-m-d') !== $created_at) {
throw new Exception('Invalid date format for created_at. Expected format: YYYY-MM-DD');
}
}
$tmp_id = $this->add_object([
'department_id' => (int)$department_id,
'water_usage' => (int)$water_usage,
'water_usage_morning' => (int)$water_usage_morning,
'notes' => (string)$notes,
'filled_by' => (int)$filled_by,
'created_at' => $created_at ? $created_at : date('Y-m-d H:i:s'),
]);
$this->id = $tmp_id;
$this->getObjectProperties();
$this->objectChanged();
return $this;
}
public function getObjectProperties(): void
{
$this->department_id = new object_property($this->table, $this->id, 'department_id', 'int', false);
$this->water_usage = new object_property($this->table, $this->id, 'water_usage', 'int', false);
$this->water_usage_morning = new object_property($this->table, $this->id, 'water_usage_morning', 'int', false);
$this->notes = new object_property($this->table, $this->id, 'notes', 'string', false);
$this->filled_by = new object_property($this->table, $this->id, 'filled_by', 'int', false);
$this->created_at = new object_property($this->table, $this->id, 'created_at', 'string', false);
}
public function objectChanged(): void
{
//TODO: Add cache invalidation
}
/**
* Get the amount of products sold on a given date
* @note This does not count products from removed orders, only products from orders that are still active
@@ -429,7 +374,8 @@ class department_daily_reports_o extends db
{
global $db;
if (!$this->doesDepartmentDailyReportExist($department_id, $date)) {
throw new Exception('Department daily report does not exist');
// Create a new report for the given date, if it does not exist
return $this->add($department_id, 0, 0, '', 0, $date);
}
$conn = $db->conn();
$stmt = $conn->prepare(
@@ -458,6 +404,61 @@ class department_daily_reports_o extends db
return $this;
}
/**
* Add a department daily report
* @param int $department_id The id of the department
* @param int $water_usage The cm3 of water used in the department today
* @param int $water_usage_morning The cm3 of water used in the department (Checked in the morning)
* @param string $notes Any notes for the report
* @param int $filled_by The user (id) that filled the report
* @return department_daily_reports_o
* @throws Exception If the object was not created successfully
*/
public function add(
int $department_id,
int $water_usage,
int $water_usage_morning,
string $notes,
int $filled_by,
string $created_at = null
): department_daily_reports_o
{
// Validate the created_at date, if provided
if ($created_at !== null) {
$dateTime = \DateTime::createFromFormat('Y-m-d', $created_at);
if (!$dateTime || $dateTime->format('Y-m-d') !== $created_at) {
throw new Exception('Invalid date format for created_at. Expected format: YYYY-MM-DD');
}
}
$tmp_id = $this->add_object([
'department_id' => (int)$department_id,
'water_usage' => (int)$water_usage,
'water_usage_morning' => (int)$water_usage_morning,
'notes' => (string)$notes,
'filled_by' => (int)$filled_by,
'created_at' => $created_at ? $created_at : date('Y-m-d H:i:s'),
]);
$this->id = $tmp_id;
$this->getObjectProperties();
$this->objectChanged();
return $this;
}
public function getObjectProperties(): void
{
$this->department_id = new object_property($this->table, $this->id, 'department_id', 'int', false);
$this->water_usage = new object_property($this->table, $this->id, 'water_usage', 'int', false);
$this->water_usage_morning = new object_property($this->table, $this->id, 'water_usage_morning', 'int', false);
$this->notes = new object_property($this->table, $this->id, 'notes', 'string', false);
$this->filled_by = new object_property($this->table, $this->id, 'filled_by', 'int', false);
$this->created_at = new object_property($this->table, $this->id, 'created_at', 'string', false);
}
public function objectChanged(): void
{
//TODO: Add cache invalidation
}
public function hasReport(int $department_id, string $date): bool
{
return $this->doesDepartmentDailyReportExist($department_id, $date);
+3
View File
@@ -28,6 +28,7 @@ class orders_o extends db
public object_property $completed_at;
public stripe_module_orders_o $stripe_module_orders;
public object_property $invoice_collection_id;
public object_property $booking_id;
public function structure(): void
{
@@ -69,6 +70,7 @@ class orders_o extends db
$this->stripe_module_orders = (new stripe_module_orders_o())->select($this->id);
$this->deleted_at = new object_property($this->table, $this->id, 'deleted_at', 'timestamp', false);
$this->invoice_collection_id = new object_property($this->table, $this->id, 'invoice_collection_id', 'int', false);
$this->booking_id = new object_property($this->table, $this->id, 'booking_id', 'int', false);
}
@@ -158,6 +160,7 @@ class orders_o extends db
'deleted_at' => $this->deleted_at->value(),
'total_net_amount' => $this->getNetAmount(),
'invoice_collection_id' => (int)$this->invoice_collection_id->value(),
'booking_id' => (int)$this->booking_id->value(),
'closed_at' => (int)$this->invoice_collection_id->value() ? (new collected_order_invoices_o())->select((int)$this->invoice_collection_id->value())->closed_at->value() : null,
];
}
@@ -112,12 +112,16 @@ class user_key_value_pairs_o extends db
{
global $db;
// Get all the users, and their customer numbers, with the given keys
$sql = "SELECT user_id FROM $this->table WHERE var IN ('" . implode("','", $keys) . "')";
$sql = "SELECT user_id, val FROM $this->table WHERE var IN ('" . implode("','", $keys) . "')";
$result = $db->query($sql);
$result = $db->fetch_all($result);
$user_ids = [];
// Add the user ids to the array
foreach ( $result as $row ) {
// Check if the value is empty
if (empty($row['val'])) {
continue;
}
$user_ids[] = (int)$row['user_id'];
}
// Get the customer numbers from the users table
+20 -10
View File
@@ -59,8 +59,11 @@ class bookingsRoute
$response->success(
$bookings_o->parseBookings($bookings_o->listObjectsWithPaginationIfSet(
function ($booking) {
$booking['department'] = (int)$booking['department'];
return $booking;
return (new bookings_o())->select($booking['id'])->asArray(
[
'include_parsed_services' => true,
]
);
},
$bookings_o->forceRestrictFilters(
[
@@ -96,14 +99,21 @@ class bookingsRoute
// Return the list of departments
$bookings_o = new bookings_o();
$response->success(
$bookings_o->parseBookings($bookings_o->getCustomerBookingsPaginated(
$user->customer_number->value(),
($this->fromRequest('page') ?? 1),
($this->fromRequest('limit') ?? 10),
['id' => 'DESC'],
$this->fromRequest('search') === null ? '' : $this->fromRequest('search'),
$this->fromRequest('filters') === null ? [] :
$response->parseFilters($this->fromRequest('filters')) ?? []
$bookings_o->parseBookings($bookings_o->listObjectsWithPaginationIfSet(
function ($booking) {
return (new bookings_o())->select($booking['id'])->asArray(
[
'include_parsed_services' => true,
]
);
},
$bookings_o->forceRestrictFilters(
[
'customer_number' => [
$user->customer_number->value(),
],
]
)
))
);
} else {
@@ -4,6 +4,7 @@ namespace routes;
use classes\authentication;
use objects\department_daily_reports_o;
use objects\departments_o;
use objects\logs_o;
use traits\route_t;
@@ -31,7 +32,6 @@ class departmentDailyReportsRoute
// The fields that can be searched. This would otherwise make it possible to get secret information from the database, simply by searching for it and getting the result count back
'id',
'water_usage',
'water_usage_morning',
'notes',
'filled_by',
'created_at',
@@ -43,7 +43,6 @@ class departmentDailyReportsRoute
'id' => (int)$department_report['id'],
'department_id' => (int)$department_report['department_id'],
'water_usage' => (int)$department_report['water_usage'],
'water_usage_morning' => (int)$department_report['water_usage_morning'],
'notes' => (string)$department_report['notes'],
'filled_by' => (int)$department_report['filled_by'],
'created_at' => (string)$department_report['created_at'],
@@ -133,7 +132,6 @@ class departmentDailyReportsRoute
'id' => (int)$department_daily_report_latest->id,
'department_id' => (int)$department_daily_report_latest->department_id->value(),
'water_usage' => (int)$department_daily_report_latest->water_usage->value(),
'water_usage_morning' => (int)$department_daily_report_latest->water_usage_morning->value(),
'notes' => (string)$department_daily_report_latest->notes->value(),
'filled_by' => (int)$department_daily_report_latest->filled_by->value(),
'created_at' => (string)$department_daily_report_latest->created_at->value(),
@@ -165,7 +163,6 @@ class departmentDailyReportsRoute
[
'department_id',
'water_usage',
'water_usage_morning',
'notes',
'date'
]
@@ -190,16 +187,6 @@ class departmentDailyReportsRoute
(int)self::getParameter('water_usage'),
0
);
// Validate the water_usage_morning
self::requireType(
(int)self::getParameter('water_usage_morning'),
self::type_int()
);
// Require the water_usage_morning to be at least 0
self::requireMinValue(
(int)self::getParameter('water_usage_morning'),
0
);
// Validate the notes
self::requireType(
(string)self::getParameter('notes'),
@@ -249,7 +236,7 @@ class departmentDailyReportsRoute
->add(
(int)self::getParameter('department_id'),
(int)self::getParameter('water_usage'),
(int)self::getParameter('water_usage_morning'),
(int)0,
(string)self::getParameter('notes'),
$user->id
)->asArray()
@@ -304,18 +291,6 @@ class departmentDailyReportsRoute
0
);
}
if (self::isParametersSet(['water_usage_morning'])) {
// Validate the water_usage_morning
self::requireType(
(int)self::getParameter('water_usage_morning'),
self::type_int()
);
// Require the water_usage_morning to be at least 0
self::requireMinValue(
(int)self::getParameter('water_usage_morning'),
0
);
}
if (self::isParametersSet(['notes'])) {
// Validate the notes
self::requireType(
@@ -333,21 +308,23 @@ class departmentDailyReportsRoute
4000
);
}
// Check if the report exists
// Update the department daily report
$department_daily_reports_o = new department_daily_reports_o();
$department_daily_report = $department_daily_reports_o->select((int)self::getParameter('id'));
// Determine if the user has access to the department
self::requireDepartmentAccess((int)$department_daily_report->department_id->value());
// Get the department from the daily report
$department_daily_report = new department_daily_reports_o();
$department_daily_report->select((int)self::getParameter('id'));
if (!$department_daily_report->exists()) {
$response->error('Department daily report not found', 404);
}
// Get the department
$department = (new departments_o())->select((int)$department_daily_report->department_id->value());
if (!$department->exists()) {
$response->error('Department not found', 404);
}
// Require the user to have access to the department
self::requireDepartmentAccess((int)$department->id);
// Check the parameters that are set
if (self::isParametersSet(['water_usage'])) {
$department_daily_report->water_usage->set((int)self::getParameter('water_usage'));
}
if (self::isParametersSet(['water_usage_morning'])) {
$department_daily_report->water_usage_morning->set((int)self::getParameter('water_usage_morning'));
}
if (self::isParametersSet(['notes'])) {
$department_daily_report->notes->set((string)self::getParameter('notes'));
}